遞推(二):遞推法的應用


      下面通過一些典型實例及其擴展來討論遞推法的應用。

【例2】骨牌鋪方格

      在2×n的一個長方形方格中,用一種2×1的骨牌鋪滿方格。輸入n(n<=40),輸出鋪放方案的總數。

      例如n=3時,為2×3方格,骨牌的鋪放方案有三種,如下圖1所示。

 

圖1  2×3方格的骨牌鋪放方案

      (1)編程思路。

      設f[i]為鋪滿2*n方格的方案數,則有    f[i]=f[i-1]+f[i-2]。

      其中,f[i-1]為鋪滿2*(n-1)方格的方案數(既然前面的2*(n-1)的方格已經鋪滿,那么最后一個只能是豎着放)。f[i-2]為鋪滿2*(n-2)方格的方案數(如果前面的2*(n-2)的方格已經鋪滿,那么最后的只能是橫着放兩塊,否則會重復)。

      初始情況為:f[1]=1,f[2]=2。

      (2)源程序。

#include <iostream>

using namespace std;

int main()

{

    int i,n,f[41];

    cin>>n;

    f[1]=1;f[2]=2; 

    for(i=3;i<=n;i++)

     f[i]=f[i-1]+f[i-2];      // 按遞推關系實施遞推 

    cout<<f[n]<<endl;

    return 0;

}

       (3)問題擴展。

      有一個大小是2×n的長方形方格,現用2種規格的骨牌鋪滿,骨牌規格分別是 2×1和2×2。輸入n(n<=30),輸出鋪放方案的總數。

      (4)擴展問題編程思路。

      鋪方格的骨牌規格多了一種,遞推公式做相應變化。

      設f[i]為鋪滿2*n方格的方案數,則有   f[i]=f[i-1]+2*f[i-2]。

      其中,f[i-1]為鋪滿2*(n-1)方格的方案數(既然前面的2*(n-1)的方格已經鋪滿,那么最后一個只能是豎着放)。f[i-2]為鋪滿2*(n-2)方格的方案數(如果前面的2*(n-2)的方格已經鋪滿,那么最后的2*2方格或者橫着放兩塊2*1的骨牌,或者放1塊2*2的骨牌)。

      初始情況為:f[1]=1,f[2]=3。

       (5)擴展問題的源程序。

#include <iostream>

using namespace std;

int main()

{

    int i,n,f[31];

    cin>>n;

    f[1]=1;f[2]=3; 

    for(i=3;i<=n;i++)

     f[i]=f[i-1]+2*f[i-2];      // 按遞推關系實施遞推 

    cout<<f[n]<<endl;

    return 0;

}

【例3】上台階

      設有一個共有n級的台階,某人上台階一步可上1級,也可上2級。編寫一個程序,輸入台階的級數n(n<=40),輸出某人從底層上到台階頂層的走法的種數。

      (1)編程思路。

      先考慮最簡單的情況。如果只有1級台階,那顯然只有一種上法。如果有2級台階,那就有兩種上的方法了:一種是分兩次上,每次上1級;另外一種就是一次上2級。

      再來討論一般情況。設把n級台階時的上法看成是n的函數,記為f(n)。

      當n>2時,第一次上的時候就有兩種不同的選擇:一是第一次只上1級,此時上法數目等於后面剩下的n-1級台階的上法數目,即為f(n-1);另外一種選擇是第一次上2級,此時上法數目等於后面剩下的n-2級台階的上法數目,即為f(n-2)。因此n級台階時的不同上法的總數f(n)=f(n-1)+(f-2)。

      由此推導出遞推公式 f(n)=f(n-1)+(f-2)  (n>2)

      初始情況:f(1)=1; f(2)=2。

      (2)源程序。

#include <iostream>

using namespace std;

int main()

