C++ 总结了回溯问题类型 带你搞懂回溯算法(排列篇)

在上一篇题解中,我总结了回溯算法的三种类型,以及什么时候用回溯算法,怎么写回溯算法,如果没看过的,强烈建议先看:

C++ 总结了回溯问题类型 带你搞懂回溯算法(大量例题)

下面就来讲解第二种类型——排列类型(ABCD四道例题),先上回溯六步走

  • ①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要
  • ②根据题意,确立结束条件
  • ③找准选择列表(与函数参数相关),与第一步紧密关联
  • ④判断是否需要剪枝
  • ⑤作出选择,递归调用,进入下一层
  • ⑥撤销选择

A.全排列–问题描述

给定一个 没有重复 数字的序列,返回其所有可能的全排列。
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

解题步骤
①递归树

在这里插入图片描述
(最下面的叶子节点,红色【】中的就是要求的结果)
然后我们来回想一下,整个问题的思考过程,这棵树是如何画出来的。首先,我们固定1,然后只有2、3可选:如果选2,那就只剩3可选,得出结果[1,2,3];如果选3,那就只剩2可选,得出结果[1,3,2]。再来,如果固定2,那么只有1,3可选:如果选1,那就只剩3,得出结果[2,1,3]…
有没有发现一个规律:如果我们固定了(选择了)某个数,那么他的下一层的选择列表就是——除去这个数以外的其他数!!\比如,第一次选择了2,那么他的下一层的选择列表只有1和3;如果选择了3,那么他的下一层的选择列表只有1和2,那么这个时候就要引入一个used数组来记录使用过的数字,算法签名如下

void backtrack(vector<int>& nums,vector<bool>&used,vector<int>& path)//你也可以把used设置为全局变量
②找结束条件
  if(path.size()==nums.size())
        {
            res.push_back(path);
            return;
        }

③找准选择列表

for(int i=0;i<nums.size();i++)
{
    if(!used[i])//从给定的数中除去用过的,就是当前的选择列表
    {
    }
}

④判断是否需要剪枝

不需要剪枝,或者你可以认为,!used[i]已经是剪枝

⑤做出选择

for(int i=0;i<nums.size();i++)
{
    if(!used[i])//从给定的数中除去用过的,就是当前的选择列表
    {
        path.push_back(nums[i]);//做选择
        used[i]=true;//设置当前数已用
        backtrack(nums,used,path);//进入下一层
    }
}
⑥撤销选择

整体代码如下

void backtrack(vector<int>& nums,vector<bool>&used,vector<int>& path)//used初始化为false
{
    if(path.size()==nums.size())
    {
        res.push_back(path);
        return;
    }
    for(int i=0;i<nums.size();i++)//从给定的数中除去,用过的数,就是当前的选择列表
    {
        if(!used[i])//如果没用过
        {
            path.push_back(nums[i]);//做选择
            used[i]=true;//设置当前数已用
            backtrack(nums,used,path);//进入下一层
            used[i]=false;//撤销选择
            path.pop_back();//撤销选择
        }
    }

}

总结:可以发现“排列”类型问题和“子集、组合”问题不同在于:“排列”问题使用used数组来标识选择列表,而“子集、组合”问题则使用start参数

B.全排列 II(剪枝思想)–问题描述

给定一个可包含重复数字的序列,返回所有不重复的全排列。
输入: [1,2,2]
输出:
[
[1,2,2],
[2,1,2],
[2,2,1]
]

很明显又是一个“重复”问题,在上一篇文章C++ 总结了回溯问题类型 带你搞懂回溯算法(大量例题)的例题B中,当遇到有重复元素求子集时,先对nums数组的元素排序,再用if(i>start&&nums[i]==nums[i-1])来判断是否剪枝,那么在排列问题中又该怎么做呢?

解题步骤
①递归树

依旧要画递归树,判断在哪里剪枝。这个判断不是凭空想出来,而是看树上的重复部分,而归纳出来的
在这里插入图片描述
可以看到,有两组是各自重复的,那么应该剪去哪条分支?首先要弄懂,重复结果是怎么来的,比如最后边的分支,选了第二个2后,,竟然还能选第一个2,从而导致最右边整条分支都是重复的
在这里插入图片描述
②③不再赘述,直接看④

④判断是否需要剪枝,如何编码

有了前面“子集、组合”问题的判重经验,同样首先要对题目中给出的nums数组排序,让重复的元素并列排在一起,在if(i>start&&nums[i]==nums[i-1]),基础上修改为if(i>0&&nums[i]==nums[i-1]&&!used[i-1]),语义为:当i可以选第一个元素之后的元素时(因为如果i=0,即只有一个元素,哪来的重复?有重复即说明起码有两个元素或以上,i>0),然后判断当前元素是否和上一个元素相同?如果相同,再判断上一个元素是否能用?如果三个条件都满足,那么该分支一定是重复的,应该剪去

给出最终代码,记得先对nums排序,再传进来

void backtrack(vector<int>& nums,vector<bool>&used,vector<int>& path)//used初始化为false
{
    if(path.size()==nums.size())
    {
        res.push_back(path);
        return;
    }
    for(int i=0;i<nums.size();i++)//从给定的数中除去,用过的数,就是当前的选择列表
    {
        if(!used[i])
        {
            if(i>0&&nums[i]==nums[i-1]&&!used[i-1])//剪枝
                continue;
            path.push_back(nums[i]);//做选择
            used[i]=true;//设置当前数已用
            backtrack(nums,used,path);//进入下一层
            used[i]=false;//撤销选择
            path.pop_back();//撤销选择
        }
    }
}

C.字符串的全排列–问题描述(剪枝思想)

输入一个字符串,打印出该字符串中字符的所有排列。你可以以任意顺序返回这个字符串数组,但里面不能有重复元素
示例:
输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

解题步骤

其实这题跟例题B一模一样,换汤不换药,把nums数组换成了字符串,直接上最终代码,记得先用sort对字符串s进行排序,再传进来!

//vector<string>res为全局变量,表示最终的结果集,最后要返回的
 void backtrack(string s,string& path,vector<bool>& used)//used数组
    {
        if(path.size()==s.size())
        {
            res.push_back(path);
            return;
        }
        for(int i=0;i<s.size();i++)
        {
            if(!used[i])
            {
                if(i>0&&s[i-1]==s[i]&&!used[i-1])//判重剪枝
                    continue;
                path.push_back(s[i]);
                used[i]=true;
                backtrack(s,path,used);
                used[i]=false;
                path.pop_back();
            }   
        }
    }

再次总结:“排列”类型问题和“子集、组合”问题不同在于:“排列”问题使用used数组来标识选择列表,而“子集、组合”问题则使用start参数。另外还需注意两种问题的判重剪枝!!

  • 5
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值