【数据结构与算法】回溯算法
回溯法框架
1、 路径: 也就是已经做出的选择。
2、 选择列表: 也就是你当前可以做的选择。
3、 结束条件: 也就是到达决策树底层, ⽆法再做选择的条件。
回溯法框架:
result = []
def backtrack(路径, 选择列表):
if 满⾜结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
回溯法核心框架
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
backtrack(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加⼊选择列表
子集
题目描述
给你一个整数数组 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 到 9 就是选择, 全部试一遍不就行了
- 当 j 到达超过每一行的最后一个索引时, 转为增加 i 开始穷举下一行, 并且在穷举之前添加一个判断, 跳过不满足条件的数字
- 显然 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;
}