算法--遞歸策略


歡迎關注我的個人博客:www.wuyudong.com, 更多精彩文章與您分享

遞歸的概念與基本思想

一個函數、過程、概念或數學結構,如果在其定義或說明內部又直接或間接地出現有其本身的引用,則稱它們是遞歸的或者是遞歸定義的。在程序設計中,過程或函數直接或者間接調用自己,就被稱為遞歸調用。

遞歸的實現方法

遞歸是借助於一個遞歸工作棧來實現;遞歸=遞推+回歸;

遞推:問題向一極推進,這一過程叫做遞推;這一過程相當於壓棧。

回歸:問題逐一解決,最后回到原問題,這一過程叫做回歸。這一過程相當於彈棧。

例如:用遞歸算法求 n!

定義:函數 fact(n)=n!

  fact(n-1)=(n-1)!

  則有  fact(n)=n*fact(n-1)

  已知  fact(1)=1

下面畫出了調用和返回的遞歸示意圖:

 

遞歸實現的代價是巨大的棧空間的耗費,那是因為過程每向前遞推一次,程序將本層的實在變量(值參和變參)、局部變量構成一個“工作記錄”壓入工作棧的棧頂,只有退出該層遞歸時,才將這一工作記錄從棧頂彈出釋放部分空間。由此可以想到,減少每個“工作記錄”的大小便可節省部分空間。例如某些變參可以轉換為全局變量,某些值參可以省略以及過程內部的精簡。

【例題】寫出結果

#include<iostream>
using namespace std;
void rever()
{
    char c;
    cin>>c;
    if(c!='!') rever();
    cout<<c;
}
int main( )
{
    rever();
    system("PAUSE");
    return 0;
}

【樣例輸入】gnauh!        【樣例輸出】!huang

采用遞歸方法編寫的問題解決程序具有結構清晰,可讀性強等優點,且遞歸算法的設計比非遞歸算法的設計往往要容易一些,所以當問題本身是遞歸定義的,或者問題所涉及到的數據結構是遞歸定義的,或者是問題的解決方法是遞歸形式的時候,往往采用遞歸算法來解決。

遞歸算法的類型

遞歸算法可以分為兩種類型:

基於分治策略的遞歸算法;

基於回溯策略的遞歸算法。

基於分治策略的遞歸算法

分而治之(divide-and-conquer)的算法

設計思想:

1.Divide:把問題划分為若干個子問題;

2.Conquer:以同樣的方式分別去處理各個子問題;

3.Combine:把各個子問題的處理結果綜合起來,形成最終的處理結果。

如何編寫基於分治策略的遞歸程序?

在算法分析上,要建立分治遞歸的思維方式。

在編程實現上,要建立遞歸信心(To turst the recursion,  Jerry Cain,  stanford)。

如何建立分治遞歸的思維方式?

基本原則:目標驅動!

計算n!:n! = n * (n-1)!,且1! = 1。

int  main( )
{
        int   n;
        printf("請輸入一個整數:");
        scanf("%d",   &n);
        printf("%d!  =  %d \n",   n,   fact(n));
        return 0
}
int   fact(int  n)
{
        if(n  ==  1)    return(1);
        else     return(n * fact(n-1));
}

如何建立遞歸信心?

函數的遞歸調用到底是如何進行的呢?在遞歸調用時,執行的是不是相同的代碼?訪問的是不是相同的數據?如果是的話,那么大家會不會相互干擾、相互妨礙?

例題:尋找最大值

問題描述:給定一個整型數組a,找出其中的最大值。

如何來設計相應的遞歸算法?

目標:max{a[0], a[1], … a[n-1]}

可分解為:max{a[0], max{a[1], … a[n-1]}}

另外已知max{x} = x

這就是遞歸算法的遞歸形式和遞歸邊界,據

此可以編寫出相應的遞歸函數:

int Max(int a[], int first, int n)
{
    int  max;

    if(first == n-1)  return a[first];
    max = Max(a, first+1, n);
    if(max < a[first])
          return a[first];
    else  return max;
}

折半查找法

問題描述:

查找(Searching):根據給定的某個值,在一組數據(尤其是一個數組)當中,確定有沒有出現相同取值的數據元素。

順序查找、折半查找。

int bsearch(int b[], int x, int L, int R)
{
    int mid;
    if(L > R) return(-1);
    mid = (L + R)/2;
    if(x == b[mid]) 
        return mid;
    else if(x < b[mid]) 
        return bsearch(b, x, L, mid-1);
    else 
        return bsearch(b, x, mid+1, R);
}

