回溯题目总结1 :力扣P46 P47 P39 P40
前言
今天主要做了回溯的经典例题,特此总结一下。
一、全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
这道题思路比较简单,只需要将所有的分支遍历一遍即可,只需要注意用过的数不可以再用。
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<int> used;
vector<int> path;
vector<vector<int>> res;
for(int i=0;i<nums.size();i++) used.push_back(0);//均未使用
if(nums.size()==0) return res;
dfs(nums,used,path,res,nums.size());
return res;
}
void dfs(vector<int>& nums,vector<int> &used,vector<int> &path,vector<vector<int>> &res,int len)
{
if(path.size()==len) {res.push_back(path);return ;}
for(int i=0;i<nums.size();i++)
{
if(!used[i])//用过的数不可以再使用
{
path.push_back(nums[i]);
used[i]=1;
dfs(nums,used,path,res,len);
used[i]=0;//从一个分支转移到另一个时要把之前用的消除掉
path.pop_back();
}
}
}
};
二、全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
这道题与上一道不同的是,数据重复,但是结果不能重复,同样是全排列。
这里就需要考虑如何剪枝,假如仍然使用上一题的思路,会出现重复。
由于结果和顺序有关,因而不同于组合问题,第一个分支能够将所有包含该分支顶端数的组合全包含。但是假如一个数和前一个数相同,且前一个数未被使用,即在同一层的话,那么这两个数就是等价的,可以将后面的一支删掉。
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<int> used;
vector<int> path;
vector<vector<int>> res;
sort(nums.begin(),nums.end());
for(int i=0;i<nums.size();i++) used.push_back(0);//均未使用
if(nums.size()==0) return res;
dfs(nums,used,path,res,nums.size());
return res;
}
void dfs(vector<int>& nums,vector<int> &used,vector<int> &path,vector<vector<int>> &res,int len)
{
if(path.size()==len) {res.push_back(path);return ;}
for(int i=0;i<nums.size();i++)
{
if(!used[i])
{
if(i>=1&&nums[i]==nums[i-1]&&!used[i-1]) continue;//回溯到同一层时,如果之前用的和目前的数一样,且之前的数未使用,那么状态相同,直接剪枝
else
{
path.push_back(nums[i]);
used[i]=1;
dfs(nums,used,path,res,len);
used[i]=0;
path.pop_back();
}
}
}
}
};
三、组合总和
和全排列不同,这道题里每个数都可以被无限次选取,且结果与顺序无关。所以,我们需要考虑到,假如每次都从0开始遍历,势必出现重复的元素,但是第0个元素其对应的分支,就能够涵盖所有包含第0个元素的组合,同理,第一次遍历到第一个数对应的分支,也能涵盖所有出现了第一个数的组合,因而之后再进行遍历时,遍历的起点应为当前的循环变量i。(为i表示下一层仍可以使用该数,即无限次选取)
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
vector<int> path;
sort(candidates.begin(),candidates.end());
if(candidates.size()==0) return res;
dfs(candidates,target,path,res,0);
sort(res.begin(),res.end());
return res;
}
void dfs(vector<int>& candidates,int value,vector<int> &path,vector<vector<int>> &res,int begin)
{
if(value<0) return;
if(value==0)
{
res.push_back(path);
return ;
}
for(int i=begin;i<candidates.size();i++)
{
if(target-candidates[i]<0) break;
if(value-candidates[i]>=0)
{
path.push_back(candidates[i]);
dfs(candidates,value-candidates[i],path,res,i);
path.pop_back();
}
}
}
};
四、组合总和 II
和上一道题不同,这道题要求每个数只能出现一次,且会出现重复元素。假如仍使用上一道题的方式,那么同一层的重复元素会导致出现重复的结果,只需要保留第一个相同值的分支即可。每个数只出现一次,通过下一层的下标从i+1开始来实现。
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
vector<int> used;
for(int i=0;i<candidates.size();i++) used.push_back(0);
vector<int> path;
vector<vector<int>> res;
if(candidates.size()==0) return res;
dfs(candidates,target,path,res,0,used);
return res;
}
void dfs(vector<int>& candidates, int leftval,vector<int> &path,vector<vector<int>> &res,int begin,vector<int> & used)
{
if(leftval<0) return ;
if(leftval==0) {res.push_back(path); return ;}
for(int i=begin;i<candidates.size();i++)
{
if(i > begin && candidates[i]==candidates[i-1]) continue;//这里加不加前一个数是否被使用都没影响,因为我们是按次序遍历,在这之前前一个分支已经退出了,一定未被使用
if(leftval-candidates[i]<0) break;
if(!used[i])
{
used[i]=1;
path.push_back(candidates[i]);
dfs(candidates,leftval-candidates[i],path,res,i+1,used);//注意这里使用的是i,不是begin,因为剪枝过程中,前一个数已经找到了所有包含其的组合,后一个数不需要再找包含前一个数的组合;从同一个数往下面依次寻找结果时,放在前面的一定找到了所有包含该数的组合,后面的就不能使用该数;used只能用来排除同意分支上同一层的元素,不能排除不同分支同层,而用i+1则可以完全杜绝掉后面的值又使用前面元素的情况。
path.pop_back();
used[i]=0;
}
}
}
};
总结
从以上题目来看,涉及到路径的问题,剪枝的方式往往是对重复的一层操作,具体实现上是used使用以及判断和前一个数是否相等。而对于涉及组合的类型,由于先被遍历到的数,已经涵盖了所有包含该数的种类,后面再出现该数会出现重复,因而实现方式是每次的遍历起点都要增加,从而禁止后面的组合使用前面出现的数字,同时也要注意,需要预先对数组排序,才能进一步剪枝,对同一个数的重复分支,只保留第一个即可。