【算法】回溯法四步走

回溯法

对于回溯法,网上有很多种解释,这里我依照自己的(死宅)观点做了以下三种通俗易懂的解释:

  • 正经版解释:其实人生就像一颗充满了分支的n叉树,你的每一个选择都会使你走向不同的路线,获得不同的结局。如果能重来,我要选李白~呸!说错了,如果能重来,我们就能回溯到以前,选择到最美好的结局。

  • 游戏版解释:玩过互动电影游戏(如 行尸走肉)的都知道,你的每个选择都会影响游戏的结局,掌控他人的生死。每次选择错误导致主角或配角死亡,我们是不是回溯读档,希望得到一个更好的结局。

    PS:克莱曼婷天下无敌!

  • 动漫版解释:看过主角拥有死亡回归(疯狂暗示486)的都知道,主角的每个选择都能影响大局,可是486直接能回溯重选,这与我们今天要讲的回溯法极其相似。

    PS:爱蜜莉雅、雷姆我都要!

  • 总结版解释:从众多分支的路径中,找到符合结果的路径或路径集。

专业名词

  • 解空间:即 所有的可能情况

概念

回溯算法:是类似于枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
它是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术称为回溯法,而满足回溯条件的某个状态的点称为“回溯点”(你也可以理解为存档点)。


上图为八皇后的解空间树,如果当前点不符合要求就退回再走
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

基本思想

在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。
当探索到某一结点时,要先判断该结点是否包含问题的解:

  • 如果包含,就从该结点出发继续探索下去;
  • 如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)

结束条件:

  • 若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
  • 若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

网上的一般步骤

虽然我觉得网上的一般步骤太抽象了,但是还是摆在这里供大家参考吧。。

  1. 针对所给问题,确定问题的解空间:
    首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。

  2. 确定结点的扩展搜索规则:
    及时确定规则,并不是每个解空间都要走完才能发现是死路的,有时候走到一半就发现不满足条件了。

  3. 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索:
    不满足条件的路径及时剪掉(即 剪枝),避免继续走下去浪费时间。

    类比:比如说削苹果
    我们规定:苹果皮必须不断,要完整地削完整个苹果。
    那么,如果我们削到一半苹果皮断掉了,我们就可以直接退回去(即 回溯)换个苹果削了,如果继续削下去,只会浪费时间。

算法框架

问题框架:
设问题的是一个n维向量(a1,a2,………,an)约束条件ai(i=1,2,3,…..,n)之间满足某种条件,记为 f(ai)

非递归回溯框架

其中,a[n]为解空间,i为搜索的深度,框架如下:

int a[n],i; //a[n]为解空间,i为深度
初始化数组 a[]; 
i = 1; 
while (i>0(有路可走) and (未达到目标)) { //还未回溯到头
    if(i > n) { //搜索到叶结点 
        搜索到一个解,输出; 
    } else {    //处理第 i 个元素 
        a[i]第一个可能的值; 
        while(a[i]在不满足约束条件且在搜索空间内) {
            a[i]下一个可能的值; 
        }//while 
        if(a[i]在搜索空间内) {
            标识占用的资源; 
            i = i+1; //扩展下一个结点 
        } else { 
            清理所占的状态空间; //回溯 
            i = i – 1; 
        }//else 
    }//else 
}//while

递归回溯框架

回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单。
其中,a[n]为解空间,i为搜索的深度,框架如下:

int a[n];   //a[n]为解空间
BackTrace(int i) {  //尝试函数,i为深度
    if(i>n) {
        输出结果;
    } else { 
        for(j = 下界; j <= 上界; j=j+1) {   //枚举 i 所有可能的路径 
            if(check(j)) {  //检查满足限界函数和约束条件 
                a[i] = j; 
                ... //其他操作 
                BackTrace(i+1); 
                回溯前的清理工作(如 a[i]置空值等); 
            }//if 
        }//for 
    }//else 
}//BackTrace

适用场景

回溯法一般使用在问题可以树形化表示时的场景。

这样说明的话可能有点抽象,那么我们来换个方法说明。
当你发现,你的问题需要用到多重循环,具体几重循环你又没办法确定,那么就可以使用我们的回溯算法来将循环一层一层的进行嵌套。

就像这样:

void f(int count) {
      if (count == max) {
            return;
      }

      for (...) {
            f(count+1);
      }
}

这样套起来的话,无论多少重循环我们都可以满足。

回溯四步走

