生成排列(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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章