递归算法的应用

本文详细介绍了递归算法在排列和组合枚举问题中的应用,包括全排列、有重复元素的全排列、组合枚举以及指数型枚举。文章通过实例展示了如何使用递归实现各种枚举操作,并提供了相应的代码实现,帮助读者理解算法的思路和流程。
摘要由CSDN通过智能技术生成

递归算法的应用


递归实现排列型枚举

题目描述

在这里插入图片描述


核心思路

可以用递归算法来求解,也可以用STL中的next_permutation函数

在这里插入图片描述

[

枚举时,有两种情况:

  • 【1】枚举每个位置该放哪个数 这种情况可以保证按照字典序从小到大输出全排列 对于需要按字典序从小大的题目,只能用这种写法了
  • 【2】枚举每个数该放到哪个位置 这种情况并不能保证按照字典序从小到大输出全排列 对于需要按照字典序从小大的题目,就不能用这种写法了

来分析第一种情况:枚举当前位置可以放哪个数

算法流程

  • 因为我们需要枚举每个位置放什么数,因此当所有位置都放好数了,那么我们就走到了递归树的叶子节点,此时将该路径加入方案中。
  • 如果还没有到达叶子结点,那么我们需要枚举选择该位置放哪些数,因为我们每个数都必须用且只能用一次,所以我们利用st[]数组来标记哪些数已经被用过了。枚举每一个数,如果没有用过,即可加入路径并标记,然后递归到下一层,即下一个位置。
  • 递归结束后,我们需要恢复现场,消除刚才的标记,并把刚才放在该位置上的数清空,比如1_ _,我们在第一个位置上放了数字1,但是最初状态是第一个位置并没有放置数字1,因此我们需要还原到最初状态。这样做的目的是因为当前位置上还可以选择放其他数,所以需要回到往下走之前的样子,然后再选择其他路。

在这里插入图片描述

再来看第二种情况:枚举当前的这个数可以放到哪个位置

算法流程

  • 因为我们需要枚举每个数放什么位置,因此把所有数都放到了位置上,我们就走到了递归树的叶子节点,此时将我们的该路径加入方案中。
  • 如果还没有到达叶子结点,那么我们需要枚举当前数可以放在哪个位置,由于每个位置只能放一个数,所以我们利用st[]数组来标记那些位置上已经放好数了。枚举每个位置,如果没有放上任何数,即可在该位置放上数,然后递归到下一层,即继续去放下一个数。
  • 递归结束后,我们需要恢复现场,消除刚才的标记,由于只要当前位置的标记被清空,该位置就可以放数,所以当我们放下一个数时,如果发现该位置没有用过,即可放上去,此时刚好就能覆盖本来填上的数,因此位置上的数并没有必要清空

在这里插入图片描述


代码

写法1:

#include<iostream>
using namespace std;
const int N=10;
int path[N];    //存储全排列的方案
bool st[N]; //标记某个数字是否已经被使用过了
int n;
//u表示当前枚举到第几个位置了
void dfs(int u)
{
    //u=0,表示第1个位置,u=n-1表示第n个位置,从0到n-1一共有n位
    //因此如果u==n,说明当前正在枚举第n+1个位置,那么就说明了前面n位已经枚举完成了
    if(u==n)
    {
        //输出该种合法枚举的答案
        for(int i=0;i<n;i++)
            printf("%d ",path[i]);
        cout <<endl;
        return ;    //回溯
    }
    //每个位置都可以选择1到n中的某个数字填放到该位置上
    for(int i=1;i<=n;i++)
    {
        //如果i这个数字还没有被使用过
        if(!st[i])
        {
            st[i]=true;//标记i这个数字被使用过了
            path[u]=i;  //在第u个位置上放了数字i
            //递归下一个位置
            dfs(u+1);
            //恢复现场
            st[i]=false;//原来i这个数字还没有被使用过
            path[u]=0;//原来第u个位置上放的是数字0
        }
    }
}
int main()
{
    scanf("%d",&n);
    //
    dfs(0);
    return 0;
}

情况一:枚举该位置可以放哪些数

#include<iostream>
#include<vector>
using namespace std;
const int N=10;
int n;
int path[N];     //存储方案
bool st[N];           //用来判断某个数已经被使用了
int nums[N];
//u表示枚举到了哪个位置
void dfs(int u)
{
    //u从0到n-1,一共枚举了n个位置,因此当u等于n时,说明已经在枚举第n+1个位置了
    //说明已经枚举完了前n个位置,那么就输出方案数组path中的这n个位置是数
    if(u==n)
    {
        for(int i=0;i<n;i++)//输出方案
            printf("%d ",path[i]);
        cout <<endl;
        return;
    }
    //遍历nums数组 看看当前u枚举的这个位置可以填nums数组中的哪个数
    for(int i=0;i<n;i++)
    {
        //st[i]=false表示nums数组中下标i所指向的那个数nums[i]还没有被使用
        //注意i是下标 并不是nums数组中的元素
        if(!st[i])
        {
            //当前枚举的第u个位置可以放入nums[i]这个数
            path[u]=nums[i];
            //标记nums数组中下标i所指向的那个数nums[i]已经被使用过了
            st[i]=true;
            //递归继续枚举下一个位置即第u+1个位置可以填nums数组中的哪个数
            dfs(u+1);

            //恢复现场
            path[u]=0;//回溯 原来第u个位置并没有填数nums[i]
            st[i]=false;    //回溯 原来nums[i]这个数并没有被使用过
        }
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        nums[i]=i+1;
    //从u=0也就是第一个位置开始枚举
    dfs(0);
    return 0;
}

情况二:枚举当前这个数可以放到哪个位置上

#include<iostream>
#include<vector>
using namespace std;
const int N=10;
int n;
int nums[N];
bool st[N];     //用来判断某个位置是否已经被使用了
int path[N];    //用来存储方案
//u表示枚举到了nums数组中的某个数的下标 我们就要找到某个空闲位置放置下标u所对应的那个数
void dfs(int u)
{
    //u从0到n-1,一共枚举了n个下标了,也就是枚举完了nums数组中的每个数了
    //当u等于n时,说明nums数组中的n个数都已经被枚举完了  我们已经给枚举数都安排好了应该放置的位置了
    if(u==n)
    {
        for(int i=0;i<n;i++)//输出方案
            printf("%d ",path[i]);
        cout <<endl;
        return;//回溯
    }
    //枚举每个位置 有n个数,这n个数都需要有位置  因此也就是需要有n个位置
    //枚举这n个位置应该放入哪些数
    for(int i=0;i<n;i++)    //i表示位置
    {
        //如果第i个位置是空闲的,还没有放入数  那么就可以考虑这个位置上应该放什么数
        if(!st[i])
        {
            //第i个位置应该放入nums数组中下标u所指向的那个元素nums[u]
            path[i]=nums[u];
            //标记第i个位置已经放入数字了 打上封印 表示这个位置不是空闲的
            st[i]=true;
            //递归判断是数组nums中的下一个元素nums[u+1]应该放到哪个空闲位置
            dfs(u+1);
            //恢复现场
            st[i]=false;//表示第i个位置是空闲的,还没有放入任何数字
            //这里并不需要做path[i]=0,因为我们只是标记"位置"是否放入数字 关心的是位置而不关心这个位置上的内容
            //一旦知道这个位置是空闲的,那么即使这个位置上有内容了,但是我们仍然可以向这个位置中写入数据
            //这样新写入的数据就会覆盖掉之前这个位置上已有的内容
            //其实也可以写path[i]=0了 写不写都无所谓啦
        }
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&nums[i]);
    //从nums数组中的下标0所指向的那个元素开始枚举
    dfs(0);
    return 0;
}

递归实现排列型枚举II

题目来源于LeetCode 47全排列II

题目描述

在这里插入图片描述


核心思路

与上一题不同的是,该题目给的数组里面可能出现相同元素

解法一:枚举当前的这个数该放到哪个位置

如果把两个1当作不同的数,所有的方案就是下图中的叶子节点,但是很不幸,由于相同元素的出现,我们的最终方案也出现了重复。找到重复的情况,通过不同颜色的标记可以看出,对于相同的两个位置,蓝色的1在前或者绿色的1在前,方案只需要计算其中一个。因此我们可以人为地规定绿色的1只能出现在蓝色的1的后面,这样我们就可以避免重复方案了。

为了实现上述过程,我们需要将原数组中的元素进行排序,然后将原数组中元素之间的相对位置作为每一种方案中元素的相对位置

所以我们需要传入参数时添加一个参数start,该参数的意义是在枚举该数可以放哪些位置时,位置的枚举需要从start开始

  • 当我们需要递归到下一层时,如果下一个选择的数和当前的数相等,因为在原数组中下一个数在当前数的后面,因此在放下一个数时也必须放在当前数的后面,因此传入参数start等于当前数所填位置的下一个位置。
  • 当我们需要递归到下一层时,如果下一个选择的数和当前的数不相同,那么就没有限制,位置枚举从0开始。

在这里插入图片描述

代码

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<bool> st;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        path = vector<int> (n);
        st = vector<bool> (n);
        dfs(nums, 0, 0);
        return res;
    }
	//u表示nums数组中的下标     start表示枚举的位置起点
    void dfs(vector<int>& nums, int u, int start)
    {
        //u从0到n-1 一共枚举了n个  当u等于n时,说明已经枚举完了nums数组中的这n个元素
        //说明这n个元素都已经放到了相应的位置
        if (u == nums.size())
        {
            res.push_back(path);
            return;
        }
		//位置是从start开始   这里是枚举位置  下标i表示的是位置
        for (int i = start; i < nums.size(); i ++) //枚举哪个位置可以放该数
        {
            //如果第i个位置还没有放入数字
            if (!st[i]) //没有放数
            {
                //在第i个位置上放入nums数组中下标u所指向的那个元素nums[u]
                path[i] = nums[u];
                //标记第i个位置已经放入数字了  已经被使用了  不是空闲的了
                st[i] = true;
                //继续递归枚举nums数组中下标u+1所对应的元素nums[u+1]该放到哪个位置
                //如果下一个要枚举的数nums[u+1]和当前枚举了的这个数nums[u]是相同的,那么下次枚举的位置起点就是当前这个数所在位置i的下一个位置i+1
                //否则下次枚举的位置起点就是从0开始
                dfs(nums, u + 1, u + 1 < nums.size() && nums[u + 1] == nums[u] ? i + 1 : 0);  //下一个数和当前摆放的数不同,可以选择任意位置;如果相同,必须摆放在该数后面
                //恢复现场
                st[i] = false; //回溯
            }
        }
    }
};