由于上述网上的步骤太抽象了,所以在这里我自己总结了回溯四步走:

  • 编写检测函数:检测函数用来检测此路径是否满足题目条件,是否能通过。

    这步不做硬性要求。。不一定需要

  1. 明确函数功能:要清楚你写这个函数是想要做什么;

    注意:因为回溯法我们一般只关心叶子结点的结果,中间的过程函数一般没什么返回的作用,所以函数返回值类型一般为void。
    当然,也不排除中间节点返回的结果有作用的情况。不过这种情况也可以用递归函数的方法参数来存放以及利用所有返回的结果。

  2. 寻找递归出口:一般为某深度,或叶子节点。决定递归出去时要执行的操作。

    特别注意:每次提交数组的集合(即 List<List<>>)的时候,都要记得创建一个新的数组来存放结果数组元素(即 new List<>(list)),不然后面操作的都是加入集合后的那个数组。

  3. 明确所有路径(选择)这个构思路径最好用树形图表示。

    例如:走迷宫有上下左右四个方向,也就是说我们站在一个点处有四种选择,我们可以画成无限向下延伸的四叉树。
    直到向下延伸到叶子节点,那里便是出口;
    从根节点到叶子节点沿途所经过的节点就是我们满足题目条件的选择。

  4. 回溯还原现场:该节点所有选择已做完却仍然没有找到出口,那么我们需要回溯还原现场,将该节点重置为初始状态,回溯到一切都没有发生的时候,再退回去。(与递归函数之前的操作对称

    特别注意:这意味着我们使用递归函数那行的前后操作是对称的!
    注意:回溯还原现场是必要的,如果不还原现场,那你的回溯有什么意义呢。。

    类比:大雄出意外了,哆啦A梦坐时空机回到过去想要改变这一切,结果过去的一切都没有被重置到初始状态,回到过去大雄还是现在这种受伤的样子没有改变,那么回到过去有什么意义呢。

编写检测函数(非必须)

第一步,写出检测函数,来检测这个路径是否满足条件,是否能通过。
这个函数依据题目要求来编写,当然,如果要求不止一个,可能需要编写多个检测函数。

例如:凑算式


这个算式中A~I代表1~9的数字,不同的字母代表不同的数字。

比如:
6+8/3+952/714 就是一种解法,
5+3/1+972/486 是另一种解法。

这个算式一共有多少种解法?

要做出这个题,
第一步,要写出检测函数

public static int sum = 0; // 用来存放总共的解法数
public static double[] a = new double[10];

// 判断数组里前j个元素是否与t相同
/**
 * @param a 传入一个数组a
 * @param j 判断前j个元素
 * @param t 是否与t相同
 * @return
 */
public static boolean same(double[] a,int j,int t) {
    for (int i = 1; i < j; i++) {
        if (a[i] == t) {
            return true;
        }
    }
    return false;

}

/**
 * @param a 判断a数组是否满足表达式
 * @return 如果满足就true,不满足就false
 */
public static boolean expression(double[] a) {
    if ((a[1] + a[2] / a[3] + (a[4] * 100 + a[5] * 10 + a[6]) / (a[7] * 100 + a[8] * 10 + a[9]) == 10))
        return true;
    else
        return false;
}

明确函数功能

由于此题要填数字,所以我们定义choose(i)的含义为:在算式中自动填入数字 i 。

寻找递归出口

第二步,要寻找递归出口,当1~9均已填入后,判断表达式是否成立,若成立,则输出。

// 如果选择的数字大于9,则代表1~9均已选完,判断是否满足表达式,输出选择的表达式
if (i > 9) {
    if (expression(a)) {
        for (int x = 1; x < 10; x++) {
            System.out.print(a[x] + " ");
        }
        System.out.println();
        sum++;
    }
    return;
}

明确所有路径

第三步,要知道这个递归是几个选择,即 几叉树。

此题为1~9九个选择,九条路,九叉树。

for (int j = 1; j <= 9; j++) {
    // 如果将要填入的数与前面不冲突,则填入
    if (!same(a,i,j)) {
        a[i] = j;
        choose(i + 1);

    }
}

回溯还原现场

第四步,若该节点没有找到出口,则将当前位置回溯,还原现场,重新选择

在本题中,还原现场即 重置为0(表示还未填入1~9的数字)

for (int j = 1; j <= 9; j++) {
    // 如果将要填入的数与前面不冲突,则填入
    if (!same(a,j)) {
        a[i] = j;
        choose(i + 1); //递归函数的前后操作对称
        //若没有找到出口,则将当前位置重置为0,回溯,还原现场
        a[i] = 0; //你看看是不是与a[i]=j是对称的操作
    }
}

实例

凑算式


这个算式中A~I代表1~9的数字,不同的字母代表不同的数字。

比如:
6+8/3+952/714 就是一种解法,
5+3/1+972/486 是另一种解法。

这个算式一共有多少种解法?

答案:

// 凑算式
public class Sy1 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        choose(1);
        System.out.println("一共"+sum+"种解法");

    }

    public static int sum = 0; // 用来存放总共的解法数
    public static double[] a = new double[10];
    
    // 判断数组里前j个元素是否与t相同
    /**
     * @param a 传入一个数组a
     * @param j 判断前j个元素
     * @param t 是否与t相同
     * @return
     */
    public static boolean same(double[] a,int t) {
        for (int i = 1; i < j; i++) {
            if (a[i] == t) {
                return true;
            }
        }
        return false;

    }

    /**
     * @param a 判断a数组是否满足表达式
     * @return 如果满足就true,不满足就false
     */
    public static boolean expression(double[] a) {
        if ((a[1] + a[2] / a[3] + (a[4] * 100 + a[5] * 10 + a[6]) / (a[7] * 100 + a[8] * 10 + a[9]) == 10))
            return true;
        else
            return false;
    }

    /**
     * @param i 选择第i个数字 递归
     */
    public static void choose(int i) {
        // 如果选择的数字大于9,则代表1~9均已选完,输出选择的表达式
        if (i > 9) {
            if (expression(a)) {
                for (int x = 1; x < 10; x++) {
                    System.out.print(a[x] + " ");
                }
                System.out.println();
                sum++;
            }
            return;
        }

        for (int j = 1; j <= 9; j++) {
            // 如果将要填入的数与前面不冲突,则填入
            if (!same(a,j)) {
                a[i] = j;
                choose(i + 1);
                //若没有找到出口,则将当前位置重置为0,回溯,还原现场
                a[i] = 0;
            }
        }
    }
    
}

程序运行结果:

