算法篇——回溯算法

115 阅读3分钟

回溯法

解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

一、全排列问题

我们在高中的时候就做过排列组合的数学题,我们也知道 n 个不重复的数,全排列共有 n! 个。

PS:为了简单清晰起见,我们这次讨论的全排列问题不包含重复的数字

那么我们当时是怎么穷举全排列的呢?比方说给三个数 [1,2,3],你肯定不会无规律地乱穷举,一般是这样:

先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位……

其实这就是回溯算法,我们高中无师自通就会用,或者有的同学直接画出如下这棵回溯树:

img

只要从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。我们不妨把这棵树称为回溯算法的「决策树」

为啥说这是决策树呢,因为你在每个节点上其实都在做决策。比如说你站在下图的红色节点上:

你现在就在做决策,可以选择 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