漢諾(Hanoi)塔問題

相傳在古印度Bramah廟中,有位僧人整天把三根柱子上的金盤倒來倒去,原來他是想把64個一個比一個小的金盤從一根柱子上移到另一根柱子上去。移動過程中遵守以下規則:每次只允許移動一只盤,且大盤不得落在小盤上(簡單嗎?若每秒移動一只盤子,需5800億年)

分析:

在A柱上有 n 個盤子, 從小到大分別為1號、2號、3號、…、n號。

      第 1 步:將1號、2號、…、n-1號盤作為一個整體,在C的幫助下,從A移至B;

      第 2 步:將n號盤從A移至C;

  第 3 步:再將1號、2號、…、n-1號盤作為一個整體,在A的幫助下,從B移至C;

      這三步記為:

      move   n-1   discs   from   A   to   B   using   C;

      move   1      discs   from   A   to   C;

      move   n-1   discs   from   B   to   C   using   A ;

代碼如下:
#include  <stdio.h>
void   move(int n, char L, char M, char R);
int main( )
{
        int   n;
        printf("請輸入一個整數:");
        scanf("%d",  &n);
        move(n,  'A',  'B',  'C');
        return 0;
}
// L: Left post,  M: Middle post,  R: Right post
void  move(int n, char L, char M, char R)
{
        if(n  ==  1) 
            printf("move #1 from %c to %c\n",  L,  R);
        else
        {
            move(n-1,  L,  R,  M);
            printf("move #%d from %c to %c\n",  n,  L,  R);
            move(n-1,  M,  L,  R);
        }
}

基於回溯策略的遞歸

在程序設計當中,有相當一類求一組解、或求全部解或求最優解的問題,不是根據某種確定的計算法則,而是利用試探和回溯(Backtracking)的搜索技術求解。回溯法也是設計遞歸算法的一種重要方法,它的求解過程實質上是一個先序遍歷一棵“狀態樹”的過程,只不過這棵樹不是預先建立的,而是隱含在遍歷的過程當中。

例題:分書問題

有五本書,它們的編號分別為1,2,3,4,5,現准備分給 A, B, C, D, E五個人,每個人的閱讀興趣用一個二維數組來加以描述:

希望編寫一個程序,輸出所有的分書方案,讓人人皆大歡喜。

假定這5個人對這5本書的閱讀興趣如下表:

思路:

1、定義一個整型的二維數組,將上表中的閱讀喜好用初始化的方法賦給這個二維數組。可定義:

int  Like[6][6] = {{0}, {0, 0,0,1,1,0}, 
                        {0, 1,1,0,0,1},
                        {0, 0,1,1,0,1}, 
                        {0, 0,0,0,1,0},                     
                        {0, 0,1,0,0,1}};

2、定義一個整型一維數組BookFlag[6]用來記錄書是否已被選用。用后五個下標作為五本書的標號,被選用的元素值為1, 未被選用的值為0, 初始化皆為0.

int  BookFlag[6] = {0};

3、定義一個整型一維數組BookTaken[6]用來記錄每一個人選用了哪一本書。用數組元素的下標來作為人的標號,用數組元素的值來表示書號。如果某個人還沒有選好書,則相應的元素值為0。初始化時,所有的元素值均為0。

int  BookTaken[6] = {0};

4、循環變量 i 表示人,j 表示書,i, j  ε{1, 2, 3, 4, 5}

一種方法:枚舉法。

把所有可能出現的分書方案都枚舉出來,然后逐一判斷它們是否滿足條件,即是否使得每個人都能夠得到他所喜歡的書。缺點:計算量太大。

#include<stdio.h>
void person(int i);
int Like[6][6] = {{0}, {0, 0, 0, 1, 1, 0}, 
                       {0, 1, 1, 0, 0, 1},
                       {0, 0, 1, 1, 0, 1}, 
                       {0, 0, 0, 0, 1, 0}, 
                       {0, 0, 1, 0, 0, 1}};
int BookFlag[6] = {0};
int BookTaken[6] = {0};
int main( )
{
    person( 1 );
    return 0;
}
void  person(int  i)    // 嘗試給第i個人分書
{   int  j,  k;
    for(j = 1;  j <= 5;  j++)    // 嘗試把每本書分給第i個人
    {   
        if((BookFlag[j] != 0) || (Like[i][j] == 0))   continue; // 失敗
        BookTaken[i] = j;          // 把第j本書分給第i個人
        BookFlag[j] = 1;
        if(i == 5){        // 已找到一種分書方案
            for(k = 1; k <= 5; k++) printf("%d ", BookTaken[k]);
            printf("\n");
        }
        else{
            person(i + 1);    // 給第i+1個人分書
        }
        BookTaken[i] = 0;    // 回溯,把這一次分得的書退回
        BookFlag[j] = 0;
    }
}