3.0 5.0 1.0 9.0 7.0 2.0 4.0 8.0 6.0 
4.0 9.0 3.0 5.0 2.0 8.0 1.0 7.0 6.0 
5.0 3.0 1.0 9.0 7.0 2.0 4.0 8.0 6.0 
5.0 4.0 3.0 7.0 2.0 6.0 1.0 9.0 8.0 
5.0 4.0 9.0 7.0 3.0 8.0 1.0 6.0 2.0 
5.0 8.0 6.0 4.0 7.0 3.0 1.0 2.0 9.0 
6.0 4.0 2.0 3.0 5.0 8.0 1.0 7.0 9.0 
6.0 4.0 2.0 7.0 1.0 8.0 3.0 5.0 9.0 
6.0 7.0 3.0 4.0 8.0 5.0 2.0 9.0 1.0 
6.0 8.0 3.0 9.0 5.0 2.0 7.0 1.0 4.0 
6.0 9.0 8.0 4.0 3.0 7.0 1.0 5.0 2.0 
7.0 1.0 4.0 9.0 6.0 8.0 3.0 5.0 2.0 
7.0 3.0 2.0 8.0 1.0 9.0 5.0 4.0 6.0 
7.0 3.0 2.0 9.0 8.0 1.0 6.0 5.0 4.0 
7.0 5.0 3.0 2.0 6.0 4.0 1.0 9.0 8.0 
7.0 5.0 3.0 9.0 1.0 2.0 6.0 8.0 4.0 
7.0 9.0 6.0 3.0 8.0 1.0 2.0 5.0 4.0 
7.0 9.0 6.0 8.0 1.0 3.0 5.0 4.0 2.0 
8.0 1.0 3.0 4.0 6.0 5.0 2.0 7.0 9.0 
8.0 6.0 9.0 7.0 1.0 2.0 5.0 3.0 4.0 
8.0 7.0 6.0 1.0 9.0 5.0 2.0 3.0 4.0 
9.0 1.0 3.0 4.0 5.0 2.0 6.0 7.0 8.0 
9.0 1.0 3.0 5.0 2.0 4.0 7.0 8.0 6.0 
9.0 2.0 4.0 1.0 7.0 8.0 3.0 5.0 6.0 
9.0 2.0 4.0 3.0 5.0 8.0 7.0 1.0 6.0 
9.0 3.0 4.0 1.0 5.0 7.0 6.0 2.0 8.0 
9.0 4.0 8.0 1.0 7.0 6.0 3.0 5.0 2.0 
9.0 4.0 8.0 3.0 5.0 6.0 7.0 1.0 2.0 
9.0 6.0 8.0 1.0 4.0 3.0 5.0 7.0 2.0 
一共29种解法

方格填数

如下的10个格子填入0~9的数字。

  • 要求:连续的两个数字不能相邻。(左右、上下、对角都算相邻)

一共有多少种可能的填数方案?

答案:

// 方格填数
public class Sy2 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Block bk = new Block();
        bk.init();
        bk.addNum(0);//,0);
        System.out.println("一共"+Block.sum+"种方案");
    }

}

class Block {
    public int[][] b = new int[3][4];
    public static int sum;

