一,產生函數與遞推
遞推關係舉例
【例1】Hanoi問題:這是個組合數學中的著名問題。N個圓盤依其半徑大小,從下而上套在A柱上,如示。每次只允許取一個移到柱B或C上,而且不允許大盤放在小盤上方。若要求把柱A上的n個盤移到C柱上請設計一種方法來,並估計要移動幾個盤次。現在只有A、B、C三根柱子可用。
|
| |
|
| |
A
B C
第一步把A中N-1個移動到B(藉助C)
第二步把A中最下一個移動到C
第三步把B中移動到C(藉助A)
演算法複雜度:
h(n)表示n個盤子所需要轉移次數。
h(n)=2h(n-1)+1
h(1)=1
遞迴演算法:
#include <iostream> using namespace std; int num=0;void Move(int n,char x,char y) { num++; cout<<"把"<<n<<"號從"<<x<<"挪動到"<<y<<endl; } void Hannoi(int n,char a,char b,char c)//把n個盤子從a移動到c 藉助b { if(n==1) Move(1,a,c); else { Hannoi(n-1,a,c,b); //把n-1個盤子從a移動到b 藉助c Move(n,a,c); Hannoi(n-1,b,a,c); //把n-1個盤子從b移動到c 藉助a } } int main() { cout<<"以下是 3 層漢諾塔的解法:"<<endl; Hannoi(7,'a','b','c'); cout<<"輸出完畢!共需要次數:"<<num<<" 次"<<endl; return 0; }
【例2】排錯問題.n個有序的元素應有個不同的排列,如若一個排列使得所有的元素都不在原來的位置上,則稱這個排列為錯排;有的叫重排。
以1,2,3,4四個數的錯排為例,分析其結構,找出規律性的東西來。
1)1 2的錯排是唯一的,即2 1。
2)1 2 3的錯排有3 1 2,2 3 1。這二者可以看作是1
2錯排,3分別與1,2換位而得的。
即
2 1 32與3換位3 1 2
2 1 3 1與3換位2 3 1
3)1,2,3,4的錯排
4 3 2 1,4 1 2 3,4 3 1 2,
3 4 1 2,3 4 2 1,2 4 1 3,
2 1 4 3,3 1 4 2,2 3 4 1。
第一列是4分別與1,2,3互換位置,其餘兩個元素錯排,由此產生的。
第二列是4分別與3,1,2(123的一個錯排)的每一個數互換而得到的
分析:
設n個數1,2,…,n錯排的數目為Dn,任取其中一數 i,數i分別與其他的n-1個數之一互換,其餘n-2個數進行錯排,共得 (n-1)Dn-2個錯排。另一部分位元i以外的n-1個數進行錯排,然後 i 與其中每個數互換得(n-1)Dn-1個錯排。
Dn=(n-1)(Dn-1 + Dn-2) D1=0 D2=1
Dn - nDn-1=(-1) `n
二,遞迴演算法的結構
遞迴結構定義
在進行遞迴演算法的設計時,通常先寫出問題的遞迴定義,遞迴定義由基本項和歸納項兩部分組成。
•基本項,也就是遞迴出口。它描述了一個或幾個遞迴過程的終止狀態。所謂終止狀態指的是不需要繼續遞迴而直接求解的狀態。
•歸納項,也稱為遞迴過程。它描述了如何?從目前狀態到終止狀態的變化。
#include <iostream>using namespace std;int multiplication(int n){if(n==1)return 1;return n*multiplication(n-1);}int main(){cout<<"10的階乘為:"<<multiplication(10)<<endl; return 0;}
• 基於遞迴的插入排序演算法
#include <iostream>using namespace std;void in_rec (int A[],int n)// 基於遞迴的插入排序演算法{ int a,k; if(n<1) return ; in_rec (A,n-1);//歸納項,否則,對前面的n-1個元素排序 a = A[n]; // 把第n元素插入合適位置 k = n -1; while ((k>=0)&&(A[k]>a)) { A[k+1] = A[k]; k = k - 1; } A[k+1] = a;} int main(){int a[5]={3,2,1,4,5};in_rec (a,5);for(int i=0;i<5;++i)cout<<a[i]<<" ";cout<<endl;return 0;}
三,組合演算法分析
1)全排列演算法
•模型對應
•序數法
•字典序法
•中介數法
•換位法
例題:1 2 3 4 5 6 7 8 9 字典序全排列,求8 3 9 6 4 7 5 2 1的下一個排列
分析:一個全排列可看做一個字串,字串可有首碼、尾碼。所謂一個的下一個就是這一個與下一個之間沒有其他的。這就要求這一個與下一個有儘可能長的共同首碼,也即變化限制在儘可能短的尾碼上。
例如,839647521是1--9的排列。1—9的排列最前面的是123456789,最後面的是987654321,從右向左掃描若都是增的,就到了987654321,也就沒有下一個了。否則找出第一次出現下降的位置。
解答: 1、搜尋末端的最長遞降序列。
2、記緊挨著該序列左邊的數為 a。
3、在該序列中從右至左尋找首個大於 a 的數記為 b。
4、交換 a、b,反轉原序列被 b 換入後的新序列。
(輸出全排列)演算法:
#include <stdio.h> int n = 0; void swap(int *a, int *b) { int m; m = *a; *a = *b; *b = m; } void perm(int list[], int k, int m) { int i; if(k > m) { for(i = 0; i <= m; i++) printf("%d ", list[i]); printf("\n"); n++; } else { for(i = k; i <= m; i++) { swap(&list[k], &list[i]); perm(list, k + 1, m); swap(&list[k], &list[i]); } } } int main() { int list[] = {1, 2, 3, 4, 5}; perm(list, 0, 4); printf("total:%d\n", n); return 0; }
(輸出下一個)演算法:
#include <iostream> using namespace std; void swap(int* x,int* y) { int temp; temp=*x; *x=*y; *y=temp; } void reverse(int *first,int *last) { --last; for(;first<last;first++,last--) swap(first,last); } bool nextpermutation(int* first,int* last) { if(first==last) return false; //為空白 if(first+1==last) return false; //只有一個元素 int* i=last; --i; //最後一個元素 for(;;){ int* ii=i; //後一個元素 --i;//前一個元素 if(*i<*ii){ //前<後且相鄰 int* j=last; while(!(*i<*--j));//從後向前找第一個大於i的數 swap(i,j); //交換*i,*j reverse(ii,last); //反向排列[ii,last) return true; } if(i==first){//前一元素已指向首元素,反轉整個區間,無下一排列 reverse(first,last); return false; } } } //輸出結果8 3 9 6 5 1 2 4 7 int main() { int d[]={8,3,9,6,4,7,5,2,1}; if(nextpermutation(d,d+9)) { for(int i=0;i<9;i++) cout<<d[i]<<" "; cout<<endl; } return 0; }
2)中介數
在[1,n]的全排列中,nn-1…
321是最後一個排列,其中介數是(n-1,n-2,...,3,2,1)。而其序號為
1*1!+2*2!+……(n-1)!
計算給定排列的序號
8 3 9 6 4 7 5 2 1的序號即先於此排列的排列的個數。將先於此排列的排列按首碼分類。
將8!,7!,…,1!前面的係數抽出,放在一起得到7 2 6 4 2 3 2 1。
7 2 6 4 2 3 2 1是計算排列8 3
9 6 4 7 5 2 1的序號的中間環節,我們稱之為中介數。
※這是一個很有用的概念。
【例12】由中介數推出排列的演算法,例如由中介數7
2 6 4 2 3 2 1(8個數)推算出全排列:8 3 9 6 4 7 5 2 1
方法一: 推導
p1 p2 p3 p4 p5 p6 p7 p8 p9
8 2 1
8 3 2 1
8 3 4 2 1
8 3 9 4 2 1
…… …… ……
其他詳細方法參見博文
http://blog.csdn.net/tianshuai11/article/details/7520370