目录
全排列问题不要被模板限制思路,画出决策树后,模拟思路。
每次分支选择之后继续dfs,然后代码紧接回溯,以保证能够回到原来的状态。
一、全排列


全局变量path记录路径,check用于记录使用过的数字 以便 将未使用数字插入路径,for循环保证全部遍历到,条件是小于numssize,然后for循环里写dfs递归,这样就能实现有多少数字就进行多少次for循环。
class Solution {
vector<vector<int>> ret;
vector<int> path;
bool check[7];
public:
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if(nums.size()==path.size())
{
ret.push_back(path);
return;
}
for(int i=0;i<nums.size();++i)
{
if(check[i]==false)
{
path.push_back(nums[i]);
check[i]=true;
dfs(nums);
//回溯
path.pop_back();
check[i]=false;
}
}
}
};
二、子集

对于子集问题,因为子集不像全排列那样可以按照最终元素个数和原数组元素个数相等就返回。每个元素可以选或者不选,所以子集元素个数不是固定的,具体就是个数0~n都有。按照这个想法,我们可以按照以下两种解法来解决这个问题。
法一:判断选还是不选分层,子集的决策树的话每次分布就是判断要选还是不选,dfs函数设计时多传一个参数表示第几次选择.

注意选完之后记得回溯
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums,0);
return ret;
}
void dfs(vector<int>& nums,int pos)
{
if(pos==nums.size())
{
ret.push_back(path);
return;
}
//选
path.push_back(nums[pos]);
dfs(nums,pos+1);
//回溯
path.pop_back();
//不选
dfs(nums,pos+1);
}
};
法二:根据元素个数分层

i,和pos实现按顺序往后迭代,按元素个数分层,每次dfs开头就push path->ret
实际的push顺序: 空 1 12 123 13 2 23 3
可以发现i和pos的使用实现了按顺序不重复地往后遍历
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums,0);
return ret;
}
void dfs(vector<int>& nums,int pos)
{
ret.push_back(path);
for(int i=pos;i<nums.size();++i)
{
path.push_back(nums[i]);
//注意是i+1
dfs(nums,i+1);
path.pop_back();
}
}
};
三、找出所有子集的异或总和再求和
1863. 找出所有子集的异或总和再求和 - 力扣(LeetCode)


找出所有子集,在最终返回的时候计算异或和即可
法一:按照选或者不选
class Solution {
int ret;
vector<int> path;
public:
int subsetXORSum(vector<int>& nums) {
dfs(nums,0);
return ret;
}
void dfs(vector<int>&nums,int pos)
{
int tmp=0;
//完成子集筛选,计算结果
if(pos==nums.size())
{
for(auto&x:path)tmp^=x;
ret+=tmp;
return;
}
//选
path.push_back(nums[pos]);
dfs(nums,pos+1);
//回溯
path.pop_back();
//不选
dfs(nums,pos+1);
}
};
法二:分层,因为每一步都得到了子集,不同于法一在最终的时候才能得到子集,所以path设置成int类型就行。
class Solution {
int path;
int ret;
public:
int subsetXORSum(vector<int>& nums) {
dfs(nums,0);
return ret;
}
void dfs(vector<int>& nums,int pos)
{
ret+=path;
for(int i=pos;i<nums.size();++i)
{
path^=nums[i];
dfs(nums,i+1);
path^=nums[i]; //回溯
}
}
};
四*、全排列II

画出决策树进行模拟很重要,去重->设置一个check数组判重?错误的,check用于检查该数是否使用,去重我们可以利用排序。
画出决策树后暴力模拟,发现同一节点的所有分支中,相同元素只能选择一次,但在下一个节点后这个元素仍然可以选择
具体思路如下:
当数组有相同元素时,之前的代码实现全排列会导致出现重复的子数组。
先将整个数组进行排序,模拟去除重复子数组的全排列,一共有nums.size()这么多个格子,从前往后填格子,在填同一个格子时,从数组元素里面选数,那么就有nums。size个分支,怎么选的就分成两类思考,一种是已经选过的不选,这个多设计一个bool数组就能实现;另一种就是前边分支的元素相同的(所以要提前排序),不用选,因为选了结果都一样会重复,筛选出分支(剪枝)又另外分两种思路:

1.只关心“不合法”的分支,
check[i]==true(结论2.)||
(
i!=0 (因为后边可能会越界,提前判断)
&&
nums[i]==nums[i-1] (同一分支重复,但不同格子时,也就是在填时间上 下一个格子的时候,元素相同是允许的,所以我们还得加一个判断条件)&&
check[i-1]==false(和前一个条件结合后达成真正的同一分支(填同一个格子)重复,如果check[i-1]是true代表i-1填的是之前的格子,是上一次分支的选择,和我们这次不相干)
)(结论1.)
由此,不合法分支判断结束
2.也可以只关心合法的分支
check[i]==false&&
(
i==0(i=0,还在填第一个格子,填啥都可以,不用判断越界)
||
nums[i]!=nums[i-1](和前面分支不相等,当然可以当作填格的一种可能)
||
check[i-1]==true(由于这里运算同级,于是代码从左往右运行,运行到这里已经默认了【i】==【i-1】,如果前面一个check为真,那么代表不同格的同一元素,这是允许的。
)
结合代码体会思路
法一:只关心不合法,不合法直接剪枝
class Solution {
vector<int> path;
vector<vector<int>> ret;
bool check[9];
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums,0);
return ret;
}
//pos表示填格位数
void dfs(vector<int>& nums,int pos)
{
if(nums.size()==pos)
{
ret.push_back(path);
return;
}
for(int i=0;i<nums.size();++i)
{
if(check[i]==true||(i!=0&&nums[i]==nums[i-1]&&check[i-1]==false))
continue;
path.push_back(nums[i]);
check[i]=true;
dfs(nums,pos+1);
path.pop_back();
check[i]=false;
}
}
};
法二:只关心合法的
class Solution {
vector<int> path;
vector<vector<int>> ret;
bool check[9];
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums,0);
return ret;
}
//pos表示填格位数
void dfs(vector<int>& nums,int pos)
{
if(nums.size()==pos)
{
ret.push_back(path);
return;
}
for(int i=0;i<nums.size();++i)
{
if(check[i]==false)
{
if(i==0||nums[i]!=nums[i-1]||check[i-1])
{
path.push_back(nums[i]);
check[i]=true;
dfs(nums,pos+1);
path.pop_back();
check[i]=false;
}
}
}
}
};
五、电话号码的字母组合