    /**
     * 初始化整个数组
     */
    public void init() {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 4; j++) {
                b[i][j] = -2;
            }
        }
    }

    /**
     * @param y y行
     * @param x x列
     * @param n 填数n
     * @return 返回此方格是否能填数
     */
    public boolean isAble(int y,int x,int n) {
        // y行 x列 填数n
        if (b[y][x] != -2)
            return false;
        for (int j = y - 1; j <= y + 1; j++) {
            for (int i = x - 1; i <= x + 1; i++) {
                if (j < 3 && j >= 0 && i < 4 && i >= 0) {
                    if (b[j][i] == n - 1 || b[j][i] == n + 1) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * @param n 填入数字n
     */
    public void addNum(int n) {
        if (n > 9) {
            sum++;
            return;
        }
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 4; j++) {
                if ((i == 0 && j == 0) || (i == 2 && j == 3))
                    continue;
                // 如果此方格能填数,则填入数字
                if (this.isAble(i,j,n)) {
                    b[i][j] = n;
                    this.addNum(n + 1);//,y,x+1);
                    b[i][j] = -2; // 当加入下一个不行返回后,还原现在方块,继续循环
                }
            }
        }
    }
    
}

程序运行结果:

一共1580种方案

蛙跳河

在一个 5*5 的地图上,一只蛙欲从起点跳到目的地。中间有一条河(如图),但这只蛙不会游泳,并且每次跳只能横着跳一格或者竖着跳一格。(聪明的蛙不会跳已经跳过的路)

  1. 总共有多少种跳法。
  2. 给出路径最短的跳法。

答案:

  • 明确函数功能:jump(m,n)为跳到(m,n)位置。
  • 寻找递归出口:不在边界之内 或 已走过。
  • 明确所有路径:右跳、左跳、下跳、上跳
  • 回溯还原现场:
    path--; // 回溯法关键步骤
    a[m][n] = 0;
//青蛙跳
public class Sy1 {
    static int count = 0; // 跳法种类计数
    static int x = 4,y = 4; // 目的坐标
    static int step = 0; // 记录步数
    // 地图,0代表没有走过,1 代表已经走过
    static int[][] map = { { 0,0 },{ 0,{ 1,1,1 },0 } };
    static int min = 25; // 用来记录最小步数
    static int sx[] = new int[25],sy[] = new int[25]; // 记录坐标

    // 求解总共跳法,并求出最短步数,方便下面列出路径
    static void jump(int m,int n) {
        // 该点在地图边界之外或者走过
        if (m < 0 || m >= 5 || n < 0 || n >= 5 || map[m][n] != 0) { 
            return;
        }
        
        map[m][n] = 1;  // 走到此节点
        step++;
        
        if (m == x && n == y) { // 如果到达目的地
            if (step < min)// 更新最短步数
                min = step;
            count++;
        }
            
        // 所有路径
        jump(m + 1,n); // 右跳
        jump(m - 1,n); // 左跳
        jump(m,n + 1); // 下跳
        jump(m,n - 1); // 上跳
        
        step--; // 回溯法关键步骤
        map[m][n] = 0;
    }

    // 列出最短步数的路径
    static void find(int m,int n) {
        // 该点在地图边界之外或者走过
        if (m < 0 || m >= 5 || n < 0 || n >= 5 || map[m][n] != 0) { 
            return;
        }
        
        // 记录坐标
        sx[step] = m;
        sy[step] = n; 
        
        // 走到此节点
        map[m][n] = 1;
        step++;
        
        if (m == x && n == y && step == min) { // 到达目的且为最短路径
            int p = min - 1;
            System.out.print("最短 path:" + p + "步");
            for (int i = 0; i < min; i++)
                System.out.print("(" + sx[i] + "," + sy[i] + ")");
            System.out.println();
        }
        
        find(m + 1,n);
        find(m - 1,n);
        find(m,n + 1);
        find(m,n - 1);
        
        step--;
        map[m][n] = 0;
    }

    public static void main(String[] args) {
        jump(0,0);
        step = 0;
        System.out.println("总共" + count + "种解法");
        find(0,0);
    }
    
}

程序运行结果:

走迷宫

以一个 M×N 的长方阵表示迷宫,01 分别表示迷宫中的通路障碍
设计一个程序,对任意输入的迷宫,输出一条从入口到出口的通路,或得出没有通路的结论。
例:
输入:
请输入迷宫的行数 9
请输入迷宫的列数 8
请输入 9 行 8 列的迷宫
0 0 1 0 0 0 1 0
0 0 1 0 0 0 1 0
0 0 1 0 1 1 0 1
0 1 1 1 0 0 1 0
0 0 0 1 0 0 0 0
0 1 0 0 0 1 0 1
0 1 1 1 1 0 0 1
1 1 0 0 0 1 0 1
1 1 0 0 0 0 0 0

为了方便大家观看,我换成了矩阵:
\[ \begin{matrix} 0 & 0 & 1 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 & 1 & 1 & 0 & 1 \\ 0 & 1 & 1 & 1 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 1 & 0 & 1 \\ 0 & 1 & 1 & 1 & 1 & 0 & 0 & 1 \\ 1 & 1 & 0 & 0 & 0 & 1 & 0 & 1 \\ 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ \end{matrix} \]

输出:
有路径
路径如下:
# # 1 0 0 0 1 0
0 # 1 0 0 0 1 0
# # 1 0 1 1 0 1
# 1 1 1 0 0 1 0
# # # 1 # # # 0
0 1 # # # 1 # 1
0 1 1 1 1 0 # 1
1 1 0 0 0 1 # 1
1 1 0 0 0 0 # #

为了方便大家观看,我换成了矩阵:
\[ \begin{matrix} \# & \# & 1 & 0 & 0 & 0 & 1 & 0 \\ 0 & \# & 1 & 0 & 0 & 0 & 1 & 0 \\ \# & \# & 1 & 0 & 1 & 1 & 0 & 1 \\ \# & 1 & 1 & 1 & 0 & 0 & 1 & 0 \\ \# & \# & \# & 1 & \# & \# & \# & 0 \\ 0 & 1 & \# & \# & \# & 1 & \# & 1 \\ 0 & 1 & 1 & 1 & 1 & 0 & \# & 1 \\ 1 & 1 & 0 & 0 & 0 & 1 & \# & 1 \\ 1 & 1 & 0 & 0 & 0 & 0 & \# & \# \\ \end{matrix} \]

答案:这里用栈来实现的递归,算是一个新思路。

//迷宫
/*位置类*/
class Position {
    int row;
    int col;
    
    public Position() {
    }
    public Position(int row,int col) {
        this.col = col;
        this.row = row;
    }
    public String toString() {
        return "(" + row + "," + col + ")";
    }
}

/*地图类*/
class Maze {
    int maze[][];
    private int row = 9;
    private int col = 8;
    Stack<Position> stack;
    boolean p[][] = null;
    
    public Maze() {
        maze = new int[15][15];
        stack = new Stack<Position>();
        p = new boolean[15][15];
    }

    /*
     * 构造迷宫
     */
    public void init() {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入迷宫的行数");
        row = scanner.nextInt();
        System.out.println("请输入迷宫的列数");
        col = scanner.nextInt();
        System.out.println("请输入" + row + "行" + col + "列的迷宫");
        int temp = 0;
        for(int i = 0; i < row; ++i) {
            for(int j = 0; j < col; ++j) {
                temp = scanner.nextInt();
                maze[i][j] = temp;
                p[i][j] = false;
            }
        }
    }
    /*
     * 回溯迷宫,查看是否有出路
     */
    public void findPath() {
        // 给原始迷宫的周围加一圈围墙
        int temp[][] = new int[row + 2][col + 2];
        for(int i = 0; i < row + 2; ++i) {
            for(int j = 0; j < col + 2; ++j) {
                temp[0][j] = 1;
                temp[row + 1][j] = 1;
                temp[i][0] = temp[i][col + 1] = 1;
            }
        }
        // 将原始迷宫复制到新的迷宫中
        for(int i = 0; i < row; ++i) {
            for(int j = 0; j < col; ++j) {
                temp[i + 1][j + 1] = maze[i][j];
            }
        }
        // 从左上角开始按照顺时针开始查询
        int i = 1;
        int j = 1;
        p[i][j] = true;
        stack.push(new Position(i,j));


        while (!stack.empty() && (!(i == (row) && (j == col)))) {
            if ((temp[i][j + 1] == 0) && (p[i][j + 1] == false)) {
                p[i][j + 1] = true;
                stack.push(new Position(i,j + 1));
                j++;
            } else if ((temp[i + 1][j] == 0) && (p[i + 1][j] == false)) {
                p[i + 1][j] = true;
                stack.push(new Position(i + 1,j));
                i++;
            } else if ((temp[i][j - 1] == 0) && (p[i][j - 1] == false)) {
                p[i][j - 1] = true;
                stack.push(new Position(i,j - 1));
                j--;
            } else if ((temp[i - 1][j] == 0) && (p[i - 1][j] == false)) {
                p[i - 1][j] = true;
                stack.push(new Position(i - 1,j));
                i--;
            } else {
                stack.pop();
                if(stack.empty()) {
                    break;
                }
                i = stack.peek().row;
                j = stack.peek().col;
            }
        }
        Stack<Position> newPos = new Stack<Position>();
        if (stack.empty()) {
            System.out.println("没有路径");
        } else {
            System.out.println("有路径");
            System.out.println("路径如下:");
            while (!stack.empty()) {
                Position pos = new Position();
                pos = stack.pop();
                newPos.push(pos);
            }
        }
        /*
        * 图形化输出路径
        * */
        String resault[][]=new String[row+1][col+1];
        for(int k=0; k<row; ++k) {
            for(int t=0; t<col; ++t) {
                resault[k][t]=(maze[k][t])+"";
            }
        }

        while (!newPos.empty()) {
            Position p1=newPos.pop();
            resault[p1.row-1][p1.col-1]="#";
        }
        for(int k=0; k<row; ++k) {
            for(int t=0; t<col; ++t) {
                System.out.print(resault[k][t]+"\t");
            }
            System.out.println();
        }
    }
}


/*主类*/
class Sy4 {
    public static void main(String[] args) {
        Maze demo = new Maze();
        demo.init();
        demo.findPath();
    }
}

程序运行结果:

嘿嘿,上面的那种用栈来实现递归的方法是不是看完了呢!把它放在第一个就是为了让大家以为没有递归回溯的答案,好认认真真的看完。。。(别打我)
贴心的我当然准备了用递归回溯方法的代码:

// 迷宫
class Sy4 {
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.init();
        demo.find(0,0);
    }
}

class Demo {
    int m,n;
    // 类在实例化时分配空间,但是只是逻辑上连续的空间,而不一定是物理上,毕竟有静态变量,不可能完全连续。
    String[][] maze; //不能用char,扫描器Scanner不能扫描。
                    //这里只是声明,后面输入m、n时才能确定分配空间的大小
    
    //初始化迷宫
    public void init() {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入迷宫的行数");
        m = scanner.nextInt();
        System.out.println("请输入迷宫的列数");
        n = scanner.nextInt();
        maze = new String[m][n];

        System.out.println("请输入" + m + "行" + n + "列的迷宫");
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                maze[i][j] = scanner.next();
            }
        }
        
        System.out.println("--------------------------------------------------------");
        System.out.println("迷宫如下:");
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                System.out.print(maze[i][j] + " ");
            }
            System.out.println();
        }

        System.out.println("--------------------------------------------------------");
    }

    //走到(x,y)点,找找路径
    public void find(int x,int y) {
        if (x < 0 || y < 0 || x >= m || y >= n || !maze[x][y].equals("0")) { // 注意字符串要用equals
            return;
        }

        maze[x][y] = "#";   // 走到此节点

        if (x == m - 1 && y == n - 1) {
            for (int i = 0; i < m; ++i) {
                for (int j = 0; j < n; ++j) {
                    System.out.print(maze[i][j] + " ");
                }
                System.out.println();
            }
            System.out.println("--------------------------------------------------------");
        }

        find(x + 1,y); //下移
        find(x - 1,y); //上移
        find(x,y + 1); //右移
        find(x,y - 1); //左移

        maze[x][y] = "0";
    }

}

