1. 程式人生 > >DFS——組合與排列

DFS——組合與排列

引子

1.關於深搜:深度優先搜尋是一種解決問題的演算法策略。通常,首先它把問題解決過程分解成若干個階段,然後遞迴地搜尋(列舉)每個階段所有可能的選項,得到組合式的解,到達邊界後,檢驗解的合法性。
2.學習了那麼久的深搜,再回頭看一下,就是一串格子,按照題目的要求去填空,其本質就是求組合與排列
3.演算法框架:

void dfs(int i)
{
    if(滿足邊界條件)
    {
        輸出解
        return;
    }
    for(可選擇的選擇j)
        if(沒有訪問過j&&其它條件)
        {
            標記j已經訪問過
            儲存
            dfs(i+1
); 取消標記//回溯 } }

正題

排列

生成n維向量vector

n維向量是有n個元素的序對,每個元素的取值範圍從1到k。例如3的5維向量為{1,1,1,1,1},{1,1,1,1,2},….,{3,3,3,3,3}。輸入k和n,輸出所有k的n維向量。  、
限制條件 :1<= k <=10, 1<=n<=6

分析

簡單的一個搜尋,直接用框架解決,而且元素可以重複,不用標記

#include<cstdio>
#define MAXN 6
int n,k,a[MAXN+5];
void dfs(int
i) { if(i>n) { for(int j=1;j<n;j++) printf("%d ",a[j]); printf("%d\n",a[n]); return ; } for(int j=1;j<=k;j++) { a[i]=j; dfs(i+1); } } int main() { scanf("%d %d",&n,&k); dfs(1); }
思考
  1. 這道題如果要輸出序號,可以增加一個變數tot,like this:
#include<cstdio>
#define MAXN 6
int n,k,a[MAXN+5],tot;
void dfs(int i)
{
    if(i>n)
    {
        tot++;
        for(int j=1;j<n;j++)
            printf("%d:%d ",tot,a[j]);
        printf("%d\n",a[n]);
        return ;
    }
    for(int j=1;j<=k;j++)
    {
        a[i]=j;
        dfs(i+1);
    }
}
int main()
{
    scanf("%d %d",&n,&k);
    dfs(1);
}

2.k的n維向量的總方案數是多少?

對於每一個位置i,都有k個選擇,一共n個位置,所以方案數應是k^n

全排列

輸入n,輸出數字1..n的所有排列。這裡不是要計算排列有多少種,而是列舉所 有的排列,以字典順序列舉。 
限制條件 1<=n<=10

分析

與第一題類似,要標記判重
也可以在儲存的答案中查詢一遍有無使用選項j,但此方法明顯慢得多

#include<cstdio>
#define MAXN 10
int ans[MAXN+5],n;
bool vis[MAXN+5];
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<=n;i++)
        if(!vis[i])
        {
            vis[i]=1;
            ans[x]=i;
            dfs(x+1);
            vis[i]=0;
        }
}

int main()
{
    scanf("%d",&n);
    dfs(1);
}

還有一個方法:交換法
初始:將ans陣列賦成1,2,…,n
遞迴引數x:每次將i從x列舉到n
交換ans[x]和ans[i]
遞迴x+1
換回ans[x]和ans[i]

#include<cstdio>
#include<iostream>
using namespace std;
# define MAXN 100
int ans[MAXN+5];
int n;
void dfs(int x)
{
    if(x==n)
    {
        for(int i=0;i<n;i++)
            printf("%d ",ans[i]);
        puts(" ");
        return ;
    }
    for(int i=x;i<n;i++)
{
        swap(ans[i],ans[x]);
        dfs(x+1);
        swap(ans[i],ans[x]);
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        ans[i]=i+1;
    dfs(0);
    return 0;
}

生成下一個排列:next_permutation

STL 的next_permutation()提供了便捷的列舉排列的方法。它從字典序最小的排 列開始,呼叫一次,產生下一個排列。 
遵從STL演算法庫的慣例,next_permutation(begin, end)接受兩個迭代器引數,
輸入和結果均在迭代器所指容器(通常是vector或陣列)。 
當能夠產生一個按字典序的新排列時,next_permutation()返回true,否則返
回false。可以利用返回值,在一個迴圈中,生成所有排列。 
呼叫一次next_permutation()的時間複雜度為:O(n),大約是從當前排列到下 一個排列需要呼叫交換函式swap()的次數。 
另一個成對的函式是prev_permutation(),它生成上一個排列。

舉個栗子:

生成可重集的全排列

輸入一個包含n個整數的陣列,元素可以重複。按字典序輸出所有全排列,方案不重複。 
例如{1,2,2} 所有的排列就是{1,2, 2}、{2, 1, 2} 、 {2, 2, 1} 。 
限制條件 1<=n<=10 

分析

如果還像之前那樣進行標記的話,由於有重複的元素,所以可能會造成重複(標記下標)或缺少元素(標記值),所以要進行去重。那我們就要思考在什麼情況下是重複的。如果當前數字與上一次這個位置的數字的值是相同的,那麼排列看起來沒有區別,所以我們可以用一個變數last來記錄上一次這個位置出現的值,進行判斷。在做這個方法時要注意先排序,其目的是把相同元素排在一起,否則last會失去作用,因為last僅僅記錄的是上一次的值。

#include<cstdio>
#define MAXN 20
using namespace std;
int a[MAXN+5],ans[MAXN+5],n,last;
bool vis[MAXN+5];
void dfs(int i)
{
    if(i>n)
    {
        for(int j=1;j<n;j++)
            printf("%d ",ans[j]);
        printf("%d\n",ans[n]);
    }
    last=-1;
    for(int j=1;j<=n;j++)
        if(!vis[j]&&a[j]!=last)
        {
            ans[i]=a[j];
            vis[j]=1;
            last=a[j];
            dfs(i+1);
            vis[j]=0;
        }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    dfs(1);
}

第二種方法是改進一下vis[],用一個cnt陣列來記錄這個數字有多少個,用去一個就–,如果cnt[i]為0,表示i已經用完了。

#include<cstdio>
#define MAXN 10
#define MAXVAL 30
int n;
int ans[MAXN+5];
int cnt[MAXVAL+5];
void dfs(int i)
{
    if(i>n)
    {
        for(int j=1;j<n;j++)
            printf("%d ",ans[j]); 
        printf("%d\n",ans[n]);
        return;
    }
    for(int j=1;j<=MAXVAL;j++)
        if(cnt[j])
        {
            cnt[j]--;
            ans[i]=j;
            dfs(i+1);
            cnt[j]++;
        }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int t;
        scanf("%d",&t);
        cnt[t]++;
    }
    dfs(1);
}