解法二:枚举当前的这个位置可以放入哪些数

以该种枚举顺序,我们可得下图递归搜索树,我们可以看出,出现重复方案的原因是因为,当我们在枚举某一位置时,如果我们将两个1看作是不同元素,那么我们枚举时可以选择把哪个1放在该位置上.但是明显放蓝色的1和绿色的1最终的方案都是一样的,因此我们在枚举每个位置可以放哪些数时,如果下一个选择的数和当前选择的数相同,那么就跳过

在这里插入图片描述

代码

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<bool> st;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        path = vector<int> (n);
        st = vector<bool> (n);
        dfs(nums, 0);
        return res;
    }

    void dfs(vector<int>& nums, int u) //u表示枚举到的位置
    {
        //u从0到n-1,一共枚举了n个位置  因此当u等于n时 也就说明了已经枚举完了这n个位置
        //这n个位置都已经放好数字了
        if (u == nums.size())
        {
            res.push_back(path);
            return;
        }
		//判断nums数组中的哪些数可以放入当前枚举的这个位置u中  i表示的是nums数组的下标
        for (int i = 0; i < nums.size(); i ++) //枚举该位置可以放哪些数
        {
            //st[i]=false表示nums数组中下标为i的所对应的元素nums[i]还没有被使用过
            if (!st[i]) 
            {
                //在当前的这个u位置上放入nums[i]这个数
                path[u] = nums[i];
                st[i] = true;		//标记nums数组中下标i所指向的那个元素nums[i]已经被使用过了
                //递归下一层  即下一个位置该填啥数字
                dfs(nums, u + 1);
                //恢复现场
                st[i] = false;		//最初nums数组中下标i所指向的那个元素nums[i]是没有被使用过的
                path[u]=0;			//最初u这个位置上并没有放入数字nums[i]
                //当要枚举的下一个数nums[i+1]与当前的这个数nums[i]相同 并且下标i+1并没有数组越界  
                //那么就可以跳过
                while (i + 1 < nums.size() && nums[i + 1] == nums[i]) 
                    i ++; //跳过相同的数
            }
        }
    }
};

