算法竞赛入门经典 第七章 总结
目錄:
- 7.1 簡單枚舉
- 7.2 枚舉排列
- 7.3 子集生成
7.1 簡單枚舉
例題7-1 除法(Division, UVa 725)
輸入正整數n,按從小到大的順序輸出所有形如abcde/fghij = n的表達式,其中a~j恰好 為數字0~9的一個排列(可以有前導0),2≤n≤79。
樣例輸入:
62
樣例輸出:
79546 / 01283 = 62
94736 / 01528 = 62
分析:只需要枚舉fghij就可以算出abcde,然后判斷是否 所有數字都不相同即可
例題7-2 最大乘積(Maximum Product, UVa 11059)
輸入n個元素組成的序列S,你需要找出一個乘積最大的連續子序列。如果這個最大的乘 積不是正數,應輸出0(表示無解)。1≤n≤18,-10≤Si≤10。
樣例輸入:
3
2 4-3
5
2 5 -1 2 -1
樣例輸出:
8
20
分析:連續子序列有兩個要素:起點和終點,因此只需枚舉起點和終點即可,其中起點為i,終點為j。
例題7-3 分數拆分(Fractions Again?!, UVa 10976)
輸入正整數k,找到所有的正整數x≥y,使得 1/k=1/x+1/y
樣例輸入:
2
12
樣例輸出:
2
1/2 = 1/6 + 1/3
1/2 = 1/4 + 1/4
8
1/12 = 1/156 + 1/13
1/12 = 1/84 + 1/14
1/12 = 1/60 + 1/15
1/12 = 1/48 + 1/16
1/12 = 1/36 + 1/18
1/12 = 1/30 + 1/20
1/12 = 1/28 + 1/21
1/12 = 1/24 + 1/24
分析:
既然要求找出所有的x、y,枚舉對象自然就是x、y了。可問題在于,枚舉的范圍如何? 從1/12=1/156+1/13可以看出,x可以比y大很多。難道要無休止地枚舉下去?當然不是。由 于x≥y,有 1/k<=1/y+1/y因此 ,即y≤2k。這樣,只需要在2k范圍之內枚舉y,然后根據y嘗試 計算出x即可
7.2 枚舉排列
例7-2-1:輸入整數n,按字典序從小到大的順序輸出前n個數的 所有排列。
分析:兩個序列的字典序大小關系等價于從頭開始第一個不相同位置處的大 小關系。例如,(1,3,2) < (2,1,3),字典序最小的排列是(1, 2, 3, 4,…, n),最大的排列是(n, n-1, n-2,…, 1)。n=3時,所有排列的排序結果是(1, 2, 3)、(1, 3, 2)、(2, 1, 3)、(2, 3, 1)、(3, 1, 2)、 (3, 2, 1)。
以1開頭的排列的特點是:第一位是1,后面是2~9的排列。根據字典序的定義,這些2 ~9的排列也必須按照字典序排列。換句話說,需要“按照字典序輸出2~9的排列”,不過需 注意的是,在輸出時,每個排列的最前面要加上“1”。這樣一來,所設計的遞歸函數需要以 下參數:
1.已經確定的“前綴”序列,以便輸出。
2.需要進行全排列的元素集合,以便依次選做第一個元素
下面考慮程序實現。不難想到用數組表示序列A,而集合S根本不用保存,因為它可以 由序列A完全確定——A中沒有出現的元素都可以選。C語言中的函數在接受數組參數時無法 得知數組的元素個數,所以需要傳一個已經填好的位置個數,或者當前需要確定的元素位置 cur,代碼如下:
#include<iostream> #include<cstdio> using namespace std; int a[1000]; void print_permutation(int n,int *a,int cur); int main(){int n;cin>>n;print_permutation( n, a, 0); }void print_permutation(int n,int *a,int cur){if(cur==n) {for(int i=0;i<n;++i) printf("%d ",a[i]);printf("\n") ;}else{for(int i=1;i<=n;++i){//改變前綴int ok = 1;for(int j=0;j<cur;++j){//如果i已經在A[0]~A[cur-1]出現過,則不能再選if(a[j]==i) ok = 0;}if(ok){a[cur]=i;print_permutation( n, a, cur + 1);//每一個前綴的可能排序}}} }遞歸邊界是S為空 的情形,這很好理解:現在序列A就是一個完整的排列,直接輸出即可。接下來按照從小到大的順序考慮S中的每個元素,每次遞歸調用以A開頭.
循環變量i是當前考察的A[cur]。為了檢查元素i是否已經用過,上面的程序用到了一個 標志變量ok,初始值為1(真),如果發現有某個A[j]==i時,則改為0(假)。如果最終ok仍 為1,則說明i沒有在序列中出現過,把它添加到序列末尾(A[cur]=i)后遞歸調用。
例7-2-2生成可重集的排列
如果把問題改成:輸入數組P,并按字典序輸出數組A各元素的所有全排列,則需要對 上述程序進行修改——把P加到print_permutation的參數列表中,然后把代碼中的if(A[j] == i) 和A[cur] = i分別改成if(A[j] == P[i])和A[cur] = P[i]。這樣,只要把P的所有元素按從小到大的順序排序,然后調用print_permutation(n, P, A, 0)即可,如下面代碼所示。
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; int a[1000];int p[1000]; void print_permutation(int *p,int n,int *a,int cur); int main(){int n,i;cin>>n;for(i=0;i<n;++i){cin>>p[i];}sort(p,p+n);print_permutation(p, n, a, 0); }void print_permutation(int *p,int n,int *a,int cur){int i;if(cur==n) {for(int i=0;i<n;++i) printf("%d ",a[i]);printf("\n") ;}else{for(int i=0;i<n;++i){//改變前綴int ok = 1;for(int j=0;j<cur;++j){//如果i已經在A[0]~A[cur-1]出現過,則不能再選if(a[j]==p[i]) ok = 0;}if(ok){a[cur]=p[i];print_permutation( p, n, a, cur + 1);//每一個前綴的可能排序}}} }這個方法看上去不錯,可惜有一個小問題:輸入1 1 1后,程序什么也不輸出(正確答案 應該是唯一的全排列1 1 1),原因在于,這樣禁止A數組中出現重復,而在P中本來就有重 復元素時,這個“禁令”是錯誤的。
一個解決方法是統計A[0]~A[cur-1]中P[i]的出現次數c1,以及P數組中P[i]的出現次數 c2。只要c1 < c2,就能遞歸調用
結果又如何呢?輸入1 1 1,輸出了27個1 1 1。遺漏沒有了,但是出現了重復:先試著把 第1個1作為開頭,遞歸調用結束后再嘗試用第2個1作為開頭,遞歸調用結束后再嘗試用第3 個1作為開頭,再一次遞歸調用。可實際上這3個1是相同的,應只遞歸1次,而不是3次。
換句話說,我們枚舉的下標i應不重復、不遺漏地取遍所有P[i]值。由于P數組已經排過 序,所以只需檢查P的第一個元素和所有“與前一個元素不相同”的元素,即只需在“for(i = 0; i < n; i++)”和其后的花括號之前加上“if(!i || P[i] != P[i-1])”即可。
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; int a[1000];int p[1000]; void print_permutation(int *p,int n,int *a,int cur); int main(){int n,i;cin>>n;for(i=0;i<n;++i){cin>>p[i];}sort(p,p+n);print_permutation(p, n, a, 0); }void print_permutation(int *p,int n,int *a,int cur){int i;if(cur==n) {for(int i=0;i<n;++i) printf("%d ",a[i]);printf("\n") ;}else for(int i = 0; i < n; i++) { if(!i || p[i] != p[i-1]){int c1 = 0,c2 = 0; for(int j = 0; j < cur; j++) if(a[j] == p[i]) c1++; for(int j = 0; j < n; j++) if(p[i] == p[j]) c2++; if(c1 < c2) { a[cur] = p[i]; print_permutation(p,n, a, cur+1); } }}}總結:
如果某問題的解可以由多個步驟得到,而每個步驟都有若干種選擇(這些候 選方案集可能會依賴于先前作出的選擇),且可以用遞歸枚舉法實現,則它的工作方式可以 用解答樹來描述。
例7-2-3,利用next_permutation解答
枚舉所有排列的另一個方法是從字典序最小排列開始,不停調用“求下一個排列”的過 程。如何求下一個排列呢?C++的STL中提供了一個庫函數next_permutation
上述代碼同樣適用于可重集。
總結:枚舉排列的常見方法有兩種:一是遞歸枚舉,二是用STL中的 next_permutation
7.3 子集生成
總結
以上是生活随笔為你收集整理的算法竞赛入门经典 第七章 总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (并查集)Wireless Networ
- 下一篇: 2018.9.15,Matlab实验三: