回溯算法思想
回溯算法相当于一个决策树,解决一个决策树的遍历问题,需要考虑:
- 路径:已经做出的选择
- 选择列表:可以做的选择
- 结束条件:到达决策树底层,无法做出选择的条件
回溯框架:
1 2 3 4 5 6 7 8 9 10
| result = [] def backtrack(路径,选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径,选择列表) 撤销选择
|
子集穷举
- 给定一组不含重复元素的整数数组
nums
,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
力扣题目链接
[1, 2, 3]
的全部子集为递归树上的所有节点,for循环横向遍历,递归纵向遍历
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| vector<vector<int>> res;
vector<vector<int>> subsets(vector<int>& nums) { vector<int> path; backtrack(nums, 0, path); return res; }
void backtrack(vector<int>& nums, int start, vector<int>& path) { res.push_back(path); if (start >= nums.size()) return; for (int i = start; i < nums.size(); i++) { path.push_back(nums[i]); backtrack(nums, i + 1, path); path.pop_back(); } }
|
- 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
- 输入: [1,2,2]
- 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
力扣题目链接
解法:回溯+去重,同一层横向遍历需要去重(需要首先对集合排序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| vector<vector<int>> result; vector<int> path; void backtrack(vector<int>& nums, int start, vector<int>& path) { result.push_back(path); for (int i = start; i < nums.size(); i++) { if (i > start && nums[i] == nums[i - 1] ) { continue; } path.push_back(nums[i]); backtrack(nums, i + 1, path); path.pop_back(); } }
vector<vector<int>> subsetsWithDup(vector<int>& nums) { sort(nums.begin(), nums.end()); vector<int> path; backtrack(nums, 0, path); return result; }
|
组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出: [[2,4], [3,4], [2,3], [1,2], [1,3], [1,4]]
解法:combine(4, 2)
的结果,决策树的高度为k,宽度为n的所有叶子节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| vector<vector<int>> result;
vector<vector<int>> combine(int n, int k) { if (k <= 0 || n <= 0) return res; vector<int> path; backtrack(n, k, 1, path); return result; }
void backtrack(int n, int k, int start, vector<int>& path) { if (k == path.size()) { result.push_back(path); return; } for (int i = start; i <= n; i++) { path.push_back(i); backtracking(n, k, i + 1, path); path.pop_back(); } }
|
组合可以看作特定长度的子集,combine(3, 2)
等价于 subset([1, 2, 3])
长度为2 的子集
排列
- 给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
- 输入:
[1,2,3]
- 输出:
[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
力扣题目链接
解法:排列是有序的, [1,2]
和 [2,1]
是两个集合,处理全排列问题不需要start防止重复。需要排除已经选择过的数字,将所有叶子节点作为结果。可以使用一个used
数组,标记此时path
中已经选择的元素,一个排列里一个元素只能使用一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| vector<vector<int>> result; vector<int> path; 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 (used[i] == true) continue; used[i] = true; path.push_back(nums[i]); backtracking(nums, used); path.pop_back(); used[i] = false; } }
vector<vector<int>> permute(vector<int>& nums) { vector<bool> used(nums.size(), false); backtracking(nums, used); return result; }
|
- 给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
- 输入:
nums = [1,1,2]
- 输出:
[[1,1,2], [1,2,1], [2,1,1]]
示例 2:
- 输入:
nums = [1,2,3]
- 输出:
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
力扣题目链接
解法:回溯+去重,去重需要对元素序列排序,方便通过相近节点判断是否重复使用。
去重逻辑的关键代码:
1 2 3 4 5
|
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { continue; }
|
总体代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| vector<vector<int>> result; vector<int> path; 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] == false) { used[i] = true; path.push_back(nums[i]); backtracking(nums, used); path.pop_back(); used[i] = false; } } }
vector<vector<int>> permuteUnique(vector<int>& nums) { sort(nums.begin(), nums.end()); vector<bool> used(nums.size(), false); backtracking(nums, used); return result; }
|