现在到了回溯算法的第6部分,今天的题目很难:
今天这三道题都非常难,那么这么难的题,为啥一天做三道?
因为 一刷 也不求大家能把这么难的问题解决,所以 大家一刷的时候,就了解一下题目的要求,了解一下解题思路,不求能直接写出代码,先大概熟悉一下这些题,二刷的时候,随着对回溯算法的深入理解,再去解决如下三题。
今天的任务其实是对回溯算法章节做一个总结就行。
今日任务
- 回溯算法总结
- 332.重新安排行程(可跳过)
- 51.N皇后(可跳过)
- 37.解数独(可跳过)
回溯算法总结
一、回溯算法总结
1.1 回溯算法理论篇
1、回溯是递归的副产品,只要有递归就会有回溯,所以回溯也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都用到了递归。
2、回溯算法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。
3、回溯算法可以解决的问题:
- 组合问题:N个数里面按照一定规则找出k个数的集合;
- 排列问题:N个数按一定规则全排列,有几种排列方式;
- 切割问题:一个字符串按一定规则有几种切割方式;
- 子集问题:一个N个数的集合里面有多少符合条件的子集;
- 棋盘问题(最难):N皇后,解数独。
4、回溯代码的模板:
void backtracking(参数){
if(终止条件){
存放结果
return;
}
// 遍历节点中的元素(如果是排列问题的话,i从0开始)
for(int i = startIndex; i < ***; i++){
处理节点;
backtracking();
回溯
}
}
5、在这个模板中,for循环负责横向遍历,递归负责纵向遍历,回溯不断调整结果集。
6、优化回溯算法只有剪枝一种方法。
1.2 组合问题:
1.2.1 普通的n个数中选k个数
对于组合问题,为了避免被重新选择,所以需要用一个startIndex来计数。
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(int n, int k, int startIndex){
if(path.size() == k){
result.push_back(path);
return;
}
for(int i = startIndex; i <= n; i++){
path.push_back(i);
backtracking(n, k, i+1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
如果对此过程进行剪枝优化,只需要在for循环中做调整即可:i至多从n-(k-path.size()) + 1开始,调整如下:
for(int i = 0; i < n-(k-path.size()) + 1)
1.2.2 组合总和(一)
此题的特点为:取得时候树枝上是可以重复的,所以要在递归处做文章,但是for循环处的不可以变,所以i还是从startIndex开始的。
终止条件为:元素的和 > target了
在递归时需要注意:因为元素是可以重复的,所以递归时传入的是i,而非 i + 1;
其次,剪枝操作通常都是在for循环中做文章的,然后 数组也需要先进行排序。
剪枝的操作(可以不进行剪枝):
for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
1.2.3 组合总和(二)
此题的特点为:原数组中有重复元素,需要在树层上进行去重,所以做法是先排序,然后使用used数组进行去重(也可采用unordered_map这种哈希表进行去重)
终止条件:sum > target时 return; sum == target 存到result中,然后return;
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracing(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used){
if(sum > target) return;
if(sum == target){
result.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size(); i++){
if(i>0 && candidates[i] == candidates[i-1] && used[i-1] == 0){
continue;
}
path.push_back(candidates[i]);
sum += candidates[i];
used[i] = true;
backtracing(candidates, target, sum, i+1,used);
sum -= candidates[i];
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);
sort(candidates.begin(), candidates.end());
backtracing(candidates, target, 0, 0, used);
return result;
}
};
1.2.4 组合总和(三)
终止条件为元素的个数是否为k
还是原来组合的方法,然后只需要在存放到result的时候验证下path中元素的和是否为n即可。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtraing(int n, int k, int startIndex){
if(path.size() == k){
int sum = 0;
for(int i = 0; i < k; i++){
sum += path[i];
}
if(sum == n){
result.push_back(path);
}
return;
}
for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++){
path.push_back(i);
backtraing(n, k, i + 1);
path.pop_back();
}
return;
}
vector<vector<int>> combinationSum3(int k, int n) {
backtraing(n, k, 1);
return result;
}
};
1.2.5 多个集合求组合(电话号码)
此题的特点为:每个数字代表的是不同集合,在不同集合之间的组合。
非常巧妙地利用数组做了一个字符串映射。
class Solution {
public:
vector<string> result;
string s;
string letter_map[10] = {"","", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
// 此处的index表示字符串的索引值
void backTracking(string digits, int index){
// 到达叶子节点位置
if(digits.size() == index){
result.push_back(s);
return;
}
// 取出digits中的元素
int digit = digits[index] - '0';
string letter = letter_map[digit];
for(int i = 0; i < letter.size(); i++){
s.push_back(letter[i]);
backTracking(digits, index + 1);
s.pop_back();
}
return ;
}
vector<string> letterCombinations(string digits) {
if(digits.size() == 0){
return result;
}
backTracking(digits, 0);
return result;
}
};
1.3 切割问题
此类题目就是要注意:startIndex就是切割的板子;切割出来地子串位于 [ startIndex, i ] 之间的。
1.3.1 分割回文串
1.3.2 复原IP地址
1.4 子集问题
注意子集问题去重一定要先排序
1.4.1 子集
此类题目的特点为收集结果的时机是在每个节点都进行收集,所以模板的顺序需要调整下。
result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉结果
if (startIndex >= nums.size()) { // 终止条件可以不加
return;
}
1.4.2 子集(二)
当然还有子集问题的去重,同样采用排序 + used数组:
1.4.3 递增子序列
1.5 排列问题
1.5.1 排列
它的遍历顺序跟其他的都不一样,尤其是在for循环那里,需要i从0开始,然后需要使用used数组记录path里面都存放了哪些元素。
只有没有使用过,才能取值
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& nums, vector<bool>& used, int pointnum){
if(pointnum == nums.size()){
result.push_back(path);
return;
}
for(int i = 0; i<nums.size(); i++){
if(used[i] == false){
used[i] = true;
pointnum += 1;
path.push_back(nums[i]);
backtracking(nums, used, pointnum);
used[i] = false;
pointnum -= 1;
path.pop_back();
}else{
continue;
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);
backtracking(nums, used, 0);
return result;
}
};
1.5.2 排列(二)
在数组中有重复元素,因为排列的话,不可以对原数组进行排序,所以只能换一种去重的方法(采用set哈希表)
二、N皇后问题
待补充(二刷再说)
三、解数独
待补充(二刷再说)