可以看出这个棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不在重复取。 可以发现n相当于树的宽度,k相当于树的深度。图中每次搜索到了叶子节点,我们就找到了一个结果。相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合
递归函数的返回值以及参数
函数里一定有两个参数,既然是集合n里面取k的数,那么n和k是两个int型的参数。
然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。因为每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex。
回溯函数终止条件
path这个数组的大小如果达到k(path.size()==k),说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。 此时将path放进res中,并return
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(int n,int k,int StartIndex){
//回溯函数终止条件
if(path.size()==k){
res.push_back(path);
return;
}
for(int i=StartIndex;i<=n;i++){
path.push_back(i);//处理结果
backtracking(n,k,i+1);// 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop_back();//回溯
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(n,k,1);
return res;
}
};
可以类似于39组合综合,可以重复取数
注意本题和上题全排列的区别,上题中【从左向右取数,取过的数,不在重复取】,但本题中除了总集合不能存在重复字符串,但【之前取过的数,满足一定条件可以重复取】
class Solution {
public:
//全排列问题
vector<string> res;
void dfs(string& s,string& str,vector<bool>& visited){
if(str.size()==s.size()){
res.push_back(str);
return;//找到一个结果之后,要return回去找其他结果
}
for(int i=0;i<s.size();i++){//由于同一层,元素需要从头开始取
if(visited[i]==true) continue;
if(i>0 && s[i]==s[i-1] && visited[i-1]==true){//和前一个一样的元素且前一个已取过值,继续往下
continue;
}
str.push_back(s[i]);
visited[i]=true;
dfs(s,str,visited);//递归。每层的visited传递下去的
str.pop_back();//回溯
visited[i]=false;
}
}
vector<string> permutation(string s) {
sort(s.begin(),s.end());
string str="";
vector<bool> visited(s.size(),false);
dfs(s,str,visited);
return res;
}
};
需要如下参数:
- targetSum(int)目标和,也就是题目中的n。
- k(int)就是题目中要求k个数的集合。
- startIndex(int)为下一层for循环搜索的起始位置。
回溯终止条件
特别注意如果path.size() == k 但sum != targetSum 直接返回
if(path.size()==k){//树的深度达到
if(n==0){//和符合要求
res.push_back(path);
}
return;// 如果path.size() == k 但sum != targetSum 直接返回
}
整体代码
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(int k,int n,int StartIndex){
if(path.size()==k){//树的深度达到
if(n==0){//和符合要求
res.push_back(path);
}
return;// 如果path.size() == k 但sum != targetSum 直接返回
}
for(int i=StartIndex;i<=9;i++){//之前自己写成i<=n,但题目给出的和n可能大于9,不符合题目要求的元素在1~9内
n-=i;
path.push_back(i);
backtracking(k,n,i+1);
n+=i;//回溯
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k,n,1);
return res;
}
};
已选元素总和如果已经大于n了(或者说n已经减到小于0了),那么往后遍历就没有意义了,直接剪掉。那么剪枝的地方一定是在递归终止的地方剪。
另外,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(int k,int n,int StartIndex){
if(n<0) return;//已选的元素总和超过了n。剪枝操作
if(path.size()==k){//树的深度达到
if(n==0){//和符合要求
res.push_back(path);
}
return;// 如果path.size() == k 但sum != targetSum 直接返回
}
for(int i=StartIndex;i<=9-(k-path.size())+1;i++){//之前自己写成i<=n,但题目给出的和n可能大于9,不符合题目要求的元素在1~9内。剪枝操作
n-=i;//处理
path.push_back(i);//处理
backtracking(k,n,i+1);
n+=i;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k,n,1);
return res;
}
};
解决如下三个问题:
- 数字和字母如何映射
- 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
- 输入1 * #按键等等异常情况
数字和字母如何映射
可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射,这里定义一个二维数组
回溯法来解决n个for循环的问题
遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果
确定回溯函数参数
需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来
确定终止条件
终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。然后收集结果,结束本层递归。
class Solution {
public:
//二位数组用来实现数字和字母的映射
const string letterMap[10]={
"",//0
"",//1
"abc",//2
"def",//3
"ghi",//4
"jkl",//5
"mno",//6
"pqrs",//7
"tuv", // 8
"wxyz", // 9
};
vector<string> res;
string s;
//index代表digits中的第index个数字
void backtracking(string& digits,int index){
if(index==digits.size()){//长度符合要求
res.push_back(s);
return;
}
int digit=digits[index]-'0';//将字符转化成数字
string letter=letterMap[digit];//数字对应的字符串
for(int i=0;i<letter.size();i++){
s.push_back(letter[i]);//处理
backtracking(digits,index+1);//递归
s.pop_back();//回溯
}
}
vector<string> letterCombinations(string digits) {
if(digits.size()==0) return res;//字符串digits本身是空的
backtracking(digits,0);
return res;
}
};
图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!
//for循环中,用startIndex控制起始点位置,这是避免2,2,3和3,2,2这样的重复
//下一层递归还是从原来的位置开始,表明可以重复取数
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& candidates, int target,int sum,int startIndex){
if(sum>target) return;
if(sum==target){
res.push_back(path);
return;
}
for(int i=startIndex;i<candidates.size();i++){//for循环中,用startIndex控制起始点位置,这是避免2,2,3和3,2,2这样的重复
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i);//下一层递归还是从原来的位置开始,表明可以重复取数
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates,target,0,0);
return res;
}
};
剪枝优化 (排序+剪枝+回溯)
以上的代码其实对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回 。
其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。那么可以在for循环的搜索范围上做做文章了。
在求和问题中,排序之后加剪枝是常见的套路!!!!(因为排序之后的candicate[i+1]肯定比前一个candicate[i]大,如果candicate[i]这个都不符合要求,后续就可以不做判断了)
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& candidates, int target,int sum,int startIndex){
if(sum==target){
res.push_back(path);
return;
}
for(int i=startIndex;i<candidates.size() && sum+candidates[i]<=target;i++){//for循环中,用startIndex控制起始点位置,这是避免2,2,3和3,2,2这样的重复
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i);//下一层递归还是从原来的位置开始,表明可以重复取数
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());//先排序
backtracking(candidates,target,0,0);
return res;
}
};
- 本题candidates 中的每个数字在每个组合中只能使用一次。
- 本题数组candidates的元素是有重复的,而39.组合总和 (opens new window)是无重复元素的数组candidates
最后本题和39.组合总和 (opens new window)要求一样,解集不能包含重复的组合。
***本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合。
所谓去重,其实就是使用过的元素不能重复选取。
***那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
***如果candidates[i] == candidates[i - 1]
并且 used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& candidates, int target,int sum,int startIndex,vector<bool>& used){
if(sum==target){
res.push_back(path);
return;
}
for(int i=startIndex;i<candidates.size() && sum+candidates[i]<=target;i++){
//要对同一树层使用过的元素进行跳过
if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;//同一树层使用过candidates[i - 1],去重
sum+=candidates[i];//处理
path.push_back(candidates[i]);//处理
used[i]=true;//处理
backtracking(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);//初始化为false
sort(candidates.begin(),candidates.end());
backtracking(candidates,target,0,0,used);
return res;
}
};
本题这涉及到两个关键问题:
- 切割问题,有不同的切割方式
- 判断回文(可利用双指针法判断)
其实切割问题类似组合问题
递归函数参数:全局变量数组path存放切割后回文的子串,二维数组res存放结果集。递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的
在for (int i = startIndex; i < s.size(); i++)
循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。
在循环内,先判断是否是回文串,如果是就截取放进path里面,不是的话就continue跳出
class Solution {
public:
vector<vector<string>> res;
vector<string> path;
void backtracking(string& s,int stratIndex){
//终止条件
if(stratIndex>=s.size()){
res.push_back(path);
return;
}
for(int i=stratIndex;i<s.size();i++){
if(isPalindrome(s,stratIndex,i)){//判断是否是回文串
string str=s.substr(stratIndex,i-stratIndex+1);
path.push_back(str);
}else{
continue;//如果不是回文串,就跳过
}
backtracking(s,i+1);//递归
path.pop_back();
}
}
bool isPalindrome(string& s,int begin,int end){//双指针法判断是否回文
for(int ii=begin,j=end;ii<j;ii++,j--){
if(s[ii]!=s[j]) return false;
}
return true;
}
vector<vector<string>> partition(string s) {
backtracking(s,0);
return res;
}
};
切割问题
参数
startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。
本题我们还需要一个变量pointNum,记录添加逗点的数量
终止条件
本题明确要求只会分成4段,以分割的段数作为终止条件。pointNum表示逗点数量,pointNum为3说明字符串分成了4段了。然后验证一下第四段是否合法,如果合法就加入到结果集里
递归逻辑
需要判断【startIndex,i】区间内子串是否合法。如果合法就在字符串后面加上符号.
表示已经分割。
递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符.
),同时记录分割符的数量pointNum 要 +1。
最后就是在写一个判断段位是否是有效段位了。
主要考虑到如下三点:
- 段位以0为开头的数字不合法
- 段位里有非正整数字符不合法(>9或者<0)
- 段位如果大于255了不合法
class Solution {
public:
vector<string> res;
//string str;
void backtracking(string& s,int startIndex,int pointNum){
if(pointNum==3){//分成了4个部分
if(isValid(s,startIndex,s.size()-1)){//判断第四部分是否合法
res.push_back(s);
}
return;
}
for(int i=startIndex;i<s.size();i++){
if(isValid(s,startIndex,i)){//判断 [startIndex,i] 这个区间的子串是否合法
s.insert(s.begin()+i+1,'.'); // 在i的后面插入一个逗点
pointNum++;
}else{
return;//否则,跳过
}
backtracking(s,i+2,pointNum);//下一层递归,由于插入了逗点,因此下一层递归要从i+2处开始
s.erase(s.begin()+i+1);// 回溯删掉逗点
pointNum--;
}
}
bool isValid(string& s,int begin,int end){
if(begin>end) return false;
if(s[begin]=='0' && begin!=end) return false;//表明是0开头的字段,begin!=end避免了单独是'0'的字段
int num=0;
for(int i=begin;i<=end;i++){
if(s[i]>'9' || s[i]<'0') return false;
num=10*num+(s[i]-'0');
if(num>255) return false;
}
return true;
}
vector<string> restoreIpAddresses(string s) {
backtracking(s,0,0);
return res;
}
};
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
找树的所有节点就不需要剪枝,把所有的path都放进res中
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& nums,int startIndex){
res.push_back(path);//不需要剪枝,所有节点都存放到结果集中去
for(int i=startIndex;i<nums.size();i++){
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};
和 题目40. 组合总和 II类似
给出的数组里有重复元素,但结果集里不能有重复元素。
本题的难点在于:集合(数组candidates)有重复元素,但还不能有重复的组合。
所谓去重,其实就是使用过的元素不能重复选取。
***那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
***如果candidates[i] == candidates[i - 1]
并且 used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums,int startIndex,vector<bool>& used){
res.push_back(path);
for(int i=startIndex;i<nums.size();i++){
if(i>0 && nums[i]==nums[i-1] && used[i-1]==false) continue;
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
path.pop_back();
used[i]=false;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());
backtracking(nums,0,used);
return res;
}
};
本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。所以不能使用之前的去重逻辑!(之前是排序+标记数组达到去重目的)
单层搜索逻辑
同一父节点下的同层上使用过的元素就不能在使用了
习惯写回溯的同学,看到递归函数上面的uset.insert(nums[i]);
,下面却没有对应的pop之类的操作,应该很不习惯吧。这也是需要注意的点,unordered_set<int> uset;
是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums,int startIndex){
if(path.size()>=2){
res.push_back(path);
// 注意这里不要加return,因为要取树上所有长度大于等于2的节点。(也就是话可以往下走)
}
unordered_set<int> used;//用来记录本层节点,看看是否使用过。// 使用set来对本层元素进行去重
for(int i=startIndex;i<nums.size();i++){
if((!path.empty() && nums[i]<path.back()) || used.find(nums[i])!=used.end()) continue;//待放入的元素小于末尾元素,构不成升序。或者在set中存在同一父节点下的重复节点
used.insert(nums[i]);//不存在重复节点,就把nums[i]存入used中
path.push_back(nums[i]);
backtracking(nums,i+1);//开启下一层递归
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};
排列问题的不同:
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。(其实就是同一层可以重复使用,同一树枝不能重复使用,去的是树枝的重)
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums,vector<bool>& used){
if(path.size()==nums.size()){
res.push_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if(used[i]==true) continue;//跳出本层循环,到i++那步
used[i]=true;
path.push_back(nums[i]);
backtracking(nums,used);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(),false);//直接利用used数组去重
backtracking(nums,used);
return res;
}
};
此题和上题全排列的区别在于:此题原数组元素有可能有重复,因此用数组used不仅要去同一层的重,还要去同一树枝的重。
排序+回溯+used数组去重
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums,vector<bool>& used){
if(path.size()==nums.size()){
res.push_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if((i>0 && nums[i]==nums[i-1] && used[i-1]==false) || used[i]==true) continue;//去同一层和同一树枝的重
used[i]=true;//处理
path.push_back(nums[i]);//处理
backtracking(nums,used);//开启下一层递归
used[i]=false;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());//先排序
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return res;
}
};