{

    int k,n,f[41];

    cout<<"請輸入台階總數n:";

    cin>>n;

    f[1]=1;f[2]=2; 

    for(k=3;k<=n;k++)

     f[k]=f[k-1]+f[k-2];      // 按遞推關系實施遞推 

    cout<<"上法總數為 "<<f[n]<<endl;

    return 0;

}

      (3)擴展1。

      設一個台階有n級,一步有m種跨法,一步跨多少級均從鍵盤輸入(輸入的級數從小到大)。求從底層上到台階頂層的走法的種數。

      (4)擴展1的編程思路。

      例如,設有10級台階,輸入的m依次為2、4、7。遞推式可寫成

       f(n)=f(n-2)+f(n-4)+f(n-7)  (n>=8)。但由於事先不知輸入的是2、4、7,所以遞推初始情況f(1)~f(7)無法直接賦初值,也就無法用一個這個單純的遞推式直接遞推。必須先采用某種方法根據輸入的m個一步上的台階數將初始條件求取出來。

      實際上,可以將上台階的遞推分成多級遞推,這樣初始條件可在分級遞推中求取。

當第1次輸入2時,f(1)=0,f(2)=1。(初始條件)

當第2次輸入4時,第1級遞推:

f(3)=f(3-2)=0,  f(4)=f(4-2)+1=2。(因為4本身即為一個一步到位的上法)

當第3次輸入7時,第2級遞推:

f(5)=f(5-2)+f(5-4)=0,f(6)=f(6-2)+f(6-4)=3,f(7)=f(7-2)+f(7-4)+1=1。

為求第10級台階上法,采用三級遞推:

f(8)=f(8-2)+f(8-4)+f(8-7)=5,f(9)=f(9-2)+f(9-4)+f(9-7)=2,

f(10)=f(10-2)+f(10-4)+f(10-7)=8。

下面探討一般情況。

      設上n級台階的不同上法為f(n),從鍵盤輸入一步跨多少級的m個整數分別為x(1)、x(2)、…、x(m),輸入時約定1<=x(1) <x(2) <…<x(m)。

當1<=n<x(1) 時,f(n)=0;      f(x(1))=1。(初始條件)

當x(1)<n<x(2) 時,第1級遞推:f(n)=f(n−x(1));

  f(x(2))=f(n-x(1))+1 (存在x2這個一步到位的上法)。

當x(2)<n<x(3) 時,第2級遞推:f(n)=f(n−x(1))+f(n−x(2));

  f(x(3))=f(n-x(1))+ f(n−x(2)) +1 (存在x3這個一步到位的上法)。

……

當x(m-1)<n<x(m),有第m-1級遞推:f(n)=f(n−x(1))+f(n−x(2))+…+f(n−x(m-1));

f(x(m))=f(n−x(1))+f(n−x(2))+…+f(n-x(m-1))+1。

當x(m)<n時,第m級遞推: f(n)=f(n−x(1))+f(n−x(2))+…+f(n−x(m))

為了便於統一處理,不妨附設一個x(m+1),使得x(m+1)=max(x(m),n)+1。

這樣第m級遞推統一描述為:

       當x(m)<n<x(m+1) 時,第m級遞推: f(n)=f(n−x(1))+f(n−x(2))+…+f(n−x(m))

          f(x(m+1))= f(n−x(1))+f(n−x(2))+…+f(n−x(m))+1。

      (5)擴展1的源程序。

#include <iostream>

using namespace std;

int main()

{

       int i,j,k,m,n,t,x[10],f[51];

    cout<<"請輸入總台階數:";

    cin>>n;

    cout<<"一次有幾種跳法:";

    cin>>m;

    cout<<"請從小到大輸入一步跳幾級。"<<endl;

    for(i=1; i<=m; i++)    

        cin>>x[i];

    for(i=1;i<x[1];i++)   f[i]=0;  

    f[x[1]]=1;

    x[m+1]=(x[m]>n?x[m]:n)+1;

       for(k=1;k<=m;k++)

        for(t=x[k]+1;t<=x[k+1];t++)

              {

                     f[t]=0;

            for(j=1;j<=k;j++)             // 按公式累加實現分級遞推 

               f[t]=f[t]+f[t-x[j]];

            if(t==x[k+1])             

               f[t]=f[t]+1;

              }

       cout<<"共有不同的跳法種數為 "<<f[n]<<endl;

       return 0;

}

      (6)擴展2。

      例3中的遞推式實際上就是斐波那契數列的遞推式。這個數列的增長很快,46項以后就會超過整型數據的表數范圍。現設台階有1000級,按一步可上1級,也可上2級的上法,不同的上法種數是一個209位的整數,如何正確求得這個種數。

    (7)擴展2的編程思路。

      由於要求的數據超過了整數表示的范圍,需要進行高精度計算。

      如何表示和存放大數呢?一個簡單的方法就是:用數組存放和表示大數。一個數組元素,存放大數中的一位。

      我們日常書寫一個大整數,左側為其高位,右側為其低位,在計算中往往會因進位(carry)或借位(borrow)導致高位增長或減少,因此可以定義一個整型數組(int bignum[maxlen])從低位向高位實現大整數的存儲,數組的每個元素存儲大整數中的一位。

      顯然,在C++中,int類型(4個字節/32位計算機)數組元素存儲十進制的一位數字非常浪費空間,並且運算量也非常大,因此可將存儲優化為萬進制,即數組的每個元素存儲大整數數字的四位。(為什么選擇萬進制,而不選擇更大的進制呢?這是因為萬進制中的最大值9999相乘時得到的值是99980001,不會超過4個字節的存儲范圍,而十萬進制中的最大值99999相乘時得到的值是9999800001,超過4個字節的存儲范圍而溢出,從而導致程序計算錯誤。)

      在編寫程序代碼過程中作如下定義:

const int base=10000;

const int maxlen=50+1;

int bignum[maxlen];

      說明:base表示進制為萬進制,maxlen表示大整數的長度,1個元素能存儲4個十進制位,50個元素就存儲200個十進制位,而加1表示下標為0的元素另有它用,程序用作存儲當前大整數的位數。

      下面討論大整數的加法運算的實現。

      可以采用小學中曾學習的豎式加法。兩個大整數98240567043826400046和3079472005483080794進行加法運算,如圖2所示。

 

圖2  加法的計算過程

      從圖2中可以得知,做加法運算是從低位向高位進行,如果有進位,下一位進行相加時要加上進位,如果最高位已計算完還有進位,就要增加存儲結果的位數,保存起進位來。關於進位的處理,一般定義單獨變量carry進行存儲。

      (8)擴展2的源程序。

#include <iostream>

using namespace std;

const int base=10000;

const int maxlen=60+1;

void addition(int *bignum1, int *bignum2, int *bignum_ans);

void printbignum(int *bignum);

int main()

{

    int k,n,f[1001][maxlen];

    cout<<"請輸入台階總數n:";

    cin>>n;

    f[1][0]=1;  f[1][1]=1;

       f[2][0]=1;  f[2][1]=2;

    for(k=3;k<=n;k++)

      addition(f[k-1],f[k-2],f[k]);      // 按遞推關系實施遞推 

    cout<<"上法總數為 ";

       printbignum(f[n]);

    return 0;

}

void addition(int *bignum1, int *bignum2, int *bignum_ans)

{

       int carry=0;

    memset(bignum_ans,0,sizeof(int)*maxlen);

       bignum_ans[0]=bignum1[0]>bignum2[0]?bignum1[0]:bignum2[0];

       for(int pos=1; pos<=bignum_ans[0]; pos++){

              carry+=bignum1[pos]+bignum2[pos];

              bignum_ans[pos]=carry%base;

              carry/=base;

       }

       if(carry)

              bignum_ans[++bignum_ans[0]]=carry;

}

void printbignum(int *bignum)

{

       int *p=*bignum+bignum;

       cout<<*p--;

       cout.fill('0');        // 定義填充字符'0'

       while(p>bignum){ cout.width(4); cout<<*p--; }

       cout<<endl;

}

      如果要求2000級台階的不同上法種數(是一個418位的整數),上面的程序如何修改?如果求5000級甚至10000級的台階呢?請讀者自己動手做一做。在修改過程中,定義的二維數組造成棧溢出怎么辦?

      (9)擴展3——第39級台階。

      小明剛剛看完電影《第39級台階》,離開電影院的時候,他數了數禮堂前的台階數,恰好是39級! 站在台階前,他突然又想着一個問題:

       如果我每一步只能邁上1個或2個台階。先邁左腳,然后左右交替,最后一步是邁右腳,也就是說一共要走偶數步。那么,上完39級台階,有多少種不同的上法呢?

       (10)擴展3的編程思路。

       由於在上台階的過程中需要考慮不同的邁腳情況,因此需要定義二維數組c[40][2],數組元素c[i][0]表示上到第i級台階時最后一步是左腳的方法數,c[i][1] 表示上到第i級台階時最后一步是右腳的方法數。

由於每一步能上一級或兩級台階,因此可得遞推式:

c[i][0] = c[i-1][1]+c[i-2][1] (因為最后一步為左腳,倒數第二步肯定為右腳)

c[i][1] = c[i-1][0]+c[i-2][0] (因為最后一步為右腳,倒數第二步肯定為左腳)

初始情況為:

c[1][0]=1,第一步左腳直接邁一級台階。

c[1][1]=0,因為先邁左腳,右腳不可能落在第1級台階。

c[2][0]=1,第一步左腳直接邁兩級台階。

c[2][1]=1,左右腳各邁一級台階,右腳正好落在第2級台階。

(11)擴展3的源程序。

#include<iostream>

using namespace std;

int main()

{

    int i;

    int c[40][2];

    c[1][1] = 0;   c[1][0]=1;

    c[2][1] = 1;   c[2][0]=1;

    for(i = 3; i <= 39; ++i)

       {

        c[i][0] = c[i - 1][1] + c[i - 2][1];

        c[i][1] = c[i - 1][0] + c[i - 2][0];

    }

    cout<<c[39][1]<<endl;

    return 0;

}

     

       例2和例3采用的都是順推,下面看一個采用倒推求解的實例。

      【例4】猴子吃桃

      有一個猴子第一天摘下若干個桃子,當即吃了一半,還不過癮,又多吃了一個。第二天早上又將剩下的桃子吃掉一半,又多吃了一個。以后每天早上都吃了前一天剩下的一半后又多吃一個。到第10天早上想再吃時,見只剩下一個桃子了。

求第一天共摘了多少個桃子?

(1)編程思路。

設x[n]表示第n天吃桃子前的桃子數,則有x[10]=1。

由於, x[i]=x[i-1]-x[i-1]/2-1,

因此,遞推關系式為: x[i-1]=2*(x[i]+1)。倒推求得x[1]就是猴子所摘的桃子數。

(2)源程序。

#include <iostream>

using namespace std;

int  main()

{

    int a[11],i;

       a[10]=1;

       for (i=9;i>=1;i--)

              a[i]=2*(a[i+1]+1);

       cout<<a[1]<<endl;

    return  0;

}

      (3)問題擴展。

      有一猴子第一天摘下若干個桃子,當即吃了一半,還不過癮,又多吃了m個。第二天早上又將剩下的桃子吃掉一半,又多吃了m個。以后每天早上都吃了前一天剩下的一半后又多吃m個。到第n天早上想再吃時,見只剩下d個桃子了。

      求第一天共摘了多少個桃子(m,n,d均由鍵盤輸入,且1<=n,m,d<=20)?

      (4)擴展問題的源程序。

#include <iostream>

using namespace std;

int  main()

{

    int i,m,n,d;

    cin>>n>>m>>d;

       int *a;

       a=new int [n+1];

       a[n]=d;

       for (i=n-1;i>=1;i--)

              a[i]=2*(a[i+1]+m);

       cout<<a[1]<<endl;

    return  0;

}

 【例5】過河卒

      如圖3,在棋盤的A點有一個過河卒,需要走到目標B點。卒行走規則:可以向下、或者向右。同時在棋盤上的任一點有一個對方的馬(如圖4-4的C點),該馬所在的點和所有跳躍一步可達的點稱為對方馬的控制點。例如,圖4-4中C點上的馬可以控制9個點(圖中的P1,P2,…,P8 和C)。卒不能通過對方馬的控制點。

       棋盤用坐標表示,A點(0,0)、B點(n,m)(n,m為不超過50的整數,並由鍵盤輸入),同樣馬的位置坐標通過鍵盤輸入,並約定C<>A,同時C<>B。

       編寫程序,計算出卒從A點能夠到達B點的路徑的條數。

       例如,輸入6  6  3  2,輸出為17。

 

圖3  棋盤上的過河卒和對方的控制馬

(1)編程思路。

      在棋盤的A點(0,0)的過河卒要到達棋盤上的任意一點,只能從左邊和上邊兩個方向過來。因此,要到達某一點的路徑數,等於和它相鄰的左、上兩個點的路徑數和:

        F[i][j] = F[i-1][j] + F[i][j-1]。

      可以使用逐列(或逐行)遞推的方法來求出從起始頂點到終點的路徑數目,即使有障礙(將馬的控制點稱為障礙),這一遞推方法也完全適用,只要將到達該點的路徑數目設置為0即可,用F[i][j]表示到達點(i,j)的路徑數目,g[i][j]表示點(i,j)有無障礙,遞推方程如下:

F[0][0] = 1     初始點直接可達。

F[i][0] = F[i-1][0]   (i > 0,g[i][0] =0)     // 左邊界

F[0][j] = F[0][j-1]   (j > 0,g[0][j] = 0)    // 上邊界

F[i][j] = F[i-1][j] + F[i][j-1]   (i > 0,j > 0,g[x, y] = 0) // 遞推式

(2)源程序。

#include <iostream>

using namespace std;

int main() 

    int i,j,x,y,n,m,forbidden[51][51]; 

    int ans[51][51]; 

    int dx[8]={-2,-1,1,2,2,1,-1,-2};

       int dy[8]={1,2,2,1,-1,-2,-2,-1};

    cin>>n>>m>>x>>y; 

    for (i=0;i<=n;i++)

              for (j=0;j<=m;j++)

        {

                     forbidden[i][j]=0;

            ans[i][j]=0;

              }

    ans[0][0]=1; 

    forbidden[x][y]=1;

       for (i=0;i<8; i++)

        if (x+dx[i]>=0 && x+dx[i]<=n && y+dy[i]>=0 && y+dy[i]<=m)

                 forbidden[x+dx[i]][y+dy[i]]=1;

       for (i=1; i<=n; i++) 

        if (forbidden[i][0]==0) 

            ans[i][0]=1; 

        else break; 

    for (i=1; i<=m; i++) 

        if (forbidden[0][i]==0) 

            ans[0][i]=1; 

        else break; 

    for (i=1; i<=n; i++) 

        for (j=1; j<=m; j++) 

            if (forbidden[i][j]==0) 

                ans[i][j]=ans[i-1][j]+ans[i][j-1]; 

    cout<<ans[n][m]<<endl;

    return 0; 

【例6】學生隊列。

There are many students in PHT School. One day, the headmaster whose name is PigHeader wanted all students stand in a line. He prescribed that girl can not be in single. In other words, either no girl in the queue or more than one girl stands side by side.

The case n=4 (n is the number of children) is like FFFF, FFFM, MFFF, FFMM, MFFM, MMFF, MMMM Here F stands for a girl and M stands for a boy. The total number of queue satisfied the headmaster’s needs is 7.

Can you make a program to find the total number of queue with n children?

      (1)編程思路。

      設滿足要求的n個學生的隊列數為F(n)表示。

      簡單枚舉n較小的情況為:F(0)=1 (沒有人也是合法的,這個可以特殊處理,就像0的階乘定義為1一樣); 

        F(1)=1(一個男生M);F(2)=2;(兩個男生MM或兩個女生FF);F(3)=4(MMM、MFF、FFM或FFF)。

      當人數n大於3時,n個學生排隊可以看成是由n-1個學生排好隊后再加一個學生。按最后加的學生的性別分兩種情況:

      1)當加的最后一個學生是男孩M時候,前面n-1個隨便排出來,只要符合要求就可以,即方案數為F(n-1)。

      2)當加的最后一個學生是女孩F時候,第n-1個學生肯定是女孩F,這時候又有兩種情況:

      ① 前面n-2個學生按滿足要求的方法排好隊,后面加兩個女生一定也滿足要求,此時方案數為F(n-2)。

      ② 前面n-2個人不是滿足要求的隊列,加上兩個女生也有可能是合法的。當第n-2個學生是女孩而第n-3個學生是男孩時,后面加上兩個女孩可能合法。此時前n-4個學生的隊列必須滿足要求,即方案數為F(n-4)。

      綜上所述:總數F(n)= F(n-1)+ F(n-2)+ F(n-4)。

      (2)源程序。

#include <iostream>

using namespace std;

int main()

{

    int i,n,f[31];

    f[0]=1; f[1]=1; f[2]=2; f[3]=4; 

    for(i=4;i<=30;i++)

       f[i]=f[i-1]+f[i-2]+f[i-4];      // 按遞推關系實施遞推 

    while (cin>>n && n!=-1)

       {

              cout<<f[n]<<endl;

       }

    return 0;

}