程序运行结果:

--------------------------------------------------------
迷宫如下:
0 0 1 0 0 0 1 0 
0 0 1 0 0 0 1 0 
0 0 1 0 1 1 0 1 
0 1 1 1 0 0 1 0 
0 0 0 1 0 0 0 0 
0 1 0 0 0 1 0 1 
0 1 1 1 1 0 0 1 
1 1 0 0 0 1 0 1 
1 1 0 0 0 0 0 0 
--------------------------------------------------------
# 0 1 0 0 0 1 0 
# 0 1 0 0 0 1 0 
# 0 1 0 1 1 0 1 
# 1 1 1 # # 1 0 
# # # 1 # # # 0 
0 1 # # # 1 # 1 
0 1 1 1 1 0 # 1 
1 1 0 0 0 1 # 1 
1 1 0 0 0 0 # # 
--------------------------------------------------------
# 0 1 0 0 0 1 0 
# 0 1 0 0 0 1 0 
# 0 1 0 1 1 0 1 
# 1 1 1 0 0 1 0 
# # # 1 # # # 0 
0 1 # # # 1 # 1 
0 1 1 1 1 0 # 1 
1 1 0 0 0 1 # 1 
1 1 0 0 0 0 # # 
--------------------------------------------------------
# 0 1 0 0 0 1 0 
# # 1 0 0 0 1 0 
# # 1 0 1 1 0 1 
# 1 1 1 # # 1 0 
# # # 1 # # # 0 
0 1 # # # 1 # 1 
0 1 1 1 1 0 # 1 
1 1 0 0 0 1 # 1 
1 1 0 0 0 0 # # 
--------------------------------------------------------
# 0 1 0 0 0 1 0 
# # 1 0 0 0 1 0 
# # 1 0 1 1 0 1 
# 1 1 1 0 0 1 0 
# # # 1 # # # 0 
0 1 # # # 1 # 1 
0 1 1 1 1 0 # 1 
1 1 0 0 0 1 # 1 
1 1 0 0 0 0 # # 
--------------------------------------------------------
# # 1 0 0 0 1 0 
0 # 1 0 0 0 1 0 
# # 1 0 1 1 0 1 
# 1 1 1 # # 1 0 
# # # 1 # # # 0 
0 1 # # # 1 # 1 
0 1 1 1 1 0 # 1 
1 1 0 0 0 1 # 1 
1 1 0 0 0 0 # # 
--------------------------------------------------------
# # 1 0 0 0 1 0 
0 # 1 0 0 0 1 0 
# # 1 0 1 1 0 1 
# 1 1 1 0 0 1 0 
# # # 1 # # # 0 
0 1 # # # 1 # 1 
0 1 1 1 1 0 # 1 
1 1 0 0 0 1 # 1 
1 1 0 0 0 0 # # 
--------------------------------------------------------
# # 1 0 0 0 1 0 
# # 1 0 0 0 1 0 
# 0 1 0 1 1 0 1 
# 1 1 1 # # 1 0 
# # # 1 # # # 0 
0 1 # # # 1 # 1 
0 1 1 1 1 0 # 1 
1 1 0 0 0 1 # 1 
1 1 0 0 0 0 # # 
--------------------------------------------------------
# # 1 0 0 0 1 0 
# # 1 0 0 0 1 0 
# 0 1 0 1 1 0 1 
# 1 1 1 0 0 1 0 
# # # 1 # # # 0 
0 1 # # # 1 # 1 
0 1 1 1 1 0 # 1 
1 1 0 0 0 1 # 1 
1 1 0 0 0 0 # # 
--------------------------------------------------------

