回溯法基本思想-01背包、N皇后回溯法图解

基本思想:

​ 回溯法是一种系统地搜索问题解空间的算法,常用于解决组合优化和约束满足问题。其核心思想是利用深度优先搜索逐步构建可能的解,同时在搜索过程中进行剪枝操作,以排除那些无法满足问题约束或不能产生最优解的分支,从而减少不必要的计算,提高搜索效率。

深度优先搜索(DFS)简介:

​ 对于二叉树来说,先序、中序、后序遍历都是深度优先遍历。

​ 深度优先就是一条路径走到底后,再返回上一步,搜索第二条路径。

在这里插入图片描述

回溯法的一般步骤

1.定义问题并构造状态空间树

​ 明确要解决的问题,确定解空间的结构(通常是一个树或图),确定每一步决策的选择范围。

​ 将问题的解空间表示为一棵树,树的每个节点表示一个状态,根节点表示初始状态,叶节点表示最终状态或解。

2.编写递归函数

​ 编写一个递归函数来遍历状态空间树,函数通常包括以下部分:

  • 递归边界:定义何时到达叶节点,即找到一个解或无法继续深入。
  • 选择和判断(剪枝):在当前状态下,尝试每一种可能的选择,并判断是否满足约束条件。
  • 递归调用:如果满足条件,则进行递归调用,进入下一个状态。
  • 回溯:如果不满足条件,或递归调用返回后,需要撤销当前选择,回溯到上一步继续尝试其他选择。

3.输出结果

举例:01背包

问题描述:

​ 有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?

背包体积:10

物品编号体积(vol)价值(val)
189
232
344
433

​ 前面在动态规划中,说了一下蛮力法的如何解决该问题,实际上回溯法与蛮力法差不多,只是搜索方式不同,并添加剪枝(提前结束当前遍历)和回溯(记忆之前的状态)。

1、定义问题并构造状态空间树

​ 前面使用蛮力法解决时,对于问题的每一种状态通过数值的二进制表示,用数字二进制中为1的位置表示该位置的物品是否放入,比如:

​ 1(0000 0001)表示第1个物品放入,其他都不放入;

​ 2(0000 0010)表示第2个物品放入,其他都不放入;

​ 3(0000 0011)表示第1和第2哥物品放入,其他都不放入。

​ 使用回溯法解决问题,需要将问题所有的情况构造成一颗树或图,通过深度优先搜索遍历获取最优解。

​ 每个物品只有两种状态,放入背包或不放入背包,可以将各种物品是否放入构造成一颗二叉树,如下图:

在这里插入图片描述

2、选择和判断(剪枝)

​ 边界:当当前物品为最后一个物品时,则到达叶子节点,递归结束。

​ 选择:每一步递归,针对当前物品,都存在放与不放两个选择。

​ 剪枝:如果放入当前物品,通过判断放入后当前物品的总体积是否超过总体积,如果超过就进行剪枝。

​ 回溯:当当前状态的所有情况考虑完之后,进行回溯。

剪枝:

​ 在第一个物品放入,第二个物品尝试放入时,发现放入第二个物品后,体积变为:8+3=11> 背包体积=10,所以,在第一、第二个物品放入,无论剩下的物品怎么放,都会超出背包体积,此时进行剪枝,减少了大量的计算。

​ 对比蛮力法:对于1100、1101、1110、1111这四种情况,其都会去计算一次,发现超出背包容量再结束,而回溯则在计算1100时,发现超出背包容量就直接剪枝,后面的其他几种情况都不再计算。

回溯:

​ 在计算了0111这种情况后,进行回溯,到达011这一步,然后计算0110,直接使用了011这一步的状态(当前体积7,当前价值6),在这个基础上计算0110。

​ 对比蛮力法:在计算了0111后,再次计算0110这种情况时,还需要计算011的体积和价值,然而这一步在计算0111时,011状态的体积和价值就已经计算过了,蛮力法进行了重复计算,而回溯法则保存了之前的状态,减少了重复计算。

3、输出结果

​ 在递归过程中,更新最大价值,最终输出,

4、代码实现

​ 不需要真的去构建这颗二叉树,通过递归模拟二叉树即可。

public class ZeroOneBackpackBacktrack {
    private static int maxValue = 0;
    public static void main(String[] args) {
        int maxVolume = 10;
        Item[] items = new Item[]{
                new Item(8, 9),
                new Item(3, 2),
                new Item(4, 4),
                new Item(3, 3)};
        execute(items, maxVolume);
        System.out.println(maxValue);
    }