例題:八皇后問題

在8×8的棋盤上,放置8個皇后(棋子),使兩兩之間互不攻擊。所謂互不攻擊是說任何兩個皇后都要滿足:

(1)不在棋盤的同一行;
(2)不在棋盤的同一列;
(3)不在棋盤的同一對角線上。

因此可以推論出,棋盤共有8行,故至多有8個皇后,即每一行有且僅有一個皇后。這8個皇后中的每一個應該擺放在哪一列上是解該題的任務。

數據的定義(1):

 i —— 第i行(個)皇后,1 ≤ i ≤ 8;

 j —— 第j列, 1 ≤ j ≤ 8;

 Queen[i] —— 第i行皇后所在的列;

 Column[j]—— 第j列是否安全,{0, 1};

數據的定義(2):

Down[-7..7 ]——記錄每一條從上到下的對角線,是否安全,{0,1}

Up[2..16]——記錄每一條從下到上的對角角線,是否安全,{0,1}

利用以上的數據定義:

當我們需要在棋盤的( i, j ) 位置擺放一個皇后的時候,可以通過Column數組、Down數組和Up數組的相應元素,來判斷該位置是否安全;

當我們已經在棋盤的( i, j ) 位置擺放了一個皇后以后,就應該去修改Column數組、Down數組和Up數組的相應元素,把相應的列和對角線設置為不安全。

代碼如下:

void TryQueen(int  i);
int   Queen[9]   =  { 0 };
int   Column[9]  =  { 0 };
int   Down[15]   =  { 0 };
int   Up[15]     =  { 0 };
int  main( )
{    
    TryQueen(1);
    return 0;
}
void TryQueen(int i)    // 擺放第 i 行的皇后
{
    int   j,  k;
    for(j = 1;  j <= 8;  j++)    // 嘗試把該皇后放在每一列
    {   
        if(Column[j] || Down[i-j+7] || Up[i+j-2])   continue; // 失敗
        Queen[i] = j;  // 把該皇后放在第j列上
        Column[j] = 1;    Down[i-j+7] = 1;    Up[i+j-2] = 1;
        if(i  ==  8)    // 已找到一種解決方案
        {
            for(k = 1; k <= 8; k++)   printf("%d  ",   Queen[k]);
            printf("\n");
        }
        else    TryQueen(i + 1);    // 擺放第i+1行的皇后
        Queen[i] = 0;    // 回溯,把該皇后從第j列拿起
        Column[j] = 0;    Down[i-j+7] = 0;    Up[i+j-2] = 0;
    }
}

例題:過河問題

問題描述:

  M條狼和N條狗(N≥M)渡船過河,從河西到河東。在每次航行中,該船最多能容納2只動物,且最少需搭載1只動物。安全限制:無論在河東、河西還是船上,狗的數量不能小於狼的數量。請問:能否找到一種方案,使所有動物都能順利過河。如果能,移動的步驟是什么?

問題分析:

如何描述系統的當前狀態?

位置:河西岸、河東岸、河;

對象:船、狼、狗。

三元組(W、 D、 B)       (其中:W代表Wolf;D代表Dog;B代表Boat)

例如:(2, 2, W)

(2, 2, W)--> (0, 2, E)--> (1, 2, W)--> (1, 0, E) -->(2, 0, W) -->(0, 0, E)

第一步:帶2只狼到E,則W剩下0只狼,2只羊  (0, 2, E)

第二步:帶1只狼到W,則W剩下1只狼,2只羊  (1, 2, W)

第三步:帶2只羊到E,則W剩下1只狼,0只羊   (1, 0, E) 

第四步:帶1只狼到W,則W剩下2只狼,0只羊  (2, 0, W)

第五步:帶2只狼到E,則W剩下0只狼,0只羊  (0, 0, E)

1.問題實質:在一個有向圖中尋找一條路徑;

2.狀態轉換:如何從一個結點跳轉到另一個結點;

代碼如下:

#include <stdio.h>
#define MAX_M   20
#define MAX_N   20

int M, N;
struct Status  //構建三元組
{
    int W, D, B;
}steps[1000];

int s = 0, num = 0;
int flags[MAX_M][MAX_N][2] = {0};

