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);
}
思考
- 這道題如果要輸出序號,可以增加一個變數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:內容相照應《演算法競賽入門經典》中第七章