回溯法
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
一、全排列问题
我们在高中的时候就做过排列组合的数学题,我们也知道 n 个不重复的数,全排列共有 n! 个。
PS:为了简单清晰起见,我们这次讨论的全排列问题不包含重复的数字。
那么我们当时是怎么穷举全排列的呢?比方说给三个数 [1,2,3],你肯定不会无规律地乱穷举,一般是这样:
先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位……
其实这就是回溯算法,我们高中无师自通就会用,或者有的同学直接画出如下这棵回溯树:
只要从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。我们不妨把这棵树称为回溯算法的「决策树」。
为啥说这是决策树呢,因为你在每个节点上其实都在做决策。比如说你站在下图的红色节点上:
你现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。
现在可以解答开头的几个名词:****[2] 就是「路径」,记录你已经做过的选择;****[1,3] 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层,在这里就是选择列表为空的时候。
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* @author SJ
* @date 2020/11/20
*/
public class Permute {
public static void main(String[] args) {
int[] nums={1,2,3};
Stack<Integer> stack=new Stack<>();
permute(nums,stack);
for (String re : res) {
System.out.println(re);
}
}
//存放排列结果
public static List<String> res=new ArrayList<>();
public static void permute(int[] nums, Stack<Integer> stack){
if (stack.size()==nums.length){
res.add(stack.toString());
return;
}
for (int i = 0; i < nums.length; i++) {
//选过的元素不能再选
if (stack.contains(nums[i]))
continue;
stack.push(nums[i]);
permute(nums,stack);
stack.pop();
}
}
}
结果:
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]
Process finished with exit code 0
至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,应为对链表使用 contains 方法需要 O(N) 的时间复杂度。有更好的方法通过交换元素达到目的,但是难理解一些,这里就不写了,有兴趣可以自行搜索一下。
但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
二、N 皇后问题
这个问题很经典了,简单解释一下:给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后
import java.util.Arrays;
/**
* @author SJ
* @date 2020/11/20
*/
public class NQueen {
//NxN的棋盘
public static int N;
public static int sum = 0;
//判断坐标为(row,col)的位置能否放置皇后
public static Boolean isValid(int[][] board, int row, int col) {
//放置是前一行放过会直接跳到下一行,所以不用判断同一行。
//判断同一行有无皇后
for (int i = 0; i < N; i++) {
if (board[row][i] == 1)
return false;
}
//判断同一列有无皇后
for (int i = 0; i < N; i++) {
if (board[i][col] == 1)
return false;
}
//是从左得到右,从上往下放置的,所以只要判断前面的就行了
// 检查右上方是否有皇后互相冲突
for (int i = row - 1, j = col + 1;
i >= 0 && j < N; i--, j++) {
if (board[i][j] == 1)
return false;
}
// 检查左上方是否有皇后互相冲突
for (int i = row - 1, j = col - 1;
i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 1)
return false;
}
return true;
}
public static void placeQueen(int[][] board, int row) {
//结束条件
if (row == N) {
sum++;
for (int[] ints : board) {
System.out.println(Arrays.toString(ints));
}
System.out.println("------------");
return;
}
for (int col = 0; col < N; col++) {
//如果不能放
if (!isValid(board, row, col))
continue;
board[row][col] = 1;
//进入下一层决策
placeQueen(board, row + 1);
//回溯
board[row][col] = 0;
}
}
public static void main(String[] args) {
NQueen.N = 8;
int[][] board = new int[N][N];
for (int i = 0; i < board.length; i++) {
for (int i1 = 0; i1 < board[i].length; i1++) {
board[i][i1] = 0;
}
}
placeQueen(board, 0);
System.out.println(sum);
}
}
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
[0, 1, 0, 0]
[0, 0, 0, 1]
[1, 0, 0, 0]
[0, 0, 1, 0]
------------
[0, 0, 1, 0]
[1, 0, 0, 0]
[0, 0, 0, 1]
[0, 1, 0, 0]
------------
2
Process finished with exit code 0