目录
一.子集
题目描述:
解题思路:
1.暴力枚举即可,就是每个元素有两种选择,一个是要,一个是不要。我们将其全部枚举出来即可:以数组[1,2,3]为例:
总结一下就是:
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择对应过程图:
对应代码:
class Solution { public: vector<vector<int>>ans;//记录答案 vector<vector<int>> subsets(vector<int>& nums) { vector<int>tmp; process(nums,0,tmp); return ans; } void process(vector<int>&nums,int index,vector<int>&tmp){ if(nums.size()==index){ ans.push_back(tmp); return; } //不选择当前的数 process(nums,index+1,tmp); tmp.push_back(nums[index]); //选择当前的数 process(nums,index+1,tmp); tmp.pop_back();//递归回来的时侯删掉 } };
二.子集II
题目描述:
解题思路:
本题与上题不同的是数组中有重复的元素,这样我们在一个数选或者一个数不选的时候就有可能出现重复的子集,而题目要求我们不能出现重复的子集,所以在这里我们需要剪枝,这里以数组[1,2,2,3]为例
这里出现了重复的子集,那么怎么过滤掉重复的呢,就是在同一深度的两个不同的分支,如果当前元素和前面的元素相同,我们就跳过。如下图所示。
对应代码:
class Solution { public: vector<int>tmp; vector<vector<int>>ans; vector<vector<int>> subsetsWithDup(vector<int>& nums) { sort(nums.begin(),nums.end());//注意一定要排序 //方便去重 process(nums,0,INT_MAX); return ans; } void process(vector<int>&nums,int index,int prev) //prev代表前一个已经被选择的元素 { if(index==nums.size())//如果已经到了最后了 { ans.push_back(tmp); return; } tmp.push_back(nums[index]);//选择当前的数 process(nums,index+1,nums[index]); tmp.pop_back();//撤回选择 if(nums[index]!=prev)//如果当前数和前一个选择的数不相等才进行下一个分支 process(nums,index+1,prev); } };
三.全排列
题目描述:
解题思路:
每个元素在自己可以尝试的位置,每一个位置都尝试一下。每个元素只能尝试自己和自己后面的位置。下面展示数组[1,2,3]的全排列
对应代码:
class Solution { public: vector<vector<int>>ans; vector<vector<int>> permute(vector<int>& nums) { process(nums,0); return ans; } void process(vector<int>&nums,int i) { if(i==nums.size())//不能交互 { ans.push_back(nums); return; } for(int j=i;j<nums.size();j++) { swap(nums[i],nums[j]);//来到当前位置 process(nums,i+1); swap(nums[i],nums[j]);//撤销决定 } } };
四.全排列II
对应letecode链接:
题目描述:
解题思路:
本题与上题唯一不同的是数组中有重复的元素,因此如果我们不做任何去重处理的话肯定会出现重复的全排列。因此我们需要进行去重,如何去重了?我们只需要定义一张哈希表记录当前已经来到了的位置,如果来到的下一个位置和已经在哈希表里面记录过了则跳过。
具体请看代码:
class Solution { public: vector<vector<int>>ans;//记录答案 vector<vector<int>> permuteUnique(vector<int>& nums) { sort(nums.begin(),nums.end());//排序更容易去重不排序也行 process(nums,0); return ans; } void process(vector<int>&nums,int i) { if(i==nums.size()) { ans.push_back(nums); } unordered_set<int>Hash;//记录访问过了的位置 for(int j=i;j<nums.size();j++) { if(!Hash.count(nums[j]))//查看上次来到的位置和这次来到位置是否相同 { Hash.insert(nums[j]); swap(nums[i],nums[j]); process(nums,i+1);//去下一个位置 swap(nums[i],nums[j]);//撤销决定 } } } };
五.字符全排列
题目描述:
解题思路:
本题和上题没有什么区别就只是一个是数字一个是字符而已。思路上题已经讲过在这里就只给出代码。
对应代码:
class Solution { public: vector<string>ans; vector<string> permutation(string s) { process(s,0); return ans; } void process( string&str,int i) { if(i==str.size()) { ans.push_back(str); } vector<bool>isVisit(256,false); for(int j=i;j<str.size();j++) { if(!isVisit[str[j]-'a']) { isVisit[str[j]-'a']=true; swap(str[i],str[j]); process(str,i+1); swap(str[i],str[j]); } } } };
六.字符串大小全排列
题目描述:
解题思路:
每一个位置如果是字母就有两种选择:1.不变,2变成大小或者小写。如果不是字符就不变直接到下一个位置去。
七.组合总和
题目描述:
解题思路:
同样的本题两个元素有选和不选两种选择,只不过本题每个元素可以选择无数次,按照之前的方法即可:
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择本题可以先对数组排序方便递归过程中的剪枝,具体请看代码:
对应代码:
class Solution { public: vector<vector<int>>ans;//记录答案 vector<int>tmp; vector<vector<int>> combinationSum(vector<int>& candidates, int target) { sort(candidates.begin(),candidates.end());//排序方便后序剪枝优化 dfs(candidates,target,0); return ans; } void dfs(vector<int>&nums,int target,int index) { if(target==0)//是否已经找到答案了 { ans.push_back(tmp); return; } for(int i=index;i<nums.size();i++) { if(nums[i]>target)//如果当前数大于target由于数组有序所以后面的数都是大于target //不可能在凑出target { return; } tmp.push_back(nums[i]);//选择当前的数 dfs(nums,target-nums[i],i);//注意不是i+1而是i因为一个数可以选择多次 tmp.pop_back();//撤销决定 } } };
八.组合总和II
题目描述:
解题思路:
本题和上题基本上没有什么变化,只不过多了一步去重的操作:
对应代码:
class Solution { public: vector<vector<int>>ans; vector<int>tmp; vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { sort(candidates.begin(),candidates.end()); process(candidates,target,0); return ans; } void process(vector<int>&nums,int target,int index) { if(target==0) { ans.push_back(tmp); return; } for(int i=index;i<nums.size();i++) { if(i>index&&nums[i]==nums[i-1])//防止出现重复元素 { continue; } if(nums[i]>target)//有序数组后面不可能搞出target了 { return; } tmp.push_back(nums[i]);//选择当前的数 process(nums,target-nums[i],i+1); tmp.pop_back();//撤销选择 } } };
九.组合总和III
题目描述:
解题思路:
本题解题思路和上题基本一样而且还不要去重,是上题的弱化版本,具体请看代码。
对应代码:
class Solution { public: vector<int>tmp; vector<vector<int>>ans; vector<vector<int>> combinationSum3(int k, int n) { process(k,1,n); return ans; } void process(int k,int index,int taraget) { if(k==0&&taraget==0)//说明已经找到了 { ans.push_back(tmp); return; } for(int i=index;i<=9;i++)//从index到9; { tmp.push_back(i);//选择当前的数 process(k-1,i+1,taraget-i); tmp.pop_back();//撤回选择 } } };
十.组合总和IV
题目描述:
解题思路:
本题的解题思路依然是暴力递归和前面题目不同的是,只不过每次都可以从0号位置开始。具体请看代码:
对应代码:
int combinationSum4(vector<int>& nums, int target) { return process(nums,target); } int process(vector<int>&nums,int target) { if(target<0)//如果target小于0那么没有方法数 { return 0; } if(target==0)//到0了 { return 1; } int ans=0; for(int i=0;i<nums.size();i++) { ans+=process(nums,target-nums[i]);//选择当前的数 } return ans;//返回答案即可 }
但是非常的遗憾由于有大量重复的计算让这种方法直接超时了:
解决这个问题的方法有两种一种是记忆化搜索,一种是动态规划。下面这两种方法都一一给出:
记忆化搜索:其实特别的简单,我么可以定义一个dp表在返回之前把答案提前记录一下,如果发现答案已经被记录过了我可以直接返回不需要再调用递归重复的计算:
class Solution { public: vector<int>dp; int combinationSum4(vector<int>& nums, int target) { dp.resize(target+1); return Dp(nums,target); } int process(vector<int>&nums,int target) { if(target==0) { return 1; } if(target<0) { return 0; } if(dp[target]!=-1)//说明之前已经计算过了 { return dp[target]; } int ans=0; for(int i=0;i<nums.size();i++) { ans+=process(nums,target-nums[i]); } dp[target]=ans;//提前将答案记录一下 return ans;//返回答案 }
动态规划版本就是再递归的基础上该过来而已:
class Solution { public: vector<int>dp; int combinationSum4(vector<int>& nums, int target) { dp.resize(target+1); return Dp(nums,target); } int process(vector<int>&nums,int target) { if(target==0) { return 1; } if(target<0) { return 0; } if(dp[target]!=-1)//说明之前已经计算过了 { return dp[target]; } int ans=0; for(int i=0;i<nums.size();i++) { ans+=process(nums,target-nums[i]); } dp[target]=ans;//提前将答案记录一下 return ans;//返回答案 } int Dp(vector<int>&nums,int target) { vector<int>dp(target+1); dp[0]=1;//根据递归函数的base填的 for(int i=1;i<=target;i++) { long long ans=0; for(int j=0;j<nums.size();j++)//这一部分直接抄递归函数 { if(i-nums[j]>=0)//组合数小于0 { ans+=dp[i-nums[j]]; } } dp[i]=ans; } return dp[target];//根据递归函数的调用确定返回值 } };
十一.递增子序列
题目描述:
解题思路:
同样的是一个元素选和不选的两种选择但是本题要求的是要是递增所以我们再定义递归函数时需要添加一个参数表示数组中的前一个元素,来判断这个数能不能被选以及在不选这个数时进行去重.以[6,7,7,7]为例
对应代码:
class Solution { public: vector<int>tmp; vector<vector<int>>ans; vector<vector<int>> findSubsequences(vector<int>& nums) { dfs(nums,0,INT_MIN); return ans; } void dfs(vector<int>&nums,int index,int pre) { if(index==nums.size())//达到了答案 { if(tmp.size()>=2)//判断是否满足至少有两个数 { ans.push_back(tmp); } return; } if(nums[index]>=pre)//判断是否满足条件 { tmp.push_back(nums[index]); dfs(nums,index+1,nums[index]); tmp.pop_back(); } if(nums[index]!=pre)//出重 { dfs(nums,index+1,pre); } } };