可解决的问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,数独等等
总结模板
1 2 3 4 5 6 7 8 9 10 11 12
| void backtracking(参数) { if (终止条件) { 存放结果; return; }
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); 回溯,撤销处理结果 } }
|
- 回溯法抽象为树形结构后,其遍历过程就是:for循环横向遍历,递归纵向遍历,回溯不断调整结果集。
- 一般需要两个全局变量,或者放在函数形参里面;
1 2
| vector<vector<int>> result; vector<int> path;
|
还有一个可选的位于回溯函数形参的int型startIndex
变量,用来记录本层递归中,集合从哪里开始遍历;
组合问题,startIndex用在循环中for (int i = startIndex; i < n; i++)
;
排列问题,不用startIndex,是`for (int i = 0; i < n; i++);
不重复选取某一元素backTrack(candidates, target, i);
重复选取某一元素backTrack(candidates, target, i + 1);
结果集去重,注意先排序;
- 组合问题可以使用下标去重:
if (i > startIdx && nums[i] == nums[i - 1]) continue;
- 排列问题去重使用bool数组,注意树枝和树层去重,最好画N叉树图看,更直观;
- 也还可以使用set集合去重;
字符串分割时要有判断条件if (isValid(s, startIndex, i));
组合问题
一个集合
- 需要startIndex;
- 递归传参时
i+1
表示不能重复使用元素;
- 递归传参时
i
表示可以重复使用元素;
集合元素唯一
①如果是一个元素唯一的集合来求组合的话,就需要startIndex,并且递归时用i+1
;
1 2 3 4 5
| for (int i = startIndex; i <= n; i++) { path.push_back(i); backtracking(n, k, i + 1); path.pop_back(); }
|
②如果题目说明可以重复选取元素,则仍然用startIndex,但递归时不用i+1
而是i
;
1 2 3 4 5 6 7 8
| for (int i = startIndex; i < candidates.size(); i++) { sum += candidates[i]; path.push_back(candidates[i]); backtracking(candidates, target, sum, i); sum -= candidates[i]; path.pop_back(); }
|
集合元素不唯一
即组合去重问题,集合(数组)有重复元素,但还不能有重复的组合,一般要求集合即数组排序。仍然需要用到startIndex,并且加一个bool型数组used,用来记录同一树枝上的元素是否使用过。要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
在candidates[i] == candidates[i - 1]的情况下:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { continue; } sum += candidates[i]; path.push_back(candidates[i]); used[i] = true; backtracking(candidates, target, sum, i + 1, used); used[i] = false; sum -= candidates[i]; path.pop_back(); }
|
多个集合
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合,本题增加index来表示深度,以判断递归结束条件。
1 2 3 4 5
| for (int i = 0; i < letters.size(); i++) { s.push_back(letters[i]); backtracking(digits, index + 1); s.pop_back(); }
|
排列问题
无重复元素的序列
例:给定一个没有重复数字的序列,返回其所有可能的全排列。
输入: [1,2,3]
输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
思路:
- 不用startIndex
- 需要一个used数组,标记已经选择的元素
当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
1 2 3 4 5 6 7 8
| for (int i = 0; i < nums.size(); i++) { if (used[i] == true) continue; used[i] = true; path.push_back(nums[i]); backtracking(nums, used); path.pop_back(); used[i] = false; }
|
注意:
- 每层都是从0开始搜索
- 需要used数组记录path里都放了哪些元素了
有重复元素的序列
例:输入:nums = [1,1,2]
输出: [[1,1,2], [1,2,1], [2,1,1]]
需要去重,去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void backtracking (vector<int>& nums, vector<bool>& used) { if (path.size() == nums.size()) { result.push_back(path); return; } for (int i = 0; i < nums.size(); i++) { if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { continue; } if (used[i] == true) continue; used[i] = true; path.push_back(nums[i]); backtracking(nums, used); path.pop_back(); used[i] = false; } }
|
对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
分割问题
切割问题类似组合问题,需要startIndex;
对于分割回文串问题:
1 2 3
| vector<vector<string>> result; vector<string> path; void backtracking (const string& s, int startIndex)
|
注意切割过的位置,不能重复切割,所以是backtracking(s, i + 1); 传入下一层的起始位置为i + 1。
1 2 3 4 5 6 7 8 9 10 11
| for (int i = startIndex; i < s.size(); i++) { if (isValid(s, startIndex, i)) { string str = s.substr(startIndex, i - startIndex + 1); path.push_back(str); backtracking(s, i + 1); path.pop_back(); } else { continue; } }
|
棋盘问题
例一,N皇后
因为是每一行都放一个皇后,所以是一维递归;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
void backtracking(int n, int row, vector<string>& chessboard) { if (row == n) { result.push_back(chessboard); return; } for (int col = 0; col < n; col++) { if (isValid(row, col, chessboard, n)) { chessboard[row][col] = 'Q'; backtracking(n, row + 1, chessboard); chessboard[row][col] = '.'; } } }
|
例二,数独
二维递归!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| bool backtracking(vector<vector<char>>& board) { for (int i = 0; i < board.size(); i++) { for (int j = 0; j < board[0].size(); j++) { if (board[i][j] != '.') continue; for (char k = '1'; k <= '9'; k++) { if (isValid(i, j, k, board)) { board[i][j] = k; if (backtracking(board)) return true; board[i][j] = '.'; } } return false; } } return true; }
|
【参考资料】代码随想录、力扣