0%

回溯算法思想


回溯算法思想

回溯算法相当于一个决策树,解决一个决策树的遍历问题,需要考虑:

  • 路径:已经做出的选择
  • 选择列表:可以做的选择
  • 结束条件:到达决策树底层,无法做出选择的条件

回溯框架:

1
2
3
4
5
6
7
8
9
10
result = []
def backtrack(路径,选择列表):
if 满足结束条件:
result.add(路径)
return

for 选择 in 选择列表:
做选择
backtrack(路径,选择列表)
撤销选择

子集穷举

  1. 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

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

力扣题目链接

image-20220404212935154

[1, 2, 3]的全部子集为递归树上的所有节点,for循环横向遍历,递归纵向遍历

  • 递归参数:二维数组result存放子集组合,一维数组path收集路径元素,需要start参数控制递归

  • 递归终止条件:start大于等于数组的长度。没有元素可取

    1
    2
    3
    if (start >= nums.size()) {
    return;
    }
  • 单层逻辑:遍历整棵树,不需要剪枝

完整代码:

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; //可以不加,每次递归的下一层就是从i+1开始的

//从start开始,防止产生重复子集
for (int i = start; i < nums.size(); i++) {
//做选择
path.push_back(nums[i]);
//递归回溯
backtrack(nums, i + 1, path);
//撤销选择
path.pop_back();
}
}
  1. 给定一个可能包含重复元素的整数数组 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] ) { // 注意这里使用i > start
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) {
//到达叶子节点更新result
if (k == path.size()) {
result.push_back(path);
return;
}
//i从start开始递增
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. 给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

  • 输入: [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; // path里已经收录的元素,直接跳过
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); //标记bool数组
backtracking(nums, used);
return result;
}
  1. 给定一个可包含重复数字的序列 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
// used[i - 1] == false,说明同一树层nums[i - 1]使用过
// 如果同一树层nums[i - 1]使用过则直接跳过
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++) {
// used[i - 1] == true,说明同一树枝nums[i - 1]使用过
// used[i - 1] == false,说明同一树层nums[i - 1]使用过
// 如果同一树层nums[i - 1]使用过则直接跳过
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;
}