【例7】棧

      棧是計算機中經典的數據結構,簡單的說,棧就是限制在一端進行插入刪除操作的線性表。

      棧有兩種最重要的操作,即pop(從棧頂彈出一個元素)和push(將一個元素進棧)。

      棧的重要性不言自明,任何一門數據結構的課程都會介紹棧。晶晶寧寧同學在復習棧的基本概念時,想到了一個書上沒有講過的問題,而她自己無法給出答案,所以需要你的幫忙。

      晶晶考慮的是這樣一個問題:一個操作數序列,從1,2,一直到n(圖4-5所示為1到3的情況),棧A的深度大於n。

現在可以進行兩種操作,

1)將一個數,從操作數序列的頭端移到棧的頭端(對應數據結構棧的push操作)。

2)將一個數,從棧的頭端移到輸出序列的尾端(對應數據結構棧的pop操作)。

      使用這兩種操作,由一個操作數序列就可以得到一系列的輸出序列。設棧的原始狀態如圖4所示,由123生成序列231的過程如圖5所示。

 

圖4  棧的原始狀態

 

圖5  采用棧生成序列231的過程

      請編寫程序,對輸入的n,計算並輸出由操作數序列1,2,…,n經過棧操作可能得到的輸出序列的總數。

      (1)編程思路。

       先模擬入棧、出棧操作,看看能否找出規律,設f(n)表示n個數通過棧操作后的排列總數,當n很小時,很容易模擬出f(1)=1,f(2)=2,f(3)=5。通過觀察,看不出它們之間的遞推關系,再分析n=4的情況,假設入棧前的排列為“4123”,按第一個數“4”在出棧后的位置進行分情況討論:

      1)若“4”最先輸出,剛好與n=3相同,總數為f(3)。

      2)若“4”第二個輸出,則在“4”的前只能是“1”,“23”在“4”的后面,這時可以分別看作是n=1和n=2時的兩種情況,排列數分別為f(1)和f(2),所以此時的總數為f(1)*f(2)。

      3)若“4”第三個輸出,則“4”的前面二個數為“12”,后面一個數為“3”,組成的排列總數為f(2)*f(1)。

      4)若“4”第四個輸出,與情況(1)相同,總數為f(3)。

     所以有:f(4)=f(3)+f(1)*f(2)+f(2)*f(1)+f(3)。

      若設0個數通過棧后的排列總數為:f(0)=1;

      上式可變為:f(4)=f(0)*f(3)+f(1)*f(2)+f(2)*f(1)+f(3)*f(0)。

再進一步推導,不難推出遞推式:

f(n)=f(0)*f(n-1)+f(1)*f(n-2)+…+f(n-1)*f(0);

即f(n)=      (n>=1)

初始值:f(0)=1;

有了以上遞推式,就很容易用遞推法寫出程序。

(2)源程序。

#include <iostream>

using namespace std;

int  main()

{

    int a[19]={0},n,i,j;

       cin>>n;

       a[0]=1;

       for (i=1;i<=n;i++)

              for (j=0;j<=i-1;j++)

                     a[i]=a[i]+a[j]*a[i-j-1];

       cout<<a[n]<<endl;

    return  0;

}

      (3)用公式直接求解。

      實際上,一個棧的入棧序列為1,2,3,…,n,其不同的出棧序列的種數是一個卡特蘭數。

      卡特蘭數是組合數學中一個常出現在各種計數問題中出現的數列。該數列如下:

C0=1,C1=1,C2=2,C3=5,C4=14,C5=42,C6=132,C7=429,C8=1430,

C9=4862,C10=16796,C11=58786,C12=208012,……。

卡塔蘭數的一般項公式為:

 

        Cn的另一個表達形式為:

 

 