void CrossRiver(int W, int D, int B);
int IsValid(int w, int d, int b);
int main( )
{
    scanf("%d %d", &M, &N);
    flags[M][N][0] = 1;    //初始狀態(M,N,W)
    steps[0].W = M;
    steps[0].D = N;
    steps[0].B = 0;
    s = 1;
    CrossRiver(M, N, 0);
    return 0;
}
void CrossRiver(int W, int D, int B)    
{
    int i, j, f;
    int w, d, b;
    if(B == 0) f = -1;
    else f = 1;

    for(j = 1; j <= 5; j++)
    {
        switch(j)
        {
            case 1:  w = W + f*1;  d = D;  break;
            case 2:  w = W + f*2;  d = D;  break;
            case 3:  d = D + f*1;  w = W;  break;
            case 4:  d = D + f*2;  w = W;  break;
            case 5:  w = W + f*1;  d = D + f*1; break;
        }
        b = 1 - B;
        if(IsValid(w, d, b))
        {
            flags[w][d][b] = 1;
            steps[s].W = w;
            steps[s].D = d;
            steps[s].B = b;
            s++;
            if(w == 0 && d == 0 && b == 1)
            {
                num ++;
                printf("Solutions %d: \n", num);
                for(i = 0; i < s; i++) printf("%d %d %d\n", steps[i].W,
                             steps[i].D, steps[i].B);
            }
            else  CrossRiver(w, d, b);
            flags[w][d][b] = 0;
            s--;        
        }
    }
}
int IsValid(int w, int d, int b) //判斷三元組的某一狀態是否合法
{
    if(w < 0 || w > M) return 0;    
    if(d < 0 || d > N) return 0;    
    if(flags[w][d][b] == 1) return 0;  
    if(d > 0 && w > d)  return 0;   
    if((N-d > 0) && (M-w > N-d)) return 0;
    return 1;
}

例題:排列問題

n個對象的一個排列,就是把這 n 個不同的對象放在同一行上的一種安排。例如,對於三個對象 a,b,c,總共有6個排列:

  a   b   c

  a   c   b

  b   a   c

  b   c   a

  c   a   b

  c   b   a

n 個對象的排列個數就是 n!。

如何生成排列?

基於分治策略的遞歸算法:

假設這 n 個對象為 1, 2, 3, …, n;

對於前n-1個元素的每一個排列 a1 a2 … an-1,1£ai £ n-1,通過在所有可能的位置上插入數字 n,來形成 n 個所求的排列,即:
  n a1 a2 … an-1
  a1 n a2 … an-1
  ……
  a1 a2n an-1
  a1 a2 … an-1 n

例如:生成1,2,3的所有排列

permutation(3) -> permutation(2) -> permutation(1)

permutation(1):1

permutation(2):2  1,1  2

permutation(3):3  2  1,2  3  1,2  1  3,

                        3  1  2,1  3  2,1  2  3

基於回溯策略的遞歸算法:

基本思路:每一個排列的長度為 N,對這N個不同的位置,按照順序逐一地枚舉所有可能出現的數字。

定義一維數組NumFlag[N+1]用來記錄1-N之間的每一個數字是否已被使用,1表示已使用,0表示尚未被使用,初始化皆為0;

定義一維數組NumTaken[N+1],用來記錄每一個位置上使用的是哪一個數字。如果在某個位置上還沒有選好數字,則相應的數組元素值為0。初始化時,所有元素值均為0;

循環變量 i 表示第 i 個位置,j 表示整數 j,i, j ε{1, 2, …, N}。

代碼如下:

#include <stdio.h>
#define N 3
void   TryNumber( int  i );
int   NumFlag[N+1]  =  {0};
int   NumTaken[N+1]  =  {0};
int main( )
{
    TryNumber( 1 );
    return 0;
}
void TryNumber(int i)    
{
     int j, k;
     for(j = 1;  j <= N;  j++)
     {   
         if(NumFlag[j]  !=  0)   continue; 
         NumTaken[i] = j;
         NumFlag[j] = 1;
         if(i == N)
         {
              for(k = 1; k <= N; k++) printf("%d ", NumTaken[k]);
              printf("\n");
         }
         else  TryNumber(i + 1);
         NumTaken[i] = 0;
         NumFlag[j] = 0;
    }
}

你可能感興趣的:

遞歸與尾遞歸(C語言)

遞歸練習(C語言)

算法--遞推策略 

遞推求解專題練習

算法--枚舉策略


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM