下面通過一些典型實例及其擴展來討論遞推法的應用。
【例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也不可能太大。