思路如下:
建立映射关系然后遍历,+=后进入dfs进入下一个字符串的遍历,完事pop回溯
这里映射关系可以用string【】代替哈希表
代码实现如下:
string【】代替哈希表,找字符串的时候取digits【pos】-‘0’就拿到下标了。
在pos==digits.size的时候push path->ret
class Solution {
string hash[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
vector<string> ret;
string path;
public:
vector<string> letterCombinations(string digits) {
if(digits=="")return ret;
dfs(digits,0);
return ret;
}
void dfs(string& digits,int pos)
{
if(pos==digits.size())
{
ret.push_back(path);
return;
}
for(auto&ch:hash[digits[pos]-'0'])
{
path+=ch;
dfs(digits,pos+1);
path.pop_back();
}
}
};
直接用map的做法
class Solution {
vector<string> ret;
string path;
public:
vector<string> letterCombinations(string digits) {
if(digits=="")return ret;
unordered_map<char,string>table{
{'2',"abc"},{'3',"def"},{'4',"ghi"},
{'5',"jkl"},{'6',"mno"},{'7',"pqrs"},
{'8',"tuv"},{'9',"wxyz"}
};
dfs(table,digits,0);
return ret;
}
//table得到要遍历的字符串,digits提供key值还有结束判断,pos表示填第几个位置
void dfs(unordered_map<char,string>&table,string&digits,int pos)
{
if(pos==digits.size())
{
ret.push_back(path);
return;
}
string tmp=table[digits[pos]];
for(auto&ch:tmp)
{
path+=ch;
dfs(table,digits,pos+1);
path.pop_back();
}
}
};
六*、括号生成

思考许久是不是只要最左边是左括号,最右边是右括号,里边不管咋样都行。不过思考无果。。。

判断括号组合是否有效只需判断数量上:左==右,且从头开始的任意子串,左括号数量>=右括号数量,否则一定会有一个右边括号匹配不上。
于是,最开始的思路是可行的,那么决策树就是一颗从最开始选择“("最终选择“)”,中间分叉有左有右的一棵树。
这里发现因为第一条原则,中间并不是随意的分叉,比如当左括号数量达到n时,后续的分叉就不能再选择左括号了。
那么在添加路径的时候就要判断左括号,右括号是否合法,如果合法,那么就要进行对应的dfs,然后回溯,
剪枝:left>=n,right>=left剪掉!
递归结束条件为right==n。完美的思路
代码实现如下:
class Solution {
vector<string> ret;
string path;
public:
vector<string> generateParenthesis(int n) {
dfs(0,0,n);
return ret;
}
void dfs(int left,int right,int n)
{
if(right==n)
{
ret.push_back(path);
return;
}
//有效时就添加
if(left<n)
{
path+='(';
dfs(left+1,right,n);
path.pop_back();
}
//right>=left的时候一定不添加),否则一定会有右括号匹配不上
if(right<left)
{
path+=')';
dfs(left,right+1,n);
path.pop_back();
}
}
};
七、组合

该题目相当于找元素数量为k的子集
找子集就得避免重复,那么就要多创建一个pos,变量,for循环每次从pos位置往后遍历,dfs下一层时给pos位置传i+1.
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> combine(int n, int k) {
dfs(n, k, 1);
return ret;
}
void dfs(int n, int k, int pos) {
//当k的值为numssize的时候可以用pos,因为这样可以保证全部都遍历到
//这里判断size而不是pos,不然数字k之后的根本遍历不到就结束了
if (path.size() == k) {
ret.push_back(path);
return;
}
for (int i = pos; i <= n; ++i) {
path.push_back(i);
dfs(n, k, i + 1);
path.pop_back();
}
}
};
八、目标和


该题可以联系到子集题目中,找子集的思路一,选还是不选,这里就转化成+还是-。
class Solution {
int sum,ret,aim;
public:
int findTargetSumWays(vector<int>& nums, int target) {
//target传给全局变量,这样每次函数调用传参的时候就能少传一个参数
aim=target;
dfs(nums,0,0);
return ret;
}
void dfs(vector<int>& nums,int sum,int pos)
{
//到达终点,才判断
if(pos==nums.size())
{
if(sum==aim)++ret;
return;
}
//+
dfs(nums,sum+nums[pos],pos+1);
//-
dfs(nums,sum-nums[pos],pos+1);
}
};
九、组合总和


解法一:暴力
依次遍历数组,每次都选数组里的一个数,相当于嵌套for循环暴搜
该题数组元素可以重复选取,这只需要for循环遍历整个数组就行,


画出决策树后发现细节如下:
虽然是for循环遍历整个数组,但是你选择2 3 和选择3 2是算作一种结果的,那么我们就要多传pos,每次for循环起点是这个pos,从该位置开始往后遍历,又因为可以重复选取,dfs下一层时,不是从i+1位置开始往后,而是从当前位置也就是i开始往后;
当sum>target时,后边就不用再遍历了。
class Solution {
vector<vector<int>> ret;
vector<int> path;
int aim;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
aim=target;
dfs(candidates,0,0);
return ret;
}
void dfs(vector<int>& candidates,int pos,int sum)
{
if(sum==aim)
{
ret.push_back(path);return;
}
if(sum>aim||pos==candidates.size())return;
for(int i=pos;i<candidates.size();++i)
{
path.push_back(candidates[i]);
dfs(candidates,i,sum+candidates[i]);
path.pop_back();
}
}
};
解法二:
对于数组的每一个元素,我们都设置选法为选几次,最大次数取决于选多少次刚好不大于这个target,看中途会不会有sum==target,大于target直接返回
dfs每层操作是在对数组的某个元素的进行选择
注意:回溯得我走完该层全部枚举后才开始回溯,因为我们每层考虑的是选择的次数,不是是否选择这个数。