第三種方法,理解為交換法中如果交換的兩個數字是相同的,則沒有區別

Part:組合

列舉組合Combination

列舉組合就是生成n個元素的各種組合方式。本質上說,就是列舉子集。
例如{1,2,3} 所有的組合就是{} 、 {1} 、 {2} 、 {3} 、 {1,2} 、 {1,3} 、 {2,3} 、 {1,2, 3},一共有8 個組合

位向量法

計算組合個數的方法
1 可取可不取,有兩種情形、 2 可取可不取,有兩種情形、 3 可取可不取,有兩種情形。根據 乘法原理,總共2×2×2 = 2^3 種情形。
用程式實現時,模擬這個過程。設立標記陣列vis[],vis[i]=true,表示集合中包含第i個元素。在 DFS中依次考慮每個元素,取還是不取,把決策資訊記錄在vis[]中。到達邊界後,掃描vis[],輸 出一組解。
演算法思想是:依序列舉每個位置。針對每個位置,試著填入取或不取

實現

#include MAXN 10
bool vis[MAXN+5];
int A[MAXN+5];
int n;
void dfs(int i)
{
    if(i>=n)
    {
        for(int j=0;j<n;j++)
            if(vis[j]) printf("%d ",A[j]);
        puts("");
        return ;
    }
    vis[i]=0;
    dfs(i+1);

    vis[i]=1;
    dfs(i+1);
    vis[i]=0;
}
增量法(能實現字典序)

思路是往子集裡不斷放入新元素。每次遞迴進入後,當前子集都是一個合法解, 先輸出解。再考慮試著往子集裡新增一個元素。子集裡的元素應該升序生成,避免{1,2},{2,1} 這種重複,故設立變數i指示新增元素的最小值。
增量法生成的組合是按字典序排列的。
實現

#define MAXN 10
int S[MAXN+5];
int n;
void dfs(int i,int sz)
//i:下一次放入子集的最小值  sz:當前子集的大小
{
    for(int j=0;j<sz;j++)
        printf("%d ",S[j]);
    puts("");
    for(int j=i;j<=n;j++)
    {
        S[sz]=j;
        dfs(j+1,sz+1);
    }
}

思考
把列舉子集中的元素看成是下標,就可以輸出元素值為任意型別的組合。
輸入任意型別的元素,存放在陣列A中。先排序。
再把輸出子集的語句修改成輸出特定元素:

for(int j = 0; j < sz; j++) 
      printf("%d ", A[S[j]]);
二進位制(位運算)法

把十進位制數0~15寫成二進位制形式:
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
1010
1011
1100
1101
1110
1111
把數位從右往左分別看成是第0,1,2,3個元素,二進位制數該位為0,表示該元素不在子集中; 為1,表示在子集中。例如,0110表示第1,2號元素在子集中,0,3號元素不在子集中。
從0到15正好有16個數,而包含4個元素的所有組合的個數也是16,每一個數就 對應了一個子集,該整數中1的位置就指示了屬於子集的元素。
因此一個迴圈就可以枚舉出n個元素的所有組合:

 up = 1 << n;     //up -1的二進位制形式恰好有n個1 
 for(int s = 0; s < up; s++)   

要檢驗一個整數所代表的子集中有哪些元素,需要用到位運算:

1<<i //表示把1左移i位  
s & (1<<i)//表示檢驗s的右起第i位是否為1,為1則表示第i號元素在子集中  
for(int i = 0; i < n; i++)  
    if( s & (1 << i)) 
         printf(“%d “, A[i]); //輸出第i號元素

實現

#include<cstdio>
#define MAXN 10
int A[MAXN+5];
int n;
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&A[i]);
    int up=1<<n;
    for(int s=0;s<up;s++)
    {
        for(int i=0;i<n;i++)
            if(s&(1<<i))
                printf("%d",A[i]);
        puts("");
    }
    return 0;
}

思考
二進位制法沒有用到遞迴。
聯想集合的二進位制整數表示

Tip:內容相照應《演算法競賽入門經典》中第七章