    public static void execute(Item[] items, int maxVolume) {
        zeroOneBackpackBacktrack(items, maxVolume, 0, 0, 0);
    }

    public static void zeroOneBackpackBacktrack(Item[] items, int maxVolume, int index, int currentVolume, int currentValue) {
        // 当前体积已经超出背包最大体积
        if (currentVolume > maxVolume) return;
        // 更新最大价值
        maxValue = Math.max(currentValue, maxValue);
        // 未到达最后一个物品
        if (index < items.length) {
            // 放入当前物品
            zeroOneBackpackBacktrack(items, maxVolume, index + 1, currentVolume + items[index].getVolume(), currentValue + items[index].getValue());
            // 不放入当前物品
            zeroOneBackpackBacktrack(items, maxVolume, index + 1, currentVolume, currentValue);
        }
    }
}

class Item {
    int volume;
    int value;
    public Item(int volume, int value) {
        this.volume = volume;
        this.value = value;
    }
    public int getValue() {
        return value;
    }
    public int getVolume() {
        return volume;
    }
}

回溯法经典案例-N皇后图解

问题描述:

​ 根据国际象棋的规则,皇后可以攻击与同处一行、一列或一条斜线上的棋子。给定 𝑛 个皇后和一个 𝑛×𝑛 大小的棋盘,寻找使得所有皇后之间无法相互攻击的摆放方案。

例如:

​ 对于n = 4 的情况,有下面两种可能的摆放方法。

在这里插入图片描述

在这里插入图片描述

蛮力法:

​ 蛮力法只需要将所有的情况都遍历到即可,对于每一行,尝试在每一列放置一个皇后,生成所有可能的放置方案。

​ 每一行有n列,一共n行,遍历每种情况就需要 n ∗ n ∗ n . . . ∗ n = n n n*n*n...*n = n^n nnn...n=nn次,再加上每一次都要判断所有的皇后位置是否正确,又需要遍历。所以复杂度特别高。

    public static void nQueens(int[][] data, int row) {
        int n = data.length;
        // 已经到达最后一行,n个皇后已经放置完毕
        if (row == n) {
            // 校验皇后拜访位置是否合理
            for (int row1 = 0; row1 < n; row1++) {
                for (int col = 0; col < n; col++) {
                    if (data[row1][col] == 1) {
                        // 判断每一行中皇后位置是否合理
                        if (!canPlace(data, row1, col)) {
                            // 不合理直接返回
                            return;
                        } else {
                            // 到达最后一行输出结果
                            if (row1 == n - 1) {
                                System.out.println("第" + ++num + "组解:");
                                for (int[] d : data) {
                                    System.out.println(Arrays.toString(d));
                                }
                            }
                        }
                    }
                }
            }
        }else{
            // 未到达最后一行,遍历
            for (int col = 0; col < n; col++) {
                data[row][col] = 1;
                nQueens(data, row + 1);
                data[row][col] = 0;
            }
        }
    }

    public static boolean canPlace(int[][] data, int row, int col) {
        // 检查同一列是否有皇后
        for (int i = 0; i < row; i++) {
            if (data[i][col] == 1) return false;
        }
        // 检查 \ 对角线是否存在皇后
        for (int i = 1; row - i >= 0 && col - i >= 0; i++) {
            if (data[row - i][col - i] == 1) return false;
        }
        // 检查 / 对角线是否存在皇后
        for (int i = 1; row - i >= 0 && col + i < data.length; i++) {
            if (data[row - i][col + i] == 1) return false;
        }
        return true;
    }
回溯法:
1、定义问题并构造状态空间树

​ 根据棋盘大小 n ∗ n n*n nn n n n个皇后,以及皇后可以攻击与其处于同一行上的其他皇后可知,每一行仅允许放置一个皇后。可以按照逐行放置的思路:从第一行开始,在每行放置一个皇后,直至最后一行结束。

​ 那么每一行实际上就有n个选择,每个位置是否放入皇后,放入皇后之后,就可以进入下一行放置下一个皇后。

​ 采用一个n叉树就可以表示,这里使用一个二维数组表示:

    public static void dfs(int[][] data, int row) {
        for (int col = 0; col < data.length; col++) {
                // 放入皇后
                data[row][col] = 1;
                // 放入下一个皇后
                dfs(data, row + 1);
                // 取出当前皇后(回溯):每一行只能放置一个,取出当前列皇后,下一列放入
                data[row][col] = 0;
            }
        }
    }
