轉載請聲明出處:http://blog.csdn.net/zhongkelee/article/details/44901905
一、算法整體思想
對k個子問題分別求解,如果子問題的規模仍然不夠小,則再劃分爲k個子問題。 如此遞歸地進行下去,直到問題規模足夠小,很容易求出其解爲止。
將求出的小規模問題的解合併成一個更大規模的問題的解,自底向上逐步求出原來問題的解。
分治法的設計思想是,將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。
二、遞歸與分治概念
1. 遞歸的概念
直接或者間接地調用自身的算法稱爲遞歸算法;用函數自身給出定義的函數稱爲遞歸函數。
分治法產生的子問題往往是原問題的較小模式,這就爲使用遞歸技術提供了方便。原問題與子問題的唯一區別是輸入規模不同。
在這種情況下,反覆應用分治手段,可以使子問題與原問題類型一致而其規模卻不斷縮小,最終使子問題縮小到很容易直接求出其解。
分治與遞歸像一對孿生兄弟,經常同時應用在算法設計之中,並由此產生許多高效算法。
2. 分治的基本過程
Divide:將問題的規模變小
Conquer:遞歸的處理小規模問題(只需遞歸調用即可)
Combine:將小規模問題的解合併爲原始問題的解
這其中的重點是問題的分解和解的合併操作。
分解的要點是:
a.分解的條件要互斥;
b.分解後的所有條件加在一起,能夠覆蓋原始問題的所有情況。
3.相關易混概念
遞歸是算法的實現方式,分治是算法的設計思想。
迭代是不斷地循環過程,遞歸式不斷地調用自身。
4.遞歸的弊端
遞歸調用的方式,在實際代碼體現上,不斷地在消耗內存中棧的空間,並且有相關的值拷貝動作,函數不斷地壓棧,導致資源不斷地消耗。
三、實例
1.階乘函數
階乘函數可以遞歸地定義爲:
邊界條件和遞歸方程是遞歸函數的兩個要素,遞歸函數只有具備了這兩個要素,才能在有限次計算後得出結果。
階乘函數非遞歸定義爲:n! = 1 x 2 x 3 x ... x (n-1) x n
代碼實現:
#include <iostream>
using namespace std;
// factorial implement by loop
long factorial_loop(long n){
long result = 1;
for (int i = n; i > 0; -- i)
result *= i;
return result;
}
// factorial implement by recursive
long factorial_recursive(long n){
if (n == 0)
return 1;
return n*factorial_recursive(n-1);
}
int main(){
for (int i = 0; i < 10; i ++ ) {
cout << i << "!" << " = "
<< factorial_recursive(i)
<< ","
<< factorial_loop(i)
<< endl;
}
return 0;
}
運行結果:
2.Fibonacci數列
無窮數列1,1,2,3,5,8,13,21,34,55,...,稱爲Fibonacci數列。它可以遞歸地定義爲:
非遞歸定義爲:
代碼實現:
#include <iostream>
using namespace std;
long fibonacci_recursive(long n){
if (n <= 1)
return 1;
return fibonacci_recursive(n-1)+fibonacci_recursive(n-2);
}
long fibonacci_loop(long n){
if (n <= 1)
return 1;
long f1 = 1;
long f2 = 1;
long result = 0;
for (int i = 1; i < n; i++){
result = f1 + f2;
f1 = f2;
f2 = result;
}
return result;
}
int main()
{
cout << "fibonacci implement by recursive: " << endl;
for (long i = 0; i <= 20; ++ i)
cout << fibonacci_recursive(i) << " " ;
cout << endl << endl;
cout << "fibonacci implement by loop: " << endl;
for (long i = 0; i <= 20; ++ i)
cout << fibonacci_loop(i) << " " ;
cout << endl;
return 0;
}
運行結果:
3.Ackerman函數
當一個函數及它的一個變量是由函數自身定義時,稱這個函數是雙遞歸函數。
Ackerman函數A(n, m)定義如下:
並非一切遞歸函數都能用非遞歸方式定義,比如本例中的Ackerman函數就無法找到非遞歸的定義。
A(n,m) 的自變量 m 的每一個值都定義了一個單變量函數:
m = 0 時,A(n,0) = 2
m = 1 時,A(n,1) = A(A(n-1,1),0)=A(n-1,1)+2 和 A(1,1)=2,故 A(n,1)=2*n
m = 2時,A(n,2)=A(A(n-1,2),1)=2A(n-1,2) 和 A(1,2)=A(A(A(0,2)),1)=A(1,1)=2,A(n,2)=2^n
m = 3時,類似的可以推出
m = 4時,A(n,4)的增長速度非常快,以至於沒有適當的數學式子來表示這一函數。
代碼實現:
#include <iostream>
using namespace std;
// ackerman implement
long ackerman(long n, long m){
if (n == 1 && m == 0)
return (long)2;
if (n == 0 && m >= 0)
return 1;
if (m == 0 && n >= 2)
return n + 2;
if (n >= 1 && m>= 1)
return ackerman( ackerman(n-1,m) , m-1);
}
int main()
{
cout << "m = 0 : " << endl;
cout << "ackerman(1,0) = " << ackerman(1,0) << endl;
cout << "ackerman(2,0) = " << ackerman(2,0) << endl;
cout << "ackerman(3,0) = " << ackerman(3,0) << endl;
cout << "ackerman(4,0) = " << ackerman(4,0) << endl;
cout << "m = 1 : " << endl;
cout << "ackerman(1,1) = " << ackerman(1,1) << endl;
cout << "ackerman(2,1) = " << ackerman(2,1) << endl;
cout << "ackerman(3,1) = " << ackerman(3,1) << endl;
cout << "ackerman(4,1) = " << ackerman(4,1) << endl;
cout << "m = 2 : " << endl;
cout << "ackerman(1,2) = " << ackerman(1,2) << endl;
cout << "ackerman(2,2) = " << ackerman(2,2) << endl;
cout << "ackerman(3,2) = " << ackerman(3,2) << endl;
cout << "ackerman(4,2) = " << ackerman(4,2) << endl;
cout << "m = 3 : " << endl;
cout << "ackerman(1,3) = " << ackerman(1,3) << endl;
cout << "ackerman(2,3) = " << ackerman(2,3) << endl;
cout << "ackerman(3,3) = " << ackerman(3,3) << endl;
cout << "ackerman(4,3) = " << ackerman(4,3) << endl;
return 0;
}
運行結果:
4. 排列問題
設計一個遞歸算法生成 n 個元素 {r1,r2,...,rn} 的全排列。
設 R = {r1,r2,...,rn} 是要進行排列的 n 個元素,Ri = R - {ri}。集合X中元素的全排列記爲 Perm(X)。
(ri)Perm(X) 表示在全排列 Perm(X) 的每一個排列前加上前綴 ri 得到的排列。
R的全排列可以定義如下:
當 n=1 時,Perm(R)=(r),其中 r 是集合R中唯一的元素。
當 n>1 時,Perm(R) 由 (r1)Perm(R1),(r2)Perm(R2),...,(rn)Perm(Rn) 構成。
依此遞歸定義,可以設計產生Perm(R)的遞歸代碼:
#include <iostream>
#include <vector>
#include <iterator>
using namespace std;
/* 使用遞歸實現
* 遞歸產生所有前綴是list[0:k-1],
* 且後綴是list[k,m]的全排列的所有排列
* 調用算法perm(list,0,n-1)則產生list[0:n-1]的全排列
*/
template <class T>
void perm_recursion(T list[],int k,int m)
{
// 產生list[k:m]的所有排列
if (k == m) {
//只剩下1個元素
for (int i = 0; i <= m; i ++)
cout << list[i] << " ";
cout << endl;
}
else {
// 還有多個元素,遞歸產生排列
for (int i = k; i <= m; ++ i) {
swap(list[k],list[i]);
perm_recursion(list,k+1,m);
swap(list[k],list[i]);
}
}
}
// 非遞歸實現(可參照STL next_permutation源碼)
template <class T>
void perm_loop(T list[],int len)
{
int i,j;
vector<int> v_temp(len);
// 初始排列
for(i = 0; i < len ; i ++)
v_temp[i] = i;
while (true) {
for (i = 0; i < len; i ++ )
cout << list[v_temp[i]] << " ";
cout << endl;
// 從後向前查找,看有沒有後面的數大於前面的數的情況,若有則停在後一個數的位置。
for(i = len - 1;i > 0 && v_temp[i] < v_temp[i-1] ; i--);
if (i == 0)
break;
// 從後查到i,查找大於 v_temp[i-1]的最小的數,記入j
for(j = len - 1 ; j > i && v_temp[j] < v_temp[i-1] ; j--);
// 交換 v_temp[i-1] 和 v_temp[j]
swap(v_temp[i-1],v_temp[j]);
// 倒置v_temp[i]到v_temp[n-1]
for(i = i,j = len - 1 ; i < j;i ++,j --) {
swap(v_temp[i],v_temp[j]);
}
}
}
int main()
{
int list[] = {0,1,2};
cout << "permutation implement by recursion: " << endl;
perm_recursion(list,0,2);
cout << endl;
cout << "permutation implement by loop: " << endl;
perm_loop(list,3);
cout << endl;
return 0;
}
運行結果:
5.整數劃分問題
將正整數 n 表示成一系列正整數之和:n = n1+n2+...+nk,其中n1≥n2≥...≥nk,k≥1。
正整數n的這種表示成爲正整數n的劃分。正整數n的不同劃分個數稱爲正整數n的劃分數,記做p(n)。
例如,正整數6有如下11種不同的劃分,所以p(6) = 11。
6;
5+1;
4+2,4+1+1;
3+3,3+2+1,3+1+1+1;
2+2+2,2+2+1+1,2+1+1+1+1;
1+1+1+1+1+1;
前面幾個例子中,問題本身都具有比較明顯的遞推關係,因而容易用遞歸函數直接求解。
在本例中,如果設p(n)爲正整數n的劃分數,則難以找到遞歸關係,因此考慮增加一個自變量。
在正整數n的所有不同劃分中,將最大加數n1不大於m的劃分個數記作q(n,m)。
可以建立q(n,m)的如下遞歸關係。
(1) q(n,1)=1,n >= 1;當最大加數n1不大於1時,任何正整數n只有一種劃分形式,即n = 1 + 1 + … +1.
(2) q(n,m) = q(n,n),m >= n;最大加數n1實際上不能大於n。因此,q(1,m)=1。
(3) q(n,n)=1 + q(n,n-1);正整數n的劃分由n1=n的劃分和n1 ≤ n-1的劃分組成。
(4) q(n,m)=q(n,m-1)+q(n-m,m),n > m >1;正整數n的最大加數n1不大於m的劃分由n1 = m的劃分和n1 ≤ m-1的劃分組成。
以上的關係實際上給出了計算q(n,m)的遞歸式如下:
據此,可設計計算q(n,m)的遞歸函數如下。正整數n的劃分數p(n) = q(n,n)。
#include <iostream>
using namespace std;
int q(int n, int m){
if (n < 1 || m < 1)
return 0;
if (n == 1 || m == 1)
return 1;
if (n < m)
return q(n,n);
if (n == m)
return 1 + q(n,m-1);
return q(n,m-1) + q(n-m,m);
}
int p(int n){
return q(n,n);
}
int main()
{
for (int i = 1; i < 7; i++){
cout<<"interger_partition("
<<i
<<") = "
<<p(i)
<<endl;
}
return 0;
}
運行結果:
6.漢諾塔問題
hanoi(n, a, b, c)表示按照遊戲規則,n塊圓盤從a塔移至b塔,c塔作爲輔助塔。
move(a, b)表示將塔a最上面的圓盤移至塔b,並輸出這一步驟。
<span style="font-size:14px;">#include <iostream>
using namespace std;
void move(char t1, char t2){
cout<<t1<<" -> "<<t2<<endl;
}
void hanoi(int n, char a, char b, char c){
if (n > 0){
hanoi(n-1, a, c, b);
move(a,b);
hanoi(n-1, c, b, a);
}
}
int main()
{
cout << "hanoi(1,'a','b','c'): " << endl;
hanoi(1,'a','b','c');
cout << endl;
cout << "hanoi(2,'a','b','c'): " << endl;
hanoi(2,'a','b','c');
cout << endl;
cout << "hanoi(3,'a','b','c'): " << endl;
hanoi(3,'a','b','c');
cout << endl;
cout << "hanoi(4,'a','b','c'): " << endl;
hanoi(4,'a','b','c');
cout << endl;
return 0;
}</span>
運行結果:
三、遞歸小結
優點:結構清晰,可讀性強,而且容易用數學歸納法來證明算法的正確性,因此它爲設計算法、調試程序帶來很大方便。
缺點:遞歸算法的運行效率較低,無論是耗費的計算時間還是佔用的存儲空間都比非遞歸算法要多。
解決方法:在遞歸算法中消除遞歸調用,使其轉化爲非遞歸算法。
1、採用一個用戶定義的棧來模擬系統的遞歸調用工作棧。該方法通用性強,但本質上還是遞歸,只不過人工做了本來由編譯器做的事情,優化效果不明顯。
2、用遞推來實現遞歸函數。
3、通過變換能將一些遞歸轉化爲尾遞歸,從而迭代求出結果。
後兩種方法在時空複雜度上均有較大改善,但其適用範圍有限。
好了,遞歸與分治算法的第一部分內容就到這裏,後一篇博客我們着重講一下分治法的基本思想和幾個經典實例。
有任何問題請和我聯繫,共同進步:[email protected]
轉載請聲明出處:http://blog.csdn.net/zhongkelee/article/details/44901905