回溯算法:
解决一个回溯问题,实际上就是一个决策树的遍历过程。
1、路径:也就是已经做出的选择。2、选择列表:也就是你当前可以做的选择。3、结束条件:也就是到达决策树底层,无法再做选择的条件。
代码方面,回溯算法的框架:
result = [] def backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return
for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择
|
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。
全排列问题:
List<List<Integer>> res = new LinkedList<>();
List<List<Integer>> permute(int[] nums) { LinkedList<Integer> track = new LinkedList<>(); backtrack(nums, track); return res; }
void backtrack(int[] nums, LinkedList<Integer> track) { if (track.size() == nums.length) { res.add(new LinkedList(track)); return; }
for (int i = 0; i < nums.length; i++) { if (track.contains(nums[i])) continue; track.add(nums[i]); backtrack(nums, track); track.removeLast(); } }
|
N 皇后问题:
给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。
vector<vector<string>> res;
vector<vector<string>> solveNQueens(int n) { vector<string> board(n, string(n, '.')); backtrack(board, 0); return res; }
void backtrack(vector<string>& board, int row) { if (row == board.size()) { res.push_back(board); return; }
int n = board[row].size(); for (int col = 0; col < n; col++) { if (!isValid(board, row, col)) continue; board[row][col] = 'Q'; backtrack(board, row + 1); board[row][col] = '.'; } }
|
函数 backtrack
依然像个在决策树上游走的指针,通过 row
和 col
就可以表示函数遍历到的位置,通过 isValid
函数可以将不符合条件的情况剪枝:
这部分主要代码,其实跟全排列问题差不多,isValid
函数的实现也很简单:
bool isValid(vector<string>& board, int row, int col) { int n = board.size(); for (int i = 0; i < n; i++) { if (board[i][col] == 'Q') return false; } for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { if (board[i][j] == 'Q') return false; } for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { if (board[i][j] == 'Q') return false; } return true; }
|
数独:
bool backtrack(vector<string>& board, int row) { if (row == board.size()) { res.push_back(board); return true; } ... for (int col = 0; col < n; col++) { ... board[row][col] = 'Q';
if (backtrack(board, row + 1)) return true;
board[row][col] = '.'; }
return false; }
|
其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?
某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。