题目描述

在这里插入图片描述


核心思路

由于这题是要求按字典序输出,因此只能使用:枚举当前的这个位置该放入哪些数 这种方法了


代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=10;
int n;
int path[N];    //存储方案
int nums[N];
bool st[N];     //判断某个数是否已经被使用过了
//u表示枚举位置
void dfs(int u)
{
    //u从0到n-1,一共枚举了n个位置 当u=n时,说明当前已经在枚举第n+1个位置了
    //说明前面的n个位置已经枚举完了
    if(u==n)
    {
        for(int i=0;i<n;i++)//输出方案
            printf("%d ",path[i]);
        cout <<endl;
        return;//回溯
    }
    //枚举nums数组中的数字  看看当前位置u能放入nums数组中的哪些数
    for(int i=0;i<n;i++)
    {
        //st[i]=false 表示nums数组中下标为i的所对应的元素nums[i]还没有被使用过
        if(!st[i])
        {
            //在当前的这个u位置上放入nums[i]这个数
            path[u]=nums[i];
            st[i]=true;     //标记nums数组中下标i所指向的那个元素nums[i]已经被使用过了
            //递归下一层  即下一个位置该填啥数字
            dfs(u+1);

            //恢复现场
            path[u]=0;  //最初u这个位置上并没有放入数字nums[i]
            st[i]=false;    //最初nums数组中下标i所指向的那个元素nums[i]是没有被使用过的
            //当要枚举的下一个数nums[i+1]与当前的这个数nums[i]相同 并且下标i+1并没有数组越界
            //那么就可以跳过
            while(i+1<n&&nums[i+1]==nums[i])
                i++;
        }
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&nums[i]);
    sort(nums,nums+n);  //先排序
    //从u=0即第一个位置开始枚举
    dfs(0);
    return 0;
}