class Solution {
vector<vector<int>> ret;
vector<int> path;
int aim;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
aim=target;
dfs(candidates,0,0);
return ret;
}
void dfs(vector<int>& candidates,int pos,int sum)
{
if(sum==aim)
{
ret.push_back(path);return;
}
if(sum>aim||pos==candidates.size())return;
//枚举次数
for(int k=0;sum+k*candidates[pos]<=aim;++k)
{
if(k)path.push_back(candidates[pos]);
dfs(candidates,pos+1,sum+k*candidates[pos]);
}
//回溯,从1开始是得选了才能删
for(int k=1;sum+k*candidates[pos]<=aim;++k)
{
path.pop_back();
}
}
};
十、组合总和II


该题与No.15 全排列II 相同,因为存在重复元素,按常规方法pos位置往后搜的思路会导致相同数字以不同顺序作为path传进ret。
解法一:每层都是遍历整个数组,因为避免重复,我们要从pos位置往后遍历,同时开始时对数组进行排序,然后再创建一个check数字检查,避免重复的组合。
这里如果不从pos开始往后遍历就是全排列II。
class Solution {
vector<vector<int>> ret;
vector<int> path;
int aim,sum;
bool check[101];
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
aim=target;
sort(candidates.begin(),candidates.end());
dfs(candidates,0,0);
return ret;
}
void dfs(vector<int>& candidates,int pos,int sum)
{
if(sum==aim)
{
ret.push_back(path);return;
}
if(sum>aim||pos==candidates.size())return;
for(int i=pos;i<candidates.size();++i)
{
if(check[i]==false)
{
if(i==0||candidates[i]!=candidates[i-1]||check[i-1])
{
path.push_back(candidates[i]);
check[i]=true;
dfs(candidates,i+1,sum+candidates[i]);
path.pop_back();
check[i]=false;
}
}
}
}
};
十一、组合总和III


该题与No.18 找元素个数为k的子集 思路相似,好吧其实可以说是一模一样,不过这题可以帮忙复习怎么判断 怎么处理指定个数组合 的结束条件
class Solution {
vector<vector<int>> ret;
vector<int> path;
int target,sum;
public:
vector<vector<int>> combinationSum3(int k, int n) {
target=n;
dfs(0,k,1);
return ret;
}
void dfs(int sum,int k,int pos)
{
if(path.size()==k)
{
if(sum==target)
{
ret.push_back(path);return;
}
}
for(int i=pos;i<=9;++i)
{
path.push_back(i);
dfs(sum+i,k,i+1);
path.pop_back();
}
}
};
十二、组合总和IV


原始dfs必定会导致超时,这里引入记忆化搜索,也就是创建一个数组,【index】储存target为index时的方法数,从而起到空间换时间的作用,为了得到最终数据,我们采用从顶向下的思路,将memo初始化(target+1,-1)
memo[0]起点设置为1,
递归终止条件就是target
当memo[target]!=-1时,返回memo[target]
然后我们遍历数组,记录当下target的memo【】值,初始int res=0;res+=所有target-数组元素的结果,最后更新memo【target】返回res即可,
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> memo(target+1,-1);
memo[0]=1; //target为0只有空数组
return dfs(nums,target,memo);
}
int dfs(vector<int>& nums,int target,vector<int>& memo)
{
if(target<0)return 0;
if(memo[target]!=-1)return memo[target];
int res=0;
for(int&x:nums)
{
res+=dfs(nums,target-x,memo);
}
//更新缓存数据
memo[target]=res;
return res;
}
};
总结排列、子集、总和
sort+i,pos互传 解决无序性问题
sort+check 解决含有重复元素求组合,避免相同元素在不同时间加入路径得到相同组合 问题
固定大小子集结束条件
无重复元素,组合总和,两个思路,1.暴搜,有去重->i,pos 2.枚举元素选几个,在枚举结束才进行回溯
含有重复元素的组合总和,会导致相同的元素在不同时间内进入运算,得出相同的结果,也就是导致了重复的组合,解决方法和全排列II一样,重复元素,找组别,check特判
前言
解决此类问题不能被模板固定,画出决策树,模拟实现过程代码就好写了!
全排列问题细节:
刚需check数组->判断元素以不同顺序进入路径
无重复元素全排列
遍历整个数组,创建一个bool数组,进入路径的元素设为真,退出dfs后立马回溯。
有重复元素全排列
因为重复元素的问题,相同元素会在不同时间内被添加进路径,导致相同的排列结果出现,为了方便去重,我们首先要将数组进行排序。因为涉及全排列,check数组也是需要的,不过我们需要特殊判断