下面簡要描述一下這個公式的推導情況。

      對於1~n中的每一個數來說,必須入棧一次、出棧一次。設入棧為狀態“1”,出棧為狀態“0”。n個數的所有狀態對應n個1和n個0組成的2n位二進制數。由於等待入棧的操作數按照1~n的順序排列、入棧的操作數b大於等於出棧的操作數a(a≤b),因此輸出序列的總數目等於由左至右掃描由n個1和n個0組成的2n位二進制數,1的累計數不小於0的累計數的方案種數。

      在2n位二進制數中填入n個1的方案數為c(2n,n),不填1的其余n位自動填0。從中減去不符合要求(由左至右掃描,0的累計數大於1的累計數)的方案數即為所求。

      不符合要求的數的特征是由左至右掃描時,必然在某一奇數位2m+1位上首先出現m+1個0的累計數和m個1的累計數,此后的2(n-m)-1位上有n-m個 1和n-m-1個0。如果把后面這2(n-m)-1位上的0和1互換,使之成為n-m個0和n-m-1個1,結果得1個由n+1個0和n-1個1組成的2n位數,即一個不合要求的數對應於一個由n+1個0和n-1個1組成的排列。反過來,任何一個由n+1個0和n-1個1組成的2n位二進制數,由於0的個數多2個,2n為偶數,故必在某一個奇數位上出現0的累計數超過1的累計數。同樣在后面部分0和1互換,使之成為由n個0和n個1組成的2n位數,即n+1個0和n-1個1組成的2n位數必對應一個不符合要求的數。因而不合要求的2n位數與n+1個0,n-1個1組成的排列一一對應。 即不符合要求的方案數為c(2n,n+1)。

       由此得出,輸出序列的總數目= c(2n,n)-c(2n,n+1)=c(2n,n)/(n+1)。

      (4)采用公式直接求解的源程序。

#include <iostream>

using namespace std;

int combin(int n,int m)   // 求組合數C(n,m)

{

       int p=1,i,t;

    t=n;

       for (i=1;i<=m;i++)

       {

        p=p*t/i;

              t--;

       }

       return p;

}

int main() 

    int n; 

    cin>>n; 

    cout<<combin(2*n,n)/(n+1)<<endl;

    return 0; 

 

【例8】整數划分問題

      正整數s(簡稱為和數)的划分(又稱拆分)是把s分成為若干個正整數(簡稱為零數或部分)之和,划分式中允許零數重復,且不記零數的次序。

      試求s=12共有多少個不同的划分式?展示出所有這些划分式。

(1)編程思路。

整數划分問題可采用多種方法解決,下面我們采用遞推的法來完成。

為了建立遞推關系,先對和數k較小時的划分式作觀察歸納:

k=2:1+1;2    2種划分式

k=3:1+1+1;1+2;3      3種划分式

k=4:1+1+1+1;1+1+2;1+3;2+2;4     5種划分式

k=5:1+1+1+1+1;1+1+1+2;1+1+3;1+2+2;1+4;2+3;5    7種划分式

      由以上各划分看到,除和數本身k=k這一特殊划分式外,其它每一個划分式至少為兩項之和。約定在所有划分式中零數作非降序排列,探索和數k的划分式與和數k−1的划分式存在以下遞推關系:

      1)在所有和數k−1的划分式前加零數1都是和數k的划分式。

      2)和數k−1的划分式的前兩個零數作比較,如果第1個零數x1小於第2個零數x2,則把第1個零數加1后成為和數k的划分式。

定義一個三維數組a,a[k][j][i]表示為和數k的第j個划分式的第i個數。

遞推的初始條件為:a[2][1][1]=1; a[2][1][2]=1; a[2][2][1]=2。

根據遞推關系,實施遞推:

1)實施在k−1所有划分式前加1操作。

a[k][j][1]=1;

for (t=2;t<=k;t++)         

        a[k][j][t]=a[k−1][j][t−1];          //  k−1的第t−1項變為k的第t項

2)若k−1划分式第1項小於第2項,第1項加1,變為k的第i個划分式

       if(a[k-1][j][1]<a[k-1][j][2])  // 若k-1划分式第1項小於第2項 

         { i++;                     // 第1項加1為k的第i個划分式的第1項 

          a[k][i][1]=a[k-1][j][1]+1;    

          for(t=2;t<=k-1;t++)

             a[k][i][t]=a[k-1][j][t];

          }

(2)源程序及運行結果。

#include <iostream>

#include <iomanip>

using namespace std;

int main()

