2020/12/09---刷题打卡之回溯问题

回溯问题(排列、组合)

回溯算法一般可以解决组合、排列、切割、子集、棋盘(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. 全排列

leetcode46

给定一个 没有重复 数字的序列,返回其所有可能的全排列
在这里插入图片描述

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;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值