1.只关心“不合法”的分支,
check[i]==true(结论2.)||
(
i!=0 (因为后边可能会越界,提前判断)
&&
nums[i]==nums[i-1] (同一分支重复,但不同格子时,也就是在填时间上 下一个格子的时候,元素相同是允许的,所以我们还得加一个判断条件)&&
check[i-1]==false(和前一个条件结合后达成真正的同一分支(填同一个格子)重复,如果check[i-1]是true代表i-1填的是之前的格子,是上一次分支的选择,和我们这次不相干)
)(结论1.)
由此,不合法分支判断结束
2.也可以只关心合法的分支
check[i]==false&&
(
i==0(i=0,还在填第一个格子,填啥都可以,不用判断越界)
||
nums[i]!=nums[i-1](和前面分支不相等,当然可以当作填格的一种可能)
||
check[i-1]==true(由于这里运算同级,于是代码从左往右运行,运行到这里已经默认了【i】==【i-1】,如果前面一个check为真,那么代表不同格的同一元素,这是允许的。
)
子集问题细节:
集合不具有顺序性,比如 2 1 3 和 1 2 3 是一个集合
求全部子集
求全部子集问题思路分为两种:
1.按顺序遍历每个元素,每次分支选择该元素进不进入路径,pos==nums.size()结束,将路径加入结果。
2.按元素个数分层,第一层dfs只加入元素个数为1的路径,以此类推,
因为数据已经排序好了,利用i,pos互传可以避免与集合的无序性冲突
每次dfs开始就先将路径加入结果,然后for循环i从pos位置开始往后遍历
即可。
找固定大小子集
递归结束条件应该是path.size()==指定大小,按题意操作后退出,不是pos==指定大小,此时pos不再可以充当边界判断了。
组合总和问题细节:
组合其实也是集合,也有无序性,那么也需要sort + i,pos互传
组合问题也有两个思路:
1.类似暴搜,sort后i,pos互传 为了无序性
2.判断每个元素选几次,先不选直接dfs到下一层,然后for循环依次枚举选几次,循环每次运行时都添加一次该元素进入路径,for循环退出条件sum+i*【】<=target,然后依次dfs到下一层,在枚举完所有情况后才回溯,for(int k=1;sum+k*【】<=target,++k){pop path;}
无重复元素组合总和
按以上两种思路处理即可
有重复元素组合总和
和以上两种思路一致,不过每种思路都要多加一步
思路1多加check特判
check保证相同的元素如果不在同一分支的话允许添加,特判剪枝掉了重复的情况
思路2多加一个pair类型的数组
思路2每层都在判断【pos】选择几次,这个几次要选rest/【pos】,与【pos】个数的较小值,为了得到这个较小值我们要多创建一个pair类型的数组,key是【pos】,value是次数。
有元素可以无限选择的组合总和
原始dfs必定会导致超时,引入记忆化搜索,也就是创建一个数组,空间换取时间,【index】储存target为index时的方法数,从而起到空间换时间的作用,为了得到最终数据,我们采用从顶向下的思路,将memo初始化(target+1,-1)
memo[0]起点设置为1,
递归终止条件就是target
当memo[target]!=-1时,返回memo[target]
然后我们遍历数组,记录当下target的memo【】值,初始int res=0;res+=所有target-数组元素的结果,最后更新memo【target】返回res即可。
此篇完,感谢浏览。
4417



被折叠的 条评论
为什么被折叠?



