【数据结构与算法】回溯算法

回溯法框架

1、 路径: 也就是已经做出的选择。
2、 选择列表: 也就是你当前可以做的选择。
3、 结束条件: 也就是到达决策树底层, ⽆法再做选择的条件。

回溯法框架:

result = []
def backtrack(路径, 选择列表):
	if 满⾜结束条件:
		result.add(路径)
		return
for 选择 in 选择列表:
	做选择
	backtrack(路径, 选择列表)
	撤销选择

回溯法核心框架

for 选择 in 选择列表:

	# 做选择
	将该选择从选择列表移除
	路径.add(选择)
	backtrack(路径, 选择列表)
	# 撤销选择
	路径.remove(选择)
	将该选择再加⼊选择列表

子集

78. 子集

题目描述

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:

输入:nums = [0]
输出:[[],[0]]

解题思路

现在让你求 [1,2,3] 的子集, 如果你知道了 [1,2] 的子集, 是否可以推导出 [1,2,3] 的子集呢? 先把 [1,2] 的子集写出来瞅瞅:
[ [],[1],[2],[1,2] ]

你会发现这样一个规律:

subset( [1,2,3] ) - subset( [1,2] )
= [3],[1,3],[2,3],[1,2,3]

这个结果, 就是把 sebset( [1,2] ) 的结果中每个集合再添加上 3。
换句话说, 如果 A = subset([1,2]) , 那么:
subset( [1,2,3] )
= A + [A[i].add(3) for i = 1…len(A)]

[1,2,3] 的子集可以由 [1,2] 追加得
出, [1,2] 的子集可以由 [1] 追加得出, base case 显然就是当输人集合
为空集时, 输出子集也就是一个空集。

public class Subsets {
    public List<List<Integer>> subsets(int[] nums){
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null)
            return res;
        helper(res,nums,new ArrayList<>(),0);
        return res;
    }

    private void helper(List<List<Integer>> res, int[] nums, ArrayList<Integer> list, int index) {
        if (index == nums.length){
            res.add(new ArrayList<>(list));
            return;
        }
        helper(res,nums,list,index+1);
        list.add(nums[index]);
        helper(res,nums,list,index+1);
        list.remove(list.size() - 1);
    }
}

排列

全排列

题目描述

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

解题思路

在这里插入图片描述
排列问题每次通过 contains 方法来排除在 track中已经选择过的数字; 而组合问题通过传一个 start 参数, 来排除start 索引之前的数字。

public class Permute {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        //记录路径
        LinkedList<Integer> track = new LinkedList<>();
        backtrack(nums,track);
        return res;
    }

    private void backtrack(int[] nums, LinkedList<Integer> track) {
        //到达叶子结点
        if (track.size() == nums.length){
            res.add(new LinkedList<>(track));
            return;
        }
        for (int num : nums) {
            //排除不合法的选择
            if (track.contains(num)) {
                continue;
            }
            //做选择
            track.add(num);
            //进入下一层决策树
            backtrack(nums, track);
            //取消选择
            track.removeLast();
        }
    }
}

组合

组合

题目描述

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

解题思路

如输入 n = 4, k = 2 , 输出如下结果, 顺序无所谓, 但是不能包含重复(按照组合的定义, [1,2] 和 [2,1] 也算重复) :
[ [1,2], [1,3], [1,4], [2,3], [2,4], [3,4] ]

public class Combine {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        if (k <= 0 || n < k)
            return res;
        //从1开始是题目的设定
        Deque<Integer> path = new ArrayDeque<>();
        dfs(n,k,1,path,res);
        return res;
    }

    private void dfs(int n, int k, int begin, Deque<Integer> path, List<List<Integer>> res) {
        //递归终止条件是path的长度等于k
        if (path.size() == k){
            res.add(new ArrayList<>(path));
            return;
        }
        //遍历可能的搜索起点
        for (int i = begin;i <= n;i++){
            //向路径变量里添加一个数
            path.addLast(i);
            //下一轮搜索,设置的搜索起点要加1,因为组合数里不允许出现重复的元素
            dfs(n,k,i+1,path,res);
            //回溯
            path.removeLast();
        }
    }
}

三者总结

子集问题可以利用数学归纳思想, 假设已知一个规模较小的问题的结果, 思考如何推导出原问题的结果。 也可以用回溯算法, 要用 start 参数排除已选择的数字。

组合问题利用的是回溯思想, 结果可以表示成树结构, 我们只要套用回溯算法模板即可, 关键点在于要⽤⼀个 start 排除已经选择过的数字。

排列问题是回溯思想, 也可以表示成树结构套用算法模板, 关键点在于使用contains 方法排除已经选择的数字, 前文有详细分析, 这里主要是和组合问题作对比。

解数独

数独问题
在这里插入图片描述

  1. 从 1 到 9 就是选择, 全部试一遍不就行了
  2. 当 j 到达超过每一行的最后一个索引时, 转为增加 i 开始穷举下一行, 并且在穷举之前添加一个判断, 跳过不满足条件的数字
  3. 显然 r == m 的时候就说明穷举完了最后一行, 完成了所有的穷举, 就是 base case。
public class solveSudu {
    public void solveSudoku(char[][] board) {
        if (board == null || board.length == 0)
            return;
        backtrack(board,0,0);

    }

    private boolean backtrack(char[][] board, int i, int j) {

        int m = 9;
        int n = 9;
        if(j == n){
            //穷举到最后一列的话,就换到下一行重新开始
            return backtrack(board,i+1,0);
        }
        if (i == m){
            //找到一个可行解,触发base case
            return true;
        }
        if (board[i][j] != '.'){
            //有预设数字,不用我们穷举
            return backtrack(board,i,j+1);
        }
        for (char ch = '1'; ch <= '9'; ch++) {
            //如果遇到不合法的数字,就跳过
            if (!isVaild(board,i,j,ch)){
                continue;
            }
            board[i][j] = ch;
            //如果找到一个可行解,立即结束
            if (backtrack(board,i,j+1)){
                return true;
            }
            board[i][j] = '.';
        }
        //穷举完1——9,依然没有找到可行解
        //需要前面的格子换个数字穷举
        return false;

    }

    private boolean isVaild(char[][] board, int r, int c, int n) {
        for (int i = 0; i < 9; i++) {
            //判断行是否存在重复
            if (board[r][i] == n)
                return false;
            //判断列是否存在重复
            if (board[i][c] == n)
                return false;
            //判断3x3方框是否存在重复
            if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)
                return false;
        }
        return true;
    }


}

N皇后

 public List<List<String>> solveNQueens(int n) {
     List<List<String>> result = new ArrayList<>();
     List<char[]> board = new ArrayList<>();
     for (int i = 0; i < n; i++) {
         char[] chars = new char[n];
         Arrays.fill(chars,'.');
         board.add(chars);
     }
     backtracking(n,0,board,result);
     return result;
 }
 private void backtracking(int n, int row, List<char[]> board, List<L
     if (n == row){
         List<String> path = new ArrayList<>();
         for (char[] chars : board) {
             path.add(new String(chars));
         }
         result.add(path);
         return;
     }
     for (int col = 0; col < n; col++) {
         if (isValid(row,col,n,board)){
             board.get(row)[col] = 'Q';
             backtracking(n,row+1,board,result);
             board.get(row)[col]='.';
         }
     }
 }
 private boolean isValid(int row, int col, int n, List<char[]> board)
     for (int i = 0; i < row; i++) {
         if (board.get(i)[col] == 'Q'){
             return false;
         }
     }
     for (int i = row - 1,j = col - 1; i >= 0 && j >= 0 ; i--,j--) {
         if (board.get(i)[j] == 'Q'){
             return false;
         }
     }
     for (int i = row - 1,j = col + 1;i >= 0 && j < n;i--,j++){
         if (board.get(i)[j] == 'Q'){
             return false;
         }
     }
     return true;
 }
posted @ 2021-04-23 19:18  your_棒棒糖  阅读(59)  评论(0编辑  收藏  举报