递归实现组合型枚举

题目描述

在这里插入图片描述


核心思路

全排列是需要考虑顺序的,但是组合是不需要考虑顺序的。即对于全排列来说,123、132、213、231、312、321是六种不同的方案,但是对于组合来说,它们就只是相同的一种方案而已(因为都是选出了1,2,3这三个数字)。

既然123、132、213、231、312、321对于组合枚举来说都是相同的,那么我们应该选择哪一个作为结果呢?因为题目要求从小到大输出所有方案,因此我们可以选择123这一组作为方案。

问题:对于组合枚举来说,怎样才能选出正确的方案呢?

我们可以在枚举时,人为地定义一个规则:即组内是按从小到大排序的。比如123,组内就是满足从小到大排序,但是132、213、231、312、321就都不满足组内从小到大排序。因此,在我们在递归时,如果发现下一个位置的数字比上一个位置的数字还要小,那么我们就不选择走这个分支,因为一旦选择这个分支,得到的方案必定是不满足组内从小到大排序的这个要求的。

在这里插入图片描述


代码

//写法1
#include<iostream>
using namespace std;
const int N=30;
int path[N];    //存储组合枚举的方案
int n,m;
//u表示当前正在枚举的位置,start表示当前的这个位置上的数字最小该从啥开始枚举
void dfs(int u,int start)
{
    //选出来的总的数的个数不足m个,提前回溯
    if(u+n-start<m)
        return;
    //当u==m+1(即u>m时),说明从u=1到u=m,枚举填完了这m个位置,那么可以回溯了
    if(u>m)
    {
        for(int i=1;i<=m;i++)//输出方案
        {
            printf("%d ",path[i]);
        }
        cout <<endl;
        return;
    }
    //当前位置上的数字可以从[start,n]中选择。
    for(int i=start;i<=n;i++)
    {
        path[u]=i;  //在u这个位置上放入i这个数字
        //递归到下一个位置  下一个位置上的start是当前这个位置上的数字+1
        dfs(u+1,i+1);
        //恢复现场
        path[u]=0;//原来u这个位置上的数字是0
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    //从第1个位置开始枚举,当前start最小可以选1
    dfs(1,1);
    return 0;
}

//写法2
#include<iostream>
using namespace std;
const int N=30;
int n,m;  
bool st[N]; //用来标记某个数是否被选择了  st[i]=true表示选择了i这个数字
//u是枚举当前的数字 它是数而不是下标  c表示当前选了多少个数
void dfs(int u,int c)
{
    //如果已经选了m个数
    if(c==m)
    {
        for(int i=1;i<=n;i++)
        {
            if(st[i])   //如果i这个数被选择了
                printf("%d ",i);
        }
        cout <<endl;
        return;//回溯
    }
    //说明已经遍历完所有数,但是子集大小不足k
    if(u>n)
        return;
    //st[u]=true表选择了u这个数字
    st[u]=true;
    //继续递归下一个数字u+1  由于选了u这个数字  因此选的个数就是c+1
    dfs(u+1,c+1);
    st[u]=false;//恢复现场

    //表示不选择u这个数字
    dfs(u+1,c);
}
int main()
{
    scanf("%d%d",&n,&m);
    //表示从数1开始枚举,已经使用了0个数字
    dfs(1,0);
    return 0;
}

//写法3
#include<iostream>
#include<vector>
using namespace std;
const int N=30;
int n,m;
vector<int>path;    //用来存储方案
//u是枚举当前的数字 它是数而不是下标  c表示当前选了多少个数
void dfs(int u,int c)
{
    //如果已经选了m个数
    if(c==m)
    {
        for(int i=0;i<path.size();i++)
            printf("%d ",path[i]);
        cout <<endl;
        return;//回溯
    }
    //说明已经遍历完所有数,但是子集大小不足k
    if(u>n)
        return;
    //选了当前枚举的u这个数
    path.push_back(u);
    //继续递归下一个数u+1  选的个数是c+1
    dfs(u+1,c+1);
    path.pop_back();//恢复现场

    //不选择当前枚举的u这个数
    dfs(u+1,c);
}
int main()
{
    scanf("%d%d",&n,&m);
    //表示从数1开始枚举,已经使用了0个数字
    dfs(1,0);
    return 0;
}

以下这段代码可以实现:从n个数中随意选出m个数时的各种方案:

//这段代码可以实现:从n个数中随意选出m个数时的各种方案
#include<iostream>
using namespace std;
int n,m,path[30];
int a[30];
//s枚举位置(是数组的下标)  c表示选了多少个数
void dfs(int s,int c)//按字典序输出n个数选m个数的所有组合
{
    //c从0到m-1,一共选了m个数,因此当c=m时,说明到了m+1个数,那么就保证也已经选出了m个数
    //因此就可以输出这次选择的方案了
    if(c==m)
    {
        for(int i=0;i<m;i++)
            cout<<path[i]<<" ";
        cout<<endl;
        return ;
    }
    cout <<endl;
    for(int i=s;i<n;i++)
    {
        path[c]=a[i];//第c个选的数字是a[i]
        //继续枚举下一个位置i+1 然后看看第c+1个要选的数是啥
        dfs(i+1,c+1);
    }
}
int main()
{
    cin >>n>>m;
    for(int i=0;i<n;i++)
        cin >>a[i];
    dfs(0,0);
    return 0;
}


递归实现指数型枚举

题目描述

在这里插入图片描述


核心思路

解法1

这题与组合枚举不同,组合枚举是从n个数中选出m个数。但是这个题是从n个数中选出任意多个,任意多个并未指明是多少个。

由于题目要求升序排列,因此我们可以这么做:从1~n,依次考虑每个数或者不选。填坑,从填1个坑到填n个坑。

在这里插入图片描述

解法2

对于每一个数,我们只有不选两种选择,因此如果像全排列那样画出递归搜索树,求子集的递归搜索树正好是一棵满二叉树。所有方案刚好是所有叶子结点,因此对于一个大小为 n n n的集合来说,所有子集的个数为 2 n 2^n 2n个。

当我们走到叶子结点时,就把该路径加入方案中。如果还没有走到叶子节点,那么对于枚举的当前数,我们有两种选择,选或不选,做出选择再递归到下一层,同时记得回溯。

代码

写法1:

#include<iostream>
using namespace std;
const int N=16;
int st[N];// 状态,记录每个位置当前的状态:0表示还没考虑,1表示选它,2表示不选它
int n;
//u表示当前正在枚举的数
void dfs(int u)
{
    //如果u==n+1(即u>n),则说明从u=1到u=n,这n个位置上的数字都选出来了。
    if(u>n)
    {
        for(int i=1;i<=n;i++)//输出这n个位置山的数字
        {
            if(st[i]==1)//如果选择了i这个数字,则输出
            {
                printf("%d ",i);
            }
        }
        cout <<endl;
        return ;
    }
    //第一个分支:不选u这个数字
    st[u]=2;
    dfs(u+1);//递归下一个
    st[u]=0;    //恢复现场

    //第二个分支:选择u这个数字
    st[u]=1;
    dfs(u+1);//递归下一个
    st[u]=0;//恢复现场
}
int main()
{
    scanf("%d",&n);
    //从数字1开始枚举
    dfs(1);
    return 0;
}

写法2:这种写法比较好理解一些Orz

#include<iostream>
#include<vector>
using namespace std;
const int N=10;
int n;
vector<int>path;    //用来存储方案
//u表示枚举的数   是数哦 而不是下标
void dfs(int u)
{
    //当u>n时,说明已经枚举完了从1到n这n个数了
    if(u>n)
    {
        for(int i=0;i<path.size();i++)  //输出方案
            printf("%d ",path[i]);
        cout <<endl;
        return;//回溯
    }
    //不选择这个当前枚举的u这个数
    //那么直接递归枚举下一个数即u+1即可
    dfs(u+1);    //不选当前数,递归下一层

    //选择当前枚举的u这个数
    path.push_back(u);      //选当前数
    //继续递归枚举下一个数
    dfs(u+1);           //递归下一层
    //恢复现场
    path.pop_back();         //回溯
}
int main()
{
    scanf("%d",&n);
    //从数字1开始枚举
    dfs(1);
    return 0;
}

//或者这么写
#include<iostream>
#include<vector>
using namespace std;
const int N=10;
int n;
bool st[N];    //用来存储方案
//u表示枚举的数   是数哦 而不是下标
void dfs(int u)
{
    //当u>n时,说明已经枚举完了从1到n这n个数了
    if(u>n)
    {
        for(int i=1;i<=n;i++)
        {
            if(st[i])//如果选择i这个数 则输出
                printf("%d ",i);
        }
        cout <<endl;
        return;//回溯
    }
    //不选择这个当前枚举的u这个数
    //那么直接递归枚举下一个数即u+1即可
    dfs(u+1);    //不选当前数,递归下一层

    //选择当前枚举的u这个数
    st[u]=true;      //选当前数
    //继续递归枚举下一个数
    dfs(u+1);           //递归下一层
    //恢复现场
    st[u]=false;         //回溯
}
int main()
{
    scanf("%d",&n);
    //从数字1开始枚举
    dfs(1);
    return 0;
}

递归实现指数型枚举 II

题目描述

在这里插入图片描述


核心思路

该题与上一题"递归实现指数型枚举"的不同点在于,这题是包含重复元素的。对于这种求子集的问题,它与求全排列不同点在于,求子集问题并不需要考虑每个数在什么位置每个位置上该填什么数,我们只在乎每一个数选或不选。因此对于有重复元素的子集问题,我们的思路不再像全排列那样关心每个数的先后关系,我们只需要关心相同的数选几个。具体步骤如下:

  • 将原数组排序,目的是将相同元素放在一起,之后好计算每个相同数的个数
  • 计算出当前数相同的个数,然后分别做出不选,选1个,选2个…的选择,然后递归到下一层
  • 注意在每做完一个选择递归到下一层时不需要马上回溯,因为我们选1个,选2个…每一个之后的选择是建立在之前选择上多选一个,所以我们不需要马上回溯
  • 当做出所有选择后,我们才需要将选择的这一段相同的数清空,所以回溯需要在做完所有选择后再进行

下面回忆一下"递归实现指数型枚举"中的选和不选的思路 —> 构建的递归搜索树:

在这里插入图片描述

可以发现,叶子结点中是有重复的,说明我们的递归搜索树是错误的!

我们不妨从选取元素的个数出发,画出如下递归搜索树:

在这里插入图片描述

这是完全正确的,说明我们的递归搜索树构成成功!因此,我们只需要关注选取元素的个数就好了,关于元素的选取,分析当前元素的选取时:

  • 如果是选取0个(也就是不选择当前元素),那么我们直接从下一个不同的数开始搜就可以
  • 如果是要选取这个元素的话,那么就涉及到要选择多少个,可以选1个、选2个、 ⋯ \cdots ,因此,可以先排序,如果我们决定要选择该元素,则先统计该元素出现的次数,然后依次dfs看看需要选择多少个当前的这个元素。

在这里插入图片描述


代码

//写法1
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=20;
int a[N];
vector<int>path;
int n;
//u可以理解为位置的下标
void dfs(int u)
{
    //u从0到n-1 一个有n个位置  当u=n时 说明枚举完了这n个位置了
    if(u>=n)
    {
        for(int i=0;i<path.size();i++)//输出方案
            printf("%d ",path[i]);
        cout <<endl;
        return;//回溯
    }
    int k=u;
    //当前枚举到的下标u所对应的元素是a[u]
    //循环判断当前元素的后面有多少个是与a[u]相同的 继续a[u]出现的次数
    while(k<n&&a[k]==a[u])
        k++;
    //注意是dfs(k)而不是dfs(u)  当前这种数一个都不选
    //没有做path.push_back(a[k])而是直接dfs,说明没有选择元素a[k],即枚举选0个的情况
    dfs(k); //选0个 也就是不选择当前枚举到的这个元素a[u] 跳到它的下一个元素开始
    //依次选择同一段内的1个、2个...
    for(int i=u;i<k;i++)
    {
        path.push_back(a[i]);
        dfs(k);
    }
    //回溯,将选择的这一段相同的数全部清空
    for(int i=u;i<k;i++)
        path.pop_back();
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&a[i]);
    sort(a,a+n);    //先排序
    //从下标0开始递归
    dfs(0);
    return 0;
}

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20;
int n;
int a[N];
bool st[N];	//用来标记某个数是否被选择
void dfs(int u)
{
    if (u >= n)
    {
        for (int i = 0; i < n; i ++)
            if (st[i])
                cout << a[i] << ' ';

        cout << endl;
        return;
    }
    int k = u;
    while (k < n && a[k] == a[u]) k ++;
    dfs(k);
    for (int i = u; i < k; i ++)
    {
        st[i] = true;
        dfs(k);
    }
    for (int i = u; i < k; i ++) st[i] = false;
}
int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++) cin >> a[i];
    sort(a, a + n);
    dfs(0);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值