生成排列(DFS)

模板

void dfs(int x)
{
    if(到达目的地)
    {
        if(解合法) 
        输出解;
        return;
    }
    for(枚举选择个数)
    {
        if(合法)
        {
            保存结果;
            更改状态;
            dfs(下一步);
            回溯;
        }
    }
}

例题

例1

生成k的n维向量(1<=k<=10,1<=n<=6)
n维向量是有n个元素的序对,每数的取值范围从1到k
输入k和n,输出所有k的n维向量
譬如2的3维向量
有{1,1,1}{1,1,2}{1,2,1}{1,2,2}{2,1,1}{2,1,2}{2,2,1}{2,2,2}
譬如3的5维向量
有{1,1,1,1,1}{1,1,1,1,2}{1,1,1,1,3}{1,1,1,2,1}······{3,3,3,3,2}{3,3,3,3,3}
分析
可以把每一次构造的分成n个阶段
一阶段:从k个数中选一个数出来
二阶段:从k个数中选一个数出来
三阶段:从k个数中选一个数出来
·············
n阶段:从k个数中选一个数出来
n个阶段后,就输出解
代码

#include<cstdio>
#define M 5
int ans[M+5],n,k;
void dfs(int x)
{
    if(x>n)//判断是否可以输出解
    {
        for(int i=1;i<n;i++)
            printf("%d ",ans[i]);
        printf("%d\n",ans[n]);
        return;
    }
    for(int i=1;i<=k;i++)
    {
        ans[x]=i;//保存结果
        dfs(x+1);//进入下一阶段
    }
}
int main()
{
    scanf("%d%d",&k,&n);
    dfs(1);
    return 0;
}

例2

如果在例1的基础上,在每个方案前加上序号,又该怎么写?
分析
只用加上计数器就可以了,输出序号后,让序号加1;
代码实现如下

#include<cstdio>
#define M 5
int ans[M+5],n,k,cnt;
void dfs(int x)
{
    if(x>n)
    {
        cnt++;//自加
        printf("%d:",cnt);//输出序号
        for(int i=1;i<n;i++)
            printf("%d ",ans[i]);
        printf("%d\n",ans[n]);
        return;
    }
    for(int i=1;i<=k;i++)
    {
        ans[x]=i;
        dfs(x+1);
    }
}
int main()
{
    scanf("%d%d",&k,&n);
    dfs(1);
    return 0;
}

例3

如果将向量改成1到n的全排列呢?
输入n,输出1到n的全排列
分析
只需加一个check,搜锁即将放的数在之前有没有放过。
代码

#include<cstdio>
#define M 5
int ans[M+5],n,cnt;
bool check(int v,int u)//查找函数
{
    for(int i=1;i<=u;i++)
        if(ans[i]==v)return 0;//如果一样,返回0
    return 1;
}
void dfs(int x)
{
    if(x>n)
    {
        printf("%d:",++cnt);
        for(int i=1;i<n;i++)
            printf("%d ",ans[i]);
        printf("%d\n",ans[n]);
        return;
    }
    for(int i=1;i<=n;i++)
        if(check(i,x))
        {
            ans[x]=i;
            dfs(x+1);
        }
}
int main()
{
    scanf("%d",&n);
    dfs(1);
    return 0;
}

大家可以试试输入7会发生什么,8呢?9呢?
显然程序“变慢”了,那么该如何解决呢
其实,将用过的数标记就可以了,但回溯时一定改回来
代码如下

#include<cstdio>
#define M 5
int ans[M+5],n,cnt;
bool vis[M+5];
void dfs(int x)
{
    if(x>n)
    {
        printf("%d:",++cnt);
        for(int i=1;i<n;i++)
            printf("%d ",ans[i]);
        printf("%d\n",ans[n]);
        return;
    }
    for(int i=1;i<=n;i++)
        if(!vis[i])//如果没使用过
        {
            vis[i]=true;//标记使用过
            ans[x]=i;
            dfs(x+1);
            vis[i]=false;//记得回溯
        }
}
int main()
{
    scanf("%d",&n);
    dfs(1);
    return 0;
}

时间肯定快了,但仍然看不出效果,接下来介绍一种最快的算法。
大家发现,每次只用交换两数的位置就可以生成新排列。
交换时注意交换过的不要再交换。所以从当前开始往后交换。
代码实现如下

#include<cstdio>
#include<algorithm>
using namespace std;
#define M 10
int n,cnt,ans[M+5]={0,1,2,3,4,5,6,7,8,9,10,11,12,13,14};//先把数按从1到n存进去
void dfs(int x)
{
    if(x>n)
    {
        printf("%d:",++cnt);
        for(int i=1;i<n;i++)
            printf("%d ",ans[i]);
        printf("%d\n",ans[n]);
        return;
    }
    for(int i=x;i<=n;i++)//与后面的数交换,前面的数交换过
    {
        swap(ans[x],ans[i]);//交换这两个数
        dfs(x+1);
        swap(ans[x],ans[i]);//一定记得交换回来
    }
}
int main()
{
    scanf("%d",&n);
    dfs(1);
    return 0;
}

这里提一下,这种算法没有按字典序排,如果题目要求字典序输出,就用第二种。
如例4就需要字典序

例4

输入一个包含 n个非负数的数组,元素可以重复。按字典序输出所有全排列方案,要求不重复。
(1<=n<=10)
看起来复杂,一会重复一会不重复的,但跟前面差不多,只需多定义一个变量las,用来储存当层函数上一次填的数,如果一样,显然重复,就继续往后循环,但别忘了更改las的值
代码如下

#include<cstdio>
#define M 10
int ans[M+5],n,cnt,a[M+5];
bool vis[M+5];
void dfs(int x)
{
    if(x>n)
    {
        printf("%d:",++cnt);
        for(int i=1;i<n;i++)
            printf("%d ",ans[i]);
        printf("%d\n",ans[n]);
        return;
    }
    int las=-1;//输入非负数,所以las初值为负数
    for(int i=1;i<=n;i++)//枚举n个数
        if(!vis[i]&&las!=a[i])
        {
            vis[i]=true;
            ans[x]=a[i];
            las=a[i];//更改las的值
            dfs(x+1);
            vis[i]=false;
        }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    dfs(1);
    return 0;
}

另一种方法针对每个数范围小
将数字的种类表示出来,这种方法对空间需求较高
代码如下

#include<cstdio>
#define M 10
#define N 50
int ans[M+5],n,cnt,a[M+5];
int c[N+5];//假设每个数不超过50,存每种数可以使用的次数
void dfs(int x)
{
    if(x>n)
    {
        printf("%d:",++cnt);
        for(int i=1;i<n;i++)
            printf("%d ",ans[i]);
        printf("%d\n",ans[n]);
        return;
    }
    for(int i=0;i<=N;i++)//枚举数的种类
        if(c[i])
        {
            c[i]--;//使用数
            ans[x]=i;
            dfs(x+1);
            c[i]++;//回收数
        }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        c[a[i]]++;//初始化
    }
    dfs(1);
    return 0;
}

例5

STL标准库中的next_permutation()函数可以生成下一个排列,结合循环就可以将从当前排列开始的所有排列生成出来。所以,使用前往往会先排序,且一般把它放到while()循环当中。(需要头文件algorithm)
代码如下

#include<cstdio>
#include<algorithm>
using namespace std;//调用STL函数
#define M 10
int a[M],cnt;
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    sort(a+1,a+1+n);//生成最小排列(从小到大排序)
    do
    {
        printf("%d:",++cnt);
        for(int i=1;i<n;i++)
            printf("%d ",a[i]);
        printf("%d\n",a[n]);
    }while(next_permutation(a+1,a+1+n));
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章