回溯
回溯问题(排列、组合)
回溯算法一般可以解决组合、排列、切割、子集、棋盘(N皇后和解数独)
组合是无序的,排列是有序的。例如[1,2],组合就只有【1,2】,排列有[1,2]和【2,1】
回溯的一般解题步骤:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
排列问题:每层都是从0开始搜索而不是startIndex ,需要used数组记录path里都放了哪些元素。
组合问题:搜索需要startindex,元素可以重复使用,递归时用i,不可以重复使用i+1表示从下一层开始;
子集问题:收集树形结构中树的所有节点的结果。而组合问题、分割问题是收集树形结构中叶子节点的结果,子集问题是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始,可以不用used[]。
46. 全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列
class Solution {
public:
vector<int>path;
vector<vector<int>>res;
void dfs(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]) continue;
used[i]=true;
path.push_back(nums[i]);
dfs(nums,used);
path.pop_back();
used[i]=false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool>used(nums.size(),false);
dfs(nums,used);
return res;
}
};
另一种思路:
class Solution {
public:
vector<vector<int>>res;
void dfs(vector<vector<int>>&res,vector<int>&nums,int index,int len){
if(index==len-1){
res.push_back(nums);
return;
}
for(int i=index;i<len;i++){
swap(nums[i],nums[index]);
dfs(res,nums,index+1,len);
swap(nums[i],nums[index]);
}
}
vector<vector<int>> permute(vector<int>& nums) {
int len=nums.size();
if(len==0) return res;
dfs(res,nums,0,len);
return res;
}
};
47. 全排列 II
leetcode47
题目描述:
常规解法:
强调的是去重一定要对元素经行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
去重最为关键的代码为:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
如果改成 used[i - 1] == true
也是正确的:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
continue;
}
如果要对树层中前一位去重,就用
used[i - 1] == false
,如果要对树枝前一位去重用used[i- 1] == true
。对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
对于树枝去重和树层去重的理解,以[1,1,1]为例:
树层上去重(used[i - 1] == false
),的树形结构如下:
树枝上去重(used[i - 1] == true
):
class Solution {
public:
vector<vector<int>>res;
vector<int>path;
void dfs(vector<int>&nums,vector<bool>&used,int len){
if(path.size()==nums.size()){
res.push_back(path);
return ;
}
for(int i=0;i<len;i++){
//去重关键
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
if(used[i]) continue;
used[i] = true;
path.push_back(nums[i]);
dfs(nums,used,len);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
int len=nums.size();
if(len==0) return res;
sort(nums.begin(),nums.end());
vector<bool>used(len,false);
dfs(nums,used,len);
return res;
}
};
另一种思路:
class Solution {
public:
vector<vector<int>>res;
//这里不能传nums的引用
void dfs(vector<int>nums,int index,int len){
if(index==len-1){
res.push_back(nums);
}
for(int i=index;i<len;i++){
if(i!=index&&nums[i]==nums[index]) continue;
swap(nums[index],nums[i]);
dfs(nums,index+1,len);
//swap(nums[i],nums[index]);错误---不能两次swap();
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
int len=nums.size();
if(len==-1) return res;
sort(nums.begin(),nums.end());
dfs(nums,0,len);
return res;
}
};
1.为什么传引用不行?
答:因为swap了一次没有回溯,传引用下层会影响上层。
2.为什么传拷贝可以只swap一次?
答:因为每一层的任务是在当前位置选一个不同的数。swap一次可以达到目的,设A,B,C三数字,在第一个位置层的三个循环,在swap一次的情况下,ABC,BAC,CAB,这一层的把A,B,C开头都列出来了。
3.为什么传拷贝swap两次不行?
答:两次swap对于该位置以外的重复数无效,因为条件只考虑与该位置不同的数。比如1,2,2,如果swap两次的三个循环是,122, 212, 221,显然2开头出现了两次。如果只swap一次,122,212。1,2各出现一次
字符串排列
题目描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则按字典序打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
class Solution {
public:
void dfs(string str,int index,int len,vector<string>&res){
if(index==len-1){
res.push_back(str);
return ;
}
for(int i=index;i<len;i++){
if(i!=index&&str[i]==str[index])
continue;
swap(str[i],str[index]);
dfs(str,index+1,str.size(),res);
swap(str[i],str[index]);//不要此swap时,下面不需要sort。
}
}
vector<string> Permutation(string str) {
vector<string>res;
if(str.size()==0) return res;
dfs(str,0,str.size(),res);
sort(res.begin(),res.end());
return res;
}
};
注意事项:上述代码中加入第二个swap()回溯之后,一定要加上sort()排序才能是字典序输出
剑指offer32–把数组排成最小数
题目描述-
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
解题思路:
此题也可以对数组中的数字全排列,然后取出最小的即为答案
代码实现
class Solution {
public:
void dfs(vector<int>&num,int index,string &res){
if(index==num.size()-1){
//一次排列的结果
string tmp="";
for(int i=0;i<num.size();i++){
tmp+=to_string(num[i]);
}
res=min(res,tmp);
return ;
}
for(int i=index;i<num.size();i++){
swap(num[i],num[index]);
dfs(num,index+1,res);
swap(num[i],num[index]);
}
}
string PrintMinNumber(vector<int> numbers) {
string res(numbers.size(),'9');
dfs(numbers,0,res);
return res;
}
};
39. 组合总和
leetcode39
题目描述
解题思路:
利用回溯
class Solution {
public:
vector<vector<int>>res;
vector<int>path;
int sum=0;
void dfs(vector<int>&candidates,int index,int len,int sum,int target){
if(sum>target) return ;
if(sum==target){
res.push_back(path);
return ;
}
for(int i=index;i<len;i++){
sum+=candidates[i];
path.push_back(candidates[i]);
dfs(candidates,i,len,sum,target);//可以重复选取时,传入参数i.
path.pop_back();
sum-=candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
int len=candidates.size();
if(len==0) return res;
dfs(candidates,0,len,0,target);
return res;
}
};
40.组合总和II
解题思路:
参考组合II解题思路
这道题目和39.组合总和如下区别:1、本题candidates 中的每个数字在每个组合中只能使用一次。
2、本题数组candidates的元素是有重复的,而39.组合总和是无重复元素的数组candidates
3、最后本题和39.组合总和要求一样,解集不能包含重复的组合。
重要的是去重的逻辑和每个数字只能用一次,重要的都注释在代码里面了
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 dfs(vector<int>&candidates,int index,int sum,int target,int len,vector<bool>&used){
if(sum>target) return;
if(sum == target){
res.push_back(path);
return ;
}
for(int i=index;i<len;i++){
// used[i - 1] == true,说明同一树支candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false)
continue;
used[i]=true;
sum+=candidates[i];
path.push_back(candidates[i]);
dfs(candidates,i+1,sum,target,len,used);//和39组合总和的区别这里是i+1,每个数字在每个组合中只能使用一次
used[i]=false;
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
int len=candidates.size();
vector<bool>used(len,false);
if(len==0) return res;
sort(candidates.begin(),candidates.end());
dfs(candidates,0,0,target,len,used);
return res;
}
};
77.组合
class Solution {
public:
vector<vector<int>>res;
vector<int>path;
void dfs(int n,vector<bool>&used,int k,int index){
if(path.size()==k){
res.push_back(path);
return ;
}
//n-(k-path.size())+为剪枝操作,其中used数组可要可不要
for(int i=index;i<=n-(k-path.size())+1;i++){
if(used[i]) continue;
used[i]=true;
path.push_back(i);
dfs(n,used,k,i+1);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
if(n==0) return res;
vector<bool>used(n+1,false);
dfs(n,used,k,1);//从1开始是因为元素是1,2,3,4
return res;
}
};
78.子集
class Solution {
public:
vector<vector<int>>res;
vector<int>path;
void dfs(vector<int>&nums,int len,int index){
res.push_back(path);
for(int i=index;i<len;i++){
path.push_back(nums[i]);
dfs(nums,len,i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
int len=nums.size();
if(len==0) return res;
dfs(nums,len,0);
return res;
}
};
90.子集2
class Solution {
public:
vector<vector<int>>res;
vector<int>path;
void dfs(vector<int>&nums,int index,int len,vector<bool>&used){
res.push_back(path);
for(int i=index;i<len;i++){
//在树层去重
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
if(used[i]) continue;
path.push_back(nums[i]);
used[i]=true;
dfs(nums,i+1,len,used);
path.pop_back();
used[i]=false;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
int len=nums.size();
if(len==0) return res;
vector<bool>used(len,false);
sort(nums.begin(),nums.end());
dfs(nums,0,len,used);
return res;
}
};
这道题不需要used数组,因为递归的时候下一个startIndex是i+1,而不是0;全排列每次从0开始遍历,为了跳过已入栈的才需要used
子集的和小于某值
牛牛的背包
解题思路:
通过枚举出所有的子集,如果小于背包容量就加1;
#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int res=0;
void dfs(vector<long>&v,int startIndex,int w,long sum){
if(sum>w) return ;
if(sum<=w) res++;
for(int i=startIndex;i<v.size();i++){
sum+=v[i];
dfs(v,i+1,w,sum);
sum-=v[i];
}
}
int main(){
int n;
long w;
cin>>n>>w;
long val;
vector<long>v(n,0);
long sum=0;
for(int i=0;i<n;i++){
cin>>val;
v[i]=val;
sum+=v[i];
}
if(sum<=w){
res=pow(2,n);
}
else {
dfs(v,0,w,0);
}
cout<<res<<endl;
return 0;
}
18 四数之和
四树之和
解题思路:
这里首先想到的是回溯,但是没考虑好剪枝的问题导致超时了,
考虑剪枝:
1、假设要找的四个数都还没确定,现在想要把脚标为i的数字(记为nums[i])加入答案中。为了避免无用功,在加入之前先瞅一眼,如果nums[i] 加上它右边数字的三倍之后大于目标值,说明就算后面所有数字都相等,也不可能在 nums[i] 的右边找到另外三个数加上nums[i] 的和等于目标值。而且如果进行下一轮循环让 i往右移动,由于数组递增,就更不可能找到四个数加起来等于目标值了,所以直接递归返回,而不是进行下一轮循环。
------------------------------------------------
2、依–然假设要找的四个数都还没确定,现在想要把脚标为 i 的数字(记为nums[i])加入答案中。加入之前也要先瞅一眼,如果 nums[i] 加上数组最后一个数字(也就是数组中最大的那个)的三倍之后仍小于目标值,说明就算后面所有数字都相等,都是最大值,也不可能在 nums[i] 的右边找到另外三个数加上 nums[i] 的和等于目标值。但是与上面不同的是,由于数组递增,进行下一轮循环后nums[i] 会变大,整体的和也会变大,这样就有可能找到四个数加起来等于目标值了,所以是进行下一轮循环,而不是递归返回。
3、如果剩余可选的数字数量少于 n,则剪掉(递归返回);
class Solution {
public:
vector<vector<int>>res;
vector<int>path;
void dfs(vector<int>&nums,int sum,int target,int index,vector<bool>&used){
if(sum==target&&path.size()==4){
res.push_back(path);
return;
}
if(path.size()>4) return ;
for(int i=index;i<nums.size();i++){
//剪枝3
if(nums.size()-i<int(4-path.size())) return ;
//剪枝1 ,直接递归返回
if(i<nums.size()-1&&sum+nums[i]+(int)(3-path.size())*nums[i+1]>target) return;
//剪枝2,进入下一层循环
if(i<nums.size()-1&&sum+nums[i]+(int)(3-path.size())*nums[nums.size()-1]<target) continue;
if(used[i]) continue;
//树层上去重
if(i>0&&nums[i-1]==nums[i]&&used[i-1]==false) continue;
sum+=nums[i];
used[i]=true;
path.push_back(nums[i]);
dfs(nums,sum,target,i+1,used);
used[i]=false;
path.pop_back();
sum-=nums[i];
}
}
vector<vector<int>> fourSum(vector<int>& nums, int target) {
int len=nums.size();
if(len<4) return res;
vector<bool>used(len,false);
sort(nums.begin(),nums.end());
dfs(nums,0,target,0,used);
return res;
}
};
15三数之和
解题思路
和四数之和一样考虑回溯,但是卡在测试用例315,超时了
class Solution {
public:
vector<int>path;
vector<vector<int>>res;
void dfs(vector<int>&nums,int sum,int index,vector<bool>&used){
if(sum==0&&path.size()==3){
res.push_back(path);
return ;
}
for(int i=index;i<=nums.size();i++){
if(used[i]) continue;
if(nums.size()-i<int(3-path.size())) return ;
if(i>0&&nums[i-1]==nums[i]&&used[i-1]==false) continue;
if(i<nums.size()-1&&sum+nums[i]+int(2-path.size())*nums[i+1]>0) return ;
if(i<nums.size()-1&&sum+nums[i]+(int)(2-path.size())*nums[nums.size()-1]<0) continue;
sum+=nums[i];
used[i]=true;
path.push_back(nums[i]);
dfs(nums,sum,i+1,used);
path.pop_back();
sum-=nums[i];
used[i]=false;
}
}
vector<vector<int>> threeSum(vector<int>& nums) {
if(nums.size()==0) return res;
vector<bool>used(nums.size(),false);
sort(nums.begin(),nums.end());
dfs(nums,0,0,used);
return res;
}
};
另一种思路:双指针法
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>>res;
if(nums.size()<3) return res;
sort(nums.begin(),nums.end());
for(int i=0;i<nums.size();i++){
if(nums[i]>0) return res;
//去重;如果此数已经被取过。跳过;
if(i>0&&nums[i]==nums[i-1]) continue;
int left=i+1;
int right =nums.size()-1;
//注意这里不不能取等号
while(left<right){
int sum=nums[i]+nums[left]+nums[right];
if(sum==0){
// 找到一个和为零的三元组,添加到结果中,左右指针内缩,继续寻找
res.push_back(vector<int>{nums[i],nums[left],nums[right]});
left++;
right--;
// 去重:第二个数和第三个数也不重复选取
// 例如:[-4,1,1,1,2,3,3,3], i=0, left=1, right=5
//当前的num[left]如果等于num[left-1]让left++;
while(left<right && nums[left]==nums[left-1]) left++;
//当前的num[right]如果等于后面的num[right+1]让right++;
while(left<right && nums[right]==nums[right+1]) right--;
}else if(nums[left]+nums[right]+nums[i]>0){
right--;// 两数之和太大,右指针左移
}else{
left++;// 两数之和太小,左指针右移
}
}
}
return res;
}
};