2、选择和判断(剪枝)

​ 在某一行的某一列放置皇后时,可能会出现如果该位置放置皇后,就会与前几行放置的皇后冲突,那么就可以提前剪枝,直接不用放置后续的皇后了,直接去尝试再下一列放置。

	public static void dfs(int[][] data, int row) {
        for (int col = 0; col < data.length; col++) {
            // 剪枝:判断该位置是否可以放入,不可放入则直接终止
            if (canPlace(data, row, col)) {
                // 放入皇后
                data[row][col] = 1;
                // 放入下一个皇后
                dfs(data, row + 1);
                // 取出当前皇后(回溯):每一行只能放置一个,取出当前列皇后,下一列放入
                data[row][col] = 0;
            }
        }
    }

	public static boolean canPlace(int[][] data, int row, int col) {
        // 检查同一列是否有皇后
        for (int i = 0; i < row; i++) {
            if (data[i][col] == 1) return false;
        }
        // 检查 \ 对角线是否存在皇后
        for (int i = 1; row - i >= 0 && col - i >= 0; i++) {
            if (data[row - i][col - i] == 1) return false;
        }
        // 检查 / 对角线是否存在皇后
        for (int i = 1; row - i >= 0 && col + i < data.length; i++) {
            if (data[row - i][col + i] == 1) return false;
        }
        return true;
    }
3、输出结果

​ 当放置到最后一行时,此时所有皇后均放入,可以输出结果

	public static void dfs(int[][] data, int row) {
        // 最后一个皇后已经放入
        if (row == data.length) {
            printResult(data, ++resultNum);
            return;
        }
        for (int col = 0; col < data.length; col++) {
            // 剪枝:判断该位置是否可以放入,不可放入则直接终止
            if (canPlace(data, row, col)) {
                // 放入皇后
                data[row][col] = 1;
                // 放入下一个皇后
                dfs(data, row + 1);
                // 取出当前皇后(回溯):每一行只能放置一个,取出当前列皇后,下一列放入
                data[row][col] = 0;
            }
        }
    }
	public static void printResult(int[][] data, int num) {
        System.out.println("第" + num + "组解:");
        for (int i = 0; i < data.length; i++) {
            System.out.println(Arrays.toString(data[i]));
        }
    }
图解说明:

​ 因为八皇后如果画图篇幅过大,这里用四皇后讲解:

​ 其中白色表示尚未遍历,绿色表示放入皇后,红色表示被剪枝(此路不通)

1、给第一行第一列放入皇后,进入第二行在第二行第一列放入皇后,剪枝

在这里插入图片描述

2、放入第二行第二列,剪枝

在这里插入图片描述

3、放入第二行第三列,皇后可以放置,再尝试放入第三行

在这里插入图片描述

4、第二行第三列放入皇后的所有情况均被剪枝,放入第二行第四列

​ 放入第三行第一列时剪枝,放入第三行第二列

在这里插入图片描述

5、第三行确定后,放入第四行(最后一个皇后)

​ 可见,在第四行第三列放入皇后时,符合要求,直接输出结果,其他几种情况均不符合情况

在这里插入图片描述

6、回溯,计算第三行放入第三列的情况

7、重复上述步骤

具体流程下图:

​ 可以看到,这其实就是深度优先搜索,添加了剪枝

在这里插入图片描述

优化:

​ ​ 上面在判断某个位置是否可以放置皇后时,使用的计算过方式需要遍历,会导致每次计算时复杂度较高。

public static boolean canPlace(int[][] data, int row, int col) {
        // 检查同一列是否有皇后
        for (int i = 0; i < row; i++) {
            if (data[i][col] == 1) return false;
        }
        // 检查 \ 对角线是否存在皇后
        for (int i = 1; row - i >= 0 && col - i >= 0; i++) {
            if (data[row - i][col - i] == 1) return false;
        }
        // 检查 / 对角线是否存在皇后
        for (int i = 1; row - i >= 0 && col + i < data.length; i++) {
            if (data[row - i][col + i] == 1) return false;
        }
        return true;
    }

可以改为如下方式:

​ 可以利用一个长度为 𝑛 的布尔型数组 existCol 记录每一列是否有皇后。在每次决定放置前,我们通过 cols 将已有皇后的列进行剪枝,并在回溯中动态更新 cols 的状态。

那么,如何处理对角线约束呢?

​ 设棋盘中某个格子的行列索引为 (𝑟𝑜𝑤,𝑐𝑜𝑙) ,选定矩阵中的某条主对角线,我们发现该对角线上所有格子的行索引减列索引都相等,即对角线上所有格子的 𝑟𝑜𝑤−𝑐𝑜𝑙 为恒定值

​ 也就是说,如果两个格子满足 𝑟𝑜𝑤1−𝑐𝑜𝑙1=𝑟𝑜𝑤2−𝑐𝑜𝑙2 ,则它们一定处在同一条主对角线上。利用该规律,我们可以借助如图所示的数组 existLeftDiagonal 记录每条主对角线上是否有皇后。

​ 容易看出,记录某一列是否有皇后的数组existLeftDiagonal长度为2N-1

在这里插入图片描述

​ 同理,次对角线上的所有格子的 𝑟𝑜𝑤+𝑐𝑜𝑙 是恒定值。我们同样也可以借助数组 existRightDiagonal 来处理次对角线约束。

public static boolean canPlace(int row, int col, int n, boolean[] existCol, boolean[] existLeftDiagonal, boolean[] existRightDiagonal) {
        return !(existCol[col] || 
                 existLeftDiagonal[row - col + n - 1] || 
                 existRightDiagonal[row + col]);
    }

完整代码:

package backtracking;

import java.util.Arrays;

public class EightQueens2 {
    private static int num = 0;

    public static void main(String[] args) {
        execute(9);
    }

    public static void execute(int n) {
        // 皇后存在情况表
        int[][] data = new int[n][n];
        // 皇后存在列情况
        boolean[] existCol = new boolean[n];
        // 皇后存在对角线 \ 情况 (可以发现处于同一对角线的元素,行 - 列是同一个值,所以可以使用这个性质来存储对角线信息)
        boolean[] existLeftDiagonal = new boolean[2 * n - 1];
        // 皇后存在对角线 / 情况(可以发现处于同一对角线的元素,行+ 列是同一个值,所以可以使用这个性质来存储对角线信息)
        boolean[] existRightDiagonal = new boolean[2 * n - 1];
        dfs(data, n, 0, existCol, existLeftDiagonal, existRightDiagonal);
    }

    private static void dfs(int[][] data, int n, int row, boolean[] existCol, boolean[] existLeftDiagonal, boolean[] existRightDiagonal) {
        if (row == n) {
            // 所有皇后已放置完毕,输出
            printResult(data, ++num);
        }
        for (int col = 0; col < n; col++) {
            // 剪枝:判断该位置是否可以放入,不可放入则直接终止
            if (canPlace(row, col, n, existCol, existLeftDiagonal, existRightDiagonal)) {
                // 放入皇后
                placeQueen(row, col, n, data, existCol, existLeftDiagonal, existRightDiagonal);
                // 放入下一个皇后
                dfs(data, n, row + 1, existCol, existLeftDiagonal, existRightDiagonal);
                // 取出皇后
                cancelPlaceQueen(row, col, n, data, existCol, existLeftDiagonal, existRightDiagonal);
            }
        }
    }

    public static boolean canPlace(int row, int col, int n, boolean[] existCol, boolean[] existLeftDiagonal, boolean[] existRightDiagonal) {
        return !(existCol[col] || existLeftDiagonal[row - col + n - 1] || existRightDiagonal[row + col]);
    }

    public static void placeQueen(int row, int col, int n, int[][] data, boolean[] existCol, boolean[] existLeftDiagonal, boolean[] existRightDiagonal) {
        data[row][col] = 1;
        existCol[col] = existLeftDiagonal[row - col + n - 1] = existRightDiagonal[row + col] = true;
    }

    public static void cancelPlaceQueen(int row, int col, int n, int[][] data, boolean[] existCol, boolean[] existLeftDiagonal, boolean[] existRightDiagonal) {
        data[row][col] = 0;
        existCol[col] = existLeftDiagonal[row - col + n - 1] = existRightDiagonal[row + col] = false;
    }

    public static void printResult(int[][] data, int num) {
        System.out.println("第" + num + "组解:");
        for (int[] d : data) {
            System.out.println(Arrays.toString(d));
        }
    }
}
  • 25
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值