之前写的一篇:回溯算法——从LeetCode题海中总结常见套路https://xduwq.blog.csdn.net/article/details/105666096
个人来说还是挺有感悟和思考的,写一下第二篇进阶
目录
借鉴回溯思想的递归:LeetCode17.电话号码的字母组合
可重复回溯+去重剪枝框架:LeetCode491.递增子序列
基本不可重复回溯模板题:LeetCode面试题08.04.幂集
一步一步优化剪枝的可重复回溯:LeetCode39.组合总和
借鉴回溯思想的递归:LeetCode17.电话号码的字母组合
这一题不是常规的递归,也不是很常规的回溯,很有意思!
考虑一下:
如果给定的digitis大小是2,则两重循环可以搞定:
result = List()
for(i=0;i<len("abc");i++) {
for(j=0;j<len("def");j++)
tmp = i+j
result.add(tmp)
}
return result
如果给定的digits大小是3,那么三重循环可以搞定:
result = List()
for(i=0;i<len("abc");i+=1) {
for(j=0;j<len("def");j+=1) {
for(k=0;k<len("ghi");k+=1) {
tmp = i+j+k
result.add(tmp)
}
}
}
return result
同理如果digitis的大小是4,那么四重循环可以搞定……
所以咱们这个循环的次数是随着digitis的size来确定的,所以说常规的写for循环的方法肯定无法搞定!
这里就是递归的灵魂来了!
当然,每次递归的所得到的并不是最终可以用的答案,所以在递归开始的时候用if来筛选出符合我们标准的答案
这不也是回溯的小灵魂之一吗?
当然,这里的状态树如下,没有回退的步骤,所以说并不存在真正意义上的“回溯”!
class Solution {
public:
vector<string> ans;
vector<string> sList={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};// 字符表
vector<string> letterCombinations(string digits) {
if(digits.size()==0)
return {};
helper(digits, {}, 0);
return ans;
}
void helper(string digits, string s, int index){
if(index==digits.size()){
// 要筛选出符合长度条件的ans
ans.push_back(s);
return;
}else{
int pos = digits[index] - '0'; // 获取对应的下标
string temp = sList[pos]; // 获取对应映射的字符串
for(int i=0;i<temp.size();i++){ // 因为每一个对应的映射表里的每一个字符都要进行枚举,所以要循环遍历
helper(digits,s+temp[i],index+1); // 进行下一层的迭代
}
}
}
};
可重复回溯+去重剪枝框架:LeetCode491.递增子序列
这题有其他方法可以在更好的时空复杂度下完成任务,但是从常规的角度出发,是一道“回溯子集”问题!用常规的可重复回溯框架,然后在剪枝的上面多花点功夫,即可完成任务!万变不离其宗!
class Solution {
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
// 不能先排序,就是数组中取出符合条件的递增数组问题
vector<int> temp;
vector<vector<int> > ans;
backtravel(nums, 0, temp, ans);
// 再对ans进行去重
set<vector<int> > res(ans.begin(), ans.end());
return vector<vector<int> >(res.begin(), res.end());
}
// 回溯法求解子集问题
void backtravel(vector<int>& nums, int index, vector<int> temp, vector<vector<int> >& ans) {
// 筛选出没有重复出现并且size大于2
// 原来的find方法会超时
// if (temp.size()>=2 && find(ans.begin(), ans.end(), temp) == ans.end()) {
if (temp.size() >= 2) {
ans.push_back(temp);
}
for (int i = index; i<nums.size(); i++) {
// 此处要筛选出能放进temp当中的nums元素
if (!temp.empty() && temp.back() > nums[i])
continue;
temp.push_back(nums[i]);
backtravel(nums, i+1, temp, ans);
temp.pop_back();
}
}
};
经典可重复回溯框架:LeetCode77.组合
最经典的可重复回溯框架,但是如果不剪枝的话,复杂度会比较不理想
剪枝会有一些问题,我暂时还没有完全弄懂
在这里剪枝是这道题的灵魂!
class Solution {
private:
vector<vector<int>> ans;
public:
vector<vector<int>> combine(int n, int k) {
vector<int> temp;
traverback(temp, 1, n, k);
return ans;
}
void traverback(vector<int> temp, int index, int n, int k){
if(temp.size()==k){
ans.push_back(temp);
return;
}
for(int i=index; i<=n-(k-temp.size())+1; i++){ // 剪枝;不剪枝的写法是i<=n
temp.push_back(i);
traverback(temp,i+1, n, k);
temp.pop_back();
}
}
};
基本不可重复回溯模板题:LeetCode面试题08.04.幂集
class Solution {
private:
vector<vector<int>> ans;
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<int> temp;
ans.push_back(temp);
traverback(nums,0,temp);
return ans;
}
void traverback(vector<int>& nums, int index, vector<int> temp){
for(int i=index;i<nums.size();i++){
temp.push_back(nums[i]);
ans.push_back(temp);
traverback(nums, i+1, temp);
temp.pop_back();
}
}
};
非典型可重复回溯:LeetCode面试题08.09.括号
这道题的思路还是非常有意思的!
如果把n理解为可使用的左括号和右括号的数量,以这个为出发点来思考递归的思路
这道题无非就是需要分别把以n为个数的左右括号数分别用光,所以就有了以下思路的代码:
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
backtrack(res,n,0,"");
return res;
}
// left代表左括号数,rightright代表右括号数
void backtrack(vector<string>& res, int left, int right, string temp){
if(!right&&!left)
res.push_back(temp);
else{
if(left>0)
/*使用一个左括号,同时可使用右括号数加1,这样可避免生成无效括号*/
backtrack(res, left-1, right+1, temp+'(');
if(right>0)
/*可使用的右括号数大于0,则用来补齐原来的左括号*/
backtrack(res, left, right-1, temp+')');
}
}
};
一步一步优化剪枝的可重复回溯:LeetCode39.组合总和
这道题很明显 是可重复回溯模板典例,但是不剪枝的话肯定会超时!
看一下我的优化过程,励志呀!
套用模板不进行剪枝,超时:
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
// sort(candidates.begin(),candidates.end());
vector<int> temp;
backtraver(candidates,target,temp);
return ans;
}
void backtraver(vector<int> candidates,int target,vector<int> temp){
if(accumulate(temp.begin(),temp.end(),0)==target){
sort(temp.begin(),temp.end());
if(find(ans.begin(),ans.end(),temp)==ans.end()){
ans.push_back(temp);
return;
}
}
if(accumulate(temp.begin(),temp.end(),0)>target)
return;
for(int i=0;i<candidates.size();i++){
temp.push_back(candidates[i]);
backtraver(candidates,target,temp);
temp.pop_back();
}
}
};
可重复计算用的太多,先优化accumulate部分:
还是留下了弱者的泪水……
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
vector<int> temp;
backtraver(candidates,target,temp);
return ans;
}
void backtraver(vector<int> candidates,int target,vector<int> temp){
int tempSum = accumulate(temp.begin(),temp.end(),0);
if(tempSum==target){
sort(temp.begin(),temp.end());
if(find(ans.begin(),ans.end(),temp)==ans.end()){
ans.push_back(temp);
return;
}
}
if(tempSum>target)
return;
for(int i=0;i<candidates.size();i++){
if((tempSum+candidates[i])>target){
break;
}else{
temp.push_back(candidates[i]);
backtraver(candidates,target,temp);
temp.pop_back();
}
}
}
};
将求和步骤写进递归中,彻底优化accumlate
时间上会优化一些,但是没有本质改变!
再次流下弱者的泪水:
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
vector<int> temp;
backtraver(candidates,target,temp,0);
return ans;
}
void backtraver(vector<int> candidates,int target,vector<int> temp,int sum){
if(sum==target){
// 还要进行一次去重
sort(temp.begin(),temp.end());
if(find(ans.begin(),ans.end(),temp)==ans.end()){
ans.push_back(temp);
return;
}
}
for(int i=0;i<candidates.size();i++){
if((sum+candidates[i])>target){
break;
}else{
temp.push_back(candidates[i]);
backtraver(candidates,target,temp,sum+candidates[i]);
temp.pop_back();
}
}
}
};
设置回溯函数起始下标进行剪枝
时间复杂度有了一个数量级的提高!
因为每一次调用回溯函数的时候都会少了一个sort和find的步骤!
class Solution {
private:
vector<vector<int>> ans;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
vector<int> temp;
backtraver(candidates,target,temp,0,0);
return ans;
}
// 设置起始下标进行剪枝
void backtraver(vector<int> candidates,int target,vector<int> temp,int sum,int start){
if(sum==target){
ans.push_back(temp);
return;
}
for(int i=start;i<candidates.size();i++){
if((sum+candidates[i])>target){
break;
}else{
temp.push_back(candidates[i]);
backtraver(candidates,target,temp,sum+candidates[i],i);
temp.pop_back();
}
}
}
};
善用this指针和全局变量优化空间复杂度
将每次递归调用所需要但不会改变的用全局变量进行处理,空间复杂度大幅优化!
class Solution {
private:
vector<vector<int>> ans;
vector<int> candidates;
int target;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
// 递归函数中所要使用的全局变量这样优化可以大幅提高空间复杂度
this->candidates = candidates;
this->target = target;
vector<int> temp;
backtraver(temp,0,0);
return ans;
}
// 设置起始下标进行剪枝
void backtraver(vector<int> temp,int sum,int start){
if(sum==target){
ans.push_back(temp);
return;
}
for(int i=start;i<candidates.size();i++){
if((sum+candidates[i])>target)
break;
else{
temp.push_back(candidates[i]);
backtraver(temp,sum+candidates[i],i);
temp.pop_back();
}
}
}
};
在剪枝上稍作修改的LeetCode40.组合总和II
在上一题的基础上稍作剪枝即可
class Solution {
private:
vector<vector<int>> ans;
vector<int> candidates;
int target;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
// 递归函数中所要使用的全局变量这样优化可以大幅提高空间复杂度
this->candidates = candidates;
this->target = target;
vector<int> temp;
backtraver(temp,0,0);
return ans;
}
// 设置起始下标进行剪枝
void backtraver(vector<int> temp,int sum,int start){
if(sum==target){
if(find(ans.begin(),ans.end(),temp)==ans.end()){
ans.push_back(temp);
return;
}
}
for(int i=start;i<candidates.size();i++){
if((sum+candidates[i])>target)
break;
else{
temp.push_back(candidates[i]);
backtraver(temp,sum+candidates[i],i+1);
temp.pop_back();
}
}
}
};
参考