马走日

假设国际象棋棋盘有 5*5 共 25 个格子。
设计一个程序,使棋子从初始位置(棋盘编号为 1 的位)开始跳马,能够把棋盘的格子全部都走一遍,每个格子只允许走一次。

  1. 输出一个如图 2 的解,左上角为第一步起点。
  2. 总共有多少解。

PS:国际象棋的棋子是在格子中间的。国际象棋中的“马走日”,如下图所示,第一步为[1,1],
第二步为[2,8]或[2,12],第三步可以是[3,5]或[3,21]等,以此类推。

答案:

  • 明确函数功能:jump(m,n)位置。
  • 寻找递归出口:不在边界之内 或 已走过。
  • 明确所有路径:8个方位,

    技巧:这里可以用一个数组存入八个方位的变化,再用循环依次取出,比写八个方位要聪明许多。

  • 回溯还原现场:
    path--; // 回溯法关键步骤
    a[m][n] = 0;

//马走日
class Sy2 {
    private static int[][] next = { { 1,2 },-2 },{ -1,{ 2,-1 },{ -2,-1 } }; // 马的跳跃路径(技巧)
    private static int[][] map; // 地图
    private static int m,n;
    private static int count = 0;// 统计有多少种走法
    private static int step = 0;

    public static void main(String[] args) {
        m = 5;
        n = 5;
        int x = 0;
        int y = 0;
        map = new int[m][n];
        jump(x,y);
        System.out.println("---------");
        System.out.println(count);
    }

    
    private static void jump(int x,int y) {
        // 如果超出界限,那就继续下一轮
        if (x < 0 || x >= m || y < 0 || y >= n || map[x][y] != 0) {
            return;
        }
        
        // 立足此节点
        step++;
        map[x][y] = step;   
        
        if (step == m * n) {
            if (count == 0) // 如果是第一次,那就输出一个
                show(map);
            count++;
        }
        
        // 写出所有路径(技巧)
        int tx = 0,ty = 0;
        for (int i = 0; i < 8; i++) {
            tx = x + next[i][0];    // 技巧
            ty = y + next[i][1];

            jump(tx,ty);
        }
        
        // 还原
        step--; 
        map[x][y] = 0;
    }

    // 显示数组
    private static void show(int[][] arr) {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                System.out.print(arr[i][j] + " \t");
            }
            System.out.println();
        }
    }
    
}

程序运行结果:

八皇后

编程解决“八皇后问题”:即 在一个 8*8 的矩形格子中排放 8 个皇后。
要满足的条件包括:任意两个皇后不能在同一行,同一列,也不能在同一条对角线上。
要求编程给出解的个数。

答案:
算法原理:回溯法
首先,可归纳问题的条件为,8 皇后之间需满足:

  1. 不在同一行上
  2. 不在同一列上
  3. 不在同一斜线上
  4. 不在同一反斜线上

这为我们提供一种遍历的思路,我们可以逐行或者逐列来进行可行摆放方案的遍历,每一行(列)遍历出一个符合条件的位置,接着就到下一行(列)遍历下一个棋子的合适位置,这种遍历思路可以保证我们遍历过程中有一个条件是绝对符合的——就是下一个棋子的摆放位置与前面的棋子不在同一行(列)。

这里我们逐列摆放数组下标代表列号,用数组元素存放行号。

把当前列 N 的前面的某一列设为 m,则 m 的所有取值为{m>=0,m<N}的集合,故又可在上面式子的基础,归纳为如下:


从这个图可以看出,m和N若在同一斜线上,那么行差Am列差AN应该相等


所以,在点m存在的情况下,与点m列差为d的点,若行差也为±d,那么就在一条斜线上,不合法。

  • cols[N] != cols[m](与第 m 列的棋子不在同一行)
  • cols[N] != cols[m] - (N-m) (>=0,与第 m 列的棋子不在同一斜线上)
  • cols[N] != cols[m] + (N-m) (<=8-1,与第 m 列的棋子不在同一反斜线上)

我们规定当 row[i]=true 时,表示该列第 i 行不能放棋子。

总结:

  • 编写检测函数:正如上面的分析,每摆一个,将不合法的位置用数组标识,就不涉足了。当然,也可以写成函数,不过没有数组快。
  • 明确函数功能:put(n)为摆第n个皇后。
  • 寻找递归出口:当摆完第八个皇后;不同行、不同斜线、不同反斜线。
  • 明确所有路径:八行。
  • 回溯还原现场:不需要还原,没有破坏现场,因为检测的时候提前用数组标识了,所以不合法的现场都没涉足。

这样我们就能写成下列程序段了:

// 八皇后
class Sy6 {
    public static int num = 0; // 累计方案总数
    public static final int MAXQUEEN = 8;// 皇后个数,同时也是棋盘行列总数
    public static int[] cols = new int[MAXQUEEN]; // 定义cols数组,表示8列棋子摆放情况,数组元素存放行号

    public Sy6() {
        // 核心函数
        put(0);
        System.out.println(MAXQUEEN + "皇后问题有" + num + "种摆放方法。");
    }

    public void put(int n) {
        // 当摆完第八个皇后,摆第九个时
        if (n > MAXQUEEN - 1) {
            // 累计方案个数
            num++;
            return;
        }

        // 遍历该列所有不合法的行,并用 rows 数组记录,不合法即 rows[i]=true
        boolean[] rows = new boolean[MAXQUEEN];
        for (int i = 0; i < n; i++) {

            rows[cols[i]] = true; // 同行不合法

            int d = n - i; // 列差
            if (cols[i] - d >= 0) // 判断是否超界
                // 行差为-d的斜线点,不合法
                rows[cols[i] - d] = true;

            if (cols[i] + d <= MAXQUEEN - 1)// 判断是否超界
                // 行差为d的斜线点,不合法
                rows[cols[i] + d] = true;
        }

        // 所有路径:八行都能摆
        for (int i = 0; i < MAXQUEEN; i++) {
            // 判断该行是否合法,如果不合法,那就继续下一轮
            if (rows[i])
                continue;

            // 设置当前列合法棋子所在行数
            cols[n] = i;

            // 摆放下一个
            put(n + 1);
        }
    }

    public static void main(String args[]) {
        Sy6 queen = new Sy6();
    }
}

程序运行结果:

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