{

       int s,i,j,k,t,cnt;

    static int a[21][800][21]={0};

    cout<<"input s(s<=20):";

       cin>>s;

    a[2][1][1]=1; a[2][1][2]=1; a[2][2][1]=2;

    cnt=2;

    for(k=3;k<=s;k++)

    {

              for(j=1;j<=cnt;j++)

        {

                     a[k][j][1]=1;          

            for(t=2;t<=k;t++)    // 實施在k-1所有划分式前加1操作 

               a[k][j][t]=a[k-1][j][t-1];    

        }

        for(i=cnt,j=1;j<=cnt;j++)

           if(a[k-1][j][1]<a[k-1][j][2])  // 若k-1划分式第1項小於第2項 

                 {

                        i++;              // 第1項加1為k的第i個划分式的第1項 

               a[k][i][1]=a[k-1][j][1]+1;    

               for(t=2;t<=k-1;t++)

                  a[k][i][t]=a[k-1][j][t];

           }

        i++;   a[k][i][1]=k;    // k的最后一個划分式為:k=k 

        cnt=i;

       }

    for(j=1;j<=cnt;j++)        // 輸出s的所有划分式 

    {

              cout<<setw(4)<<j<<": "<<s<<"="<<a[s][j][1];

        i=2;

        while (a[s][j][i]>0)

        {

                     cout<<"+"<<a[s][j][i];

                     i++;

              }

        cout<<endl;

       }

    return 0;

}

      (3)整數划分的優化。

      上面遞推算法的時間復雜度與空間復雜度為O(n2u),其中u為n划分式的個數。由於u隨n增加非常快,難以估算其數量級,因此算法的時間復雜度與空間復雜度是很高的。

      下面我們對整數划分進行集智。

      分析上面使用三維數組a[k][j][i]完成的遞推過程。當由k−1的划分式推出k的划分式時,k−1以前的數組單元已完全閑置。因此,可以考慮把三維數組a[k][j][i]改進為二維數組a[j][i]。進行和數為k的遞推前,數組元素a[j][i]表示和數是k−1的已有划分式。根據遞推關系推出和數為k的划分式:

       1)把a[j][i]依次存儲到a[j][i+1],加上第一項a[j][1]=1;這樣完成在k−1的所有划分式前加1的操作,轉化為k的划分式。

for(t=i;t>=1;t−−)

       a[j][t+1]=a[j][t];

a[j][1]=1;

       2)對已轉化的cnt個划分式逐個檢驗,若其第2個數小於第3個數(相當於k−1時的第1個數小於第2個數),則把第2個數加1,去除第一個數后,作為k時增加的一個划分式,為第t個(t從cnt開始,每增加一個划分式,t增1)划分式。

for(t=u,j=1;j<=u;j++)

  if(a(j,2)<a(j,3))     // 若k−1划分式第1項小於第2項 

    {t++;

     a(t,1)=a(j,2)+1;  // 第1項加1 作為k的第t個划分式的第1項

     i=3;

     while(a(j,i)>0)

        {a(t,i−1)=a(j,i);i++;}

     }   

      (4)優化后的源程序。

#include <iostream>

#include <iomanip>

using namespace std;

int main()

{

       int s,i,j,k,t,cnt;

    static int a[1600][25]={0};

    cout<<"input s(s<=24):";

    cin>>s;

    a[1][1]=1;a[1][2]=1;a[2][1]=2;

       cnt=2;

    for(k=3;k<=s;k++)

    {

              for(j=1;j<=cnt;j++)

        {

                     i=k-1;          

            for(t=i;t>=1;t--)         // 實施在k-1所有划分式前加1操作 

              a[j][t+1]=a[j][t];

            a[j][1]=1;

              }

        for(t=cnt,j=1;j<=cnt;j++)

          if(a[j][2]<a[j][3])   // 若k-1划分式第1項小於第2項 

                {

                       t++;

              a[t][1]=a[j][2]+1;   // 第1項加1 

              i=3;

              while(a[j][i]>0)

              {

                              a[t][i-1]=a[j][i];

                              i++;

                       }

                }

        t++;  a[t][1]=k;             // 最后一個划分式為:k=k 

        cnt=t;

       }

    for(j=1;j<=cnt;j++)        // 輸出s的所有划分式 

    {

              cout<<setw(4)<<j<<": "<<s<<"="<<a[j][1];

        i=2;

        while (a[j][i]>0)

        {

                     cout<<"+"<<a[j][i];

                     i++;

              }

        cout<<endl;

       }

    return 0;

}

      改進的遞推程序把原有的三維數組優化為二維數組,降低了算法的空間復雜度,拓展了算法的求解范圍。

      但是,由於划分式的數量cnt隨和數s增加得相當迅速。例如,和數為24時有1575種划分。因此盡管改進為二維數組,求解的和數s也不可能太大。


免責聲明!

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



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