背景:计算机内部用补码表示二进制数。符号位1表示负数,0表示正数。正数:无区别,正数 的原码= 反码 = 补码重点讨论负数若已知 负数 -8,则其原码为:1000 1000,(1为符号位,为1代表负数,为0代表正数)反码为:1111 0111,(符号位保持不变,其他位置按位取反)补码为:1111 1000,(反码 + 1)即在计算机中 用 1111 1000表示 -8若已知补码为 1111 1000,如何求其原码呢?(1)方法1:求负数 原码---&gt;补...
大家好,我们现在来讲解关于加密方面的知识,说到加密我认为不得不提MD5,因为这是一种特殊的加密方式,它到底特殊在哪,现在我们就开始学习它全称:message-digest algorithm 5翻译过来就是:信息 摘要 算法 5加密和摘要,是不一样的加密后的消息是完整的;具有解密算法,得到原始数据;摘要得到的消息是不完整的;通过摘要的数据,不能得到原始数据;所以,当看到很多人说,md5,加密,解密的时候,呵呵一笑就好了。MD5长度有人说md5,128位,32位,16位,到
相信大家在大学的《算法与数据结构》里面都学过快速排序(QuickSort), 知道这种排序的性能很好,JDK里面直到JDK6用的都是这种经典快排的算法。但是到了JDK7的时候JDK内置的排序算法已经由经典快排变成了Dual-Pivot排序算法。那么Dual-Pivot到底是何方圣神,能比我们学过的经典快排还要快呢?我们一起来看看。经典快排在学习新的快排之前,我们首先来复习一下经典快排,它的核心思想是:接受一个数组,挑一个数(pivot),然后把比它小的那一摊数放在它的左边,把比它大的那一摊数放
加密在编程中的应用的是非常广泛的,尤其是在各种网络协议之中,对称/非对称加密则是经常被提及的两种加密方式。对称加密我们平时碰到的绝大多数加密就是对称加密,比如:指纹解锁,PIN 码锁,保险箱密码锁,账号密码等都是使用了对称加密。对称加密:加密和解密用的是同一个密码或者同一套逻辑的加密方式。这个密码也叫对称秘钥,其实这个对称和不对称指的就是加密和解密用的秘钥是不是同一个。我在上大学的时候做过一个命令行版的图书馆管理系统作为 C 语言课设。登入系统时需要输入账号密码,当然,校验用户输入的密码
前言我的目标是写一个非常详细的关于diff的干货,所以本文有点长。也会用到大量的图片以及代码举例,目的让看这篇文章的朋友一定弄明白diff的边边角角。先来了解几个点...1. 当数据发生变化时,vue是怎么更新节点的?要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们。我们先根据真实DOM生成一颗virtual DOM,当v
对称加密算法 所有的对称加密都有一个共同的特点:加密和解密所用的密钥是相同的。现代对称密码可以分为序列密码和分组密码两类:序列密码将明文中的每个字符单独加密后再组合成密文;而分组密码将原文分为若干个组,每个组进行整体加密,其最终加密结果依赖于同组的各位字符的具体内容。也就是说,分组加密的结果不仅受密钥影响,也会受到同组其他字符的影响。序列密码分组密码序列密码的安全性看上去要更弱一些,但是由于序列密码只需要对单个位进行操作,因此运行速度比分组加密要快...
本文介绍RSA加解密中必须考虑到的密钥长度、明文长度和密文长度问题,对第一次接触RSA的开发人员来讲,RSA算是比较复杂的算法,RSA算法自己其实也很简单,RSA的复杂度是由于数学家把效率和安全也考虑进去的缘故。html本文先只谈密钥长度、明文长度和密文长度的概念知识,RSA的理论及示例等之后再谈。提到密钥,咱们不得不提到RSA的三个重要大数:公钥指数e、私钥指数d和模值n。这三个大数是咱们使用RSA时须要直接接触的,理解了本文的基础概念,即便未接触过RSA的开发人员也能应对自如的使用RSA相关函数库,
直观的说,bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中。和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。算法:1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为03. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为14. 判断某个key是否在集合时
你会用什么样的算法来为你的用户保存密码?如果你还在用明码的话,那么一旦你的网站被hack了,那么你所有的用户口令都会被泄露了,这意味着,你的系统或是网站就此完蛋了。所以,我们需要通过一些不可逆的算法来保存用户的密码。比如:MD5, SHA1, SHA256, SHA512, SHA-3,等Hash算法。这些算法都是不可逆的。系统在验证用户的口令时,需要把Hash加密过后的口令与后面存放口令的数据库中的口令做比较,如果一致才算验证通过。但你觉得这些算法好吗?我说的是:MD5, SHA1, SHA256,
在日常工作中经常会使用excel,有时在表格中需要筛选出重复的数据,该怎么操作呢?1、以下图中的表格数据为例,筛选出列中重复的内容;2、打开文件,选中需要筛选的数据列,依次点击菜单项【开始】-【条件格式】-【突出显示单元格规则】-【重复值】;3、将重复的值突出颜色显示;4、选中数据列,点击【数据】-【筛选】;5、点击列标题的的下拉小三角,点击【按颜色筛选】,即可看到重复的数据;...
工作中经常有和第三方机构联调接口的事情,顾将用到过的做以记录。 在和第三方联调时,主要步骤为:网络、加解密/签名验签、接口数据等,其中接口数据没啥好说的。 在联调前就需要先将两边的网络连通,一般公司的生产环境都加了防火墙,测试环境有的是有防火墙,有的则没有防火墙,这个需要和第三方人员沟通,如果有防火墙的就需要将我们的出口ip或域名发送给第三方做配置,配置了之后网络一般都是通的。加解密与签名验签: 一般第三方公司都会有加解密或签名验签的,毕竟为了数据安全。一般就是三...
此文章不包含认证机制。任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过某种方式进行加密。如今已有很多标准的算法比如SHA或者MD5再结合salt(盐)使用是一个不错的选择。废话不多说!直接开始SpringBoot 中提供了Spring Security:BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。第一步:pom导入依赖:&lt;dependency&gt; &lt;groupId...
前言在所有的加密算法中使用最多的就是哈希加密了,很多人第一次接触的加密算法如MD5、SHA1都是典型的哈希加密算法,而哈希加密除了用在密码加密上,它还有很多的用途,如提取内容摘要、生成签名、文件对比、区块链等等。这篇文章就是想详细的讲解一下哈希加密,并分享一个哈希加密的工具类。概述哈希函数(Hash Function),也称为散列函数或杂凑函数。哈希函数是一个公开函数,可以将任意长度的消息M映射成为一个长度较短且长度固定的值H(M),称H(M)为哈希值、散列值(Hash Value)、杂凑值或者消息
#快速排序解释 快速排序 Quick Sort 与归并排序一样,也是典型的分治法的应用。 (如果有对 归并排序还不了解的童鞋,可以看看这里哟~ 归并排序)❤❤❤ ###快速排序的分治模式 1、选取基准
#堆排序解释 ##什么是堆 堆 heap 是一种近似完全二叉树的数据结构,其满足一下两个性质 1. 堆中某个结点的值总是不大于(或不小于)其父结点的值; 2. 堆总是一棵完全二叉树 将根结点最大的堆叫
#前言 本文章是建立在插入排序的基础上写的喔,如果有对插入排序还有不懂的童鞋,可以看看这里。 ❤❤❤ 直接/折半插入排序 2路插入排序 ❤❤❤ #希尔排序解释 希尔排序 Shell Sort 又名&q
#归并排序解释 归并排序 Merge Sort 是典型的分治法的应用,其算法步骤完全遵循分治模式。 ##分治法思想 分治法 思想: 将原问题分解为几个规模较小但又保持原问题性质的子问题,递归求解这些子
#前言 本文章是建立在冒泡排序的基础上写的,如还有对 冒泡排序 不了解的童鞋,可以看看这里哦~ 冒泡排序 C++ #双向冒泡排序原理 双向冒泡排序 的基本思想与 冒泡排序还是一样的。冒泡排序 每次将相
#插入排序解释 插入排序很好理解,其步骤是 :先将第一个数据元素看作是一个有序序列,后面的 n-1 个数据元素看作是未排序序列。对后面未排序序列中的第一个数据元素在这个有序序列中进行从后往前扫描,找到
#桶排序解释 ##桶排序思想 桶排序 是一种空间换取时间的排序方式,是非基于比较的。 桶排序 顾名思义,就是构建多个映射数据的桶,将数据放入桶内,对每个桶内元素进行单独排序。假设我们有 n 个待排序的