遞推算法
概述
遞推法是一種重要的數學方法,在數學的各個領域中都有廣泛的運用,也是計算機用於數值計算的一個重要算法。
這種算法特點是:一個問題的求解需一系列的計算,在已知條件和所求問題之間總存在着某種相互聯系的關系,在計算時,如果可以找到前后過程之間的數量關系(即遞推式),那么,從問題出發逐步推到已知條件,此種方法叫逆推。
無論順推還是逆推,其關鍵是要找到遞推式。
這種處理問題的方法能使復雜運算化為若干步重復的簡單運算,充分發揮出計算機擅長於重復處理的特點。
應用場景
遞推算法的首要問題是得到相鄰的數據項間的關系(即遞推關系)。遞推算法避開了求通項公式的麻煩,把一個復雜的問題的求解,分解成了連續的若干步簡單運算。一般說來,可以將遞推算法看成是一種特殊的迭代算法。
五種典型的遞推關系
Ⅰ.Fibonacci數列
在所有的遞推關系中,Fibonacci數列應該是最為大家所熟悉的。在最基礎的程序設計語言Logo語言中,就有很多這類的題目。而在較為復雜的Basic、Pascal、C語言中,Fibonacci數列類的題目因為解法相對容易一些,逐漸退出了競賽的舞台。可是這不等於說Fibonacci數列沒有研究價值,恰恰相反,一些此類的題目還是能給我們一定的啟發的。
Fibonacci數列的代表問題是由意大利著名數學家Fibonacci於1202年提出的“兔子繁殖問題”(又稱“Fibonacci問題”)。
問題的提出:有雌雄一對兔子,假定過兩個月便可繁殖雌雄各一的一對小兔子。問過n個月后共有多少對兔子?
思路:
設滿x個月共有兔子Fx對,其中當月新生的兔子數目為Nx對。第x-1個月留下的兔子數目設為Fx-1對。則:
Fx=Nx+ Fx-1,Nx=Fx-2
(即第x-2個月的所有兔子到第x個月都有繁殖能力了)
∴ Fx=Fx-1+Fx-2 邊界條件:F0=0,F1=1
由上面的遞推關系可依次得到
F2=F1+F0=1,F3=F2+F1=2,F4=F3+F2=3,F5=F4+F3=5,……。
Fabonacci數列常出現在比較簡單的組合計數問題中,例如以前的競賽中出現的“骨牌覆蓋”問題。在優選法中,Fibonacci數列的用處也得到了較好的體現。
Ⅱ.Hanoi塔問題
問題的提出:Hanoi塔由n個大小不同的圓盤和三根木柱a,b,c組成。開始時,這n個圓盤由大到小依次套在a柱上,如圖3-11所示。
要求把a柱上n個圓盤按下述規則移到c柱上:
(1)一次只能移一個圓盤;
(2)圓盤只能在三個柱上存放;
(3)在移動過程中,不允許大盤壓小盤。
問將這n個盤子從a柱移動到c柱上,總計需要移動多少個盤次?
思路:
設hn為n個盤子從a柱移到c柱所需移動的盤次。
顯然,當n=1時,只需把a 柱上的盤子直接移動到c柱就可以了,故h1=1。
當n=2時,先將a柱上面的小盤子移動到b柱上去;然后將大盤子從a柱移到c 柱;最后,將b柱上的小盤子移到c柱上,共記3個盤次,故h2=3。
以此類推,當a柱上有n(n2)個盤子時,總是先借助c柱把上面的n-1個盤子移動到b柱上,然后把a柱最下面的盤子移動到c柱上;
再借助a柱把b柱上的n-1個盤子移動到c柱上;總共移動hn-1+1+hn-1個盤次。
∴hn=2hn-1+1 邊界條件:h1=1
Ⅲ.平面分割問題
問題的提出:設有n條封閉曲線畫在平面上,而任何兩條封閉曲線恰好相交於兩點,且任何三條封閉曲線不相交於同一點,問這些封閉曲線把平面分割成的區域個數。
思路:
設an為n條封閉曲線把平面分割成的區域個數。 由圖3-13可以看出:a2-a1=2;a3-a2=4;a4-a3=6。
從這些式子中可以看出an-an-1=2(n-1)。當然,上面的式子只是我們通過觀察4幅圖后得出的結論,它的正確性尚不能保證。
下面不妨讓我們來試着證明一下。當平面上已有n-1條曲線將平面分割成an-1個區域后,第n-1條曲線每與曲線相交一次,就會增加一個區域,因為平面上已有了n-1條封閉曲線,且第n條曲線與已有的每一條閉曲線恰好相交於兩點,且不會與任兩條曲線交於同一點,故平面上一共增加2(n-1)個區域,加上已有的an-1個區域,一共有an-1+2(n-1)個區域。所以本題的遞推關系是an=an-1+2(n-1),邊界條件是a1=1。
平面分割問題是競賽中經常觸及到的一類問題,由於其靈活多變,常常感到棘手
Ⅳ.Catalan數
Catalan數首先是由Euler在精確計算對凸n邊形的不同的對角三角形剖分的個數問題時得到的,它經常出現在組合計數問題中。
問題的提出:在一個凸n邊形中,通過不相交於n邊形內部的對角線,把n邊形拆分成若干三角形,不同的拆分數目用hn表示,hn即為Catalan數。例如五邊形有如下五種拆分方案(圖3-14),故h5=5。求對於一個任意的凸n邊形相應的hn。
思路:
Catalan數是比較復雜的遞推關系,尤其在競賽的時候,選手很難在較短的時間里建立起正確的遞推關系。當然,Catalan數類的問題也可以用搜索的方法來完成,但是,搜索的方法與利用遞推關系的方法比較起來,不僅效率低,編程復雜度也陡然提高。
Ⅴ.第二類Stirling數
在五類典型的遞推關系中,第二類Stirling是最不為大家所熟悉的。也正因為如此,我們有必要先解釋一下什么是第二類Strling數。
【定義2】n個有區別的球放到m個相同的盒子中,要求無一空盒,其不同的方案數用S(n,m)表示,稱為第二類Stirling數。
下面就讓我們根據定義來推導帶兩個參數的遞推關系——第二類Stirling數。
解:設有n個不同的球,分別用b1,b2,……bn表示。從中取出一個球bn,bn的放法有以下兩種:
①bn獨自占一個盒子;那么剩下的球只能放在m-1個盒子中,方案數為S2(n-1,m-1);
②bn與別的球共占一個盒子;那么可以事先將b1,b2,……bn-1這n-1個球放入m個盒子中,然后再將球bn可以放入其中一個盒子中,方案數為mS2(n-1,m)。
綜合以上兩種情況,可以得出第二類Stirling數定理:
【定理】S2(n,m)=mS2(n-1,m)+S2(n-1,m-1) (n>1,m1)
邊界條件可以由定義2推導出:
S2(n,0)=0;S2(n,1)=1;S2(n,n)=1;S2(n,k)=0(k>n)。
第二類Stirling數在競賽中較少出現,但在競賽中也有一些題目與其類似,甚至更為復雜。讀者不妨自己來試着建立其中的遞推關系。
小結:通過上面對五種典型的遞推關系建立過程的探討,可知對待遞推類的題目,要具體情況具體分析,通過找到某狀態與其前面狀態的聯系,建立相應的遞推關系。
例題講解
【例1】數字三角形。如下所示為一個數字三角形。請編一個程序計算從頂到底的某處的一條路徑,使該路徑所經過的數字總和最大。只要求輸出總和。
1、 一步可沿左斜線向下或右斜線向下走;
2、 三角形行數小於等於100;
3、 三角形中的數字為0,1,…,99;
測試數據通過鍵盤逐行輸入,如上例數據應以如下所示格式輸入:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
【算法分析】
此題解法有多種,從遞推的思想出發,設想,當從頂層沿某條路徑走到第i層向第i+1層前進時,我們的選擇一定是沿其下兩條可行路徑中最大數字的方向前進,為此,我們可以采用倒推的手法,設a[i][j]
存放從i,j 出發到達n層的最大值,則a[i][j]=max{a[i][j]+a[i+1][j],a[i][j]+a[i+1][j+1]}
,a[1][1]
即為所求的數字總和的最大值。
實現代碼:
【參考程序】
#include<iostream>
using namespace std;
int main()
{
int n,i,j,a[101][101];
cin>>n;
for (i=1;i<=n;i++)
for (j=1;j<=i;j++)
cin>>a[i][j]; //輸入數字三角形的值
for (i=n-1;i>=1;i--)
for (j=1;j<=i;j++)
{
if (a[i+1][j]>=a[i+1][j+1]) a[i][j]+=a[i+1][j]; //路徑選擇
else a[i][j]+=a[i+1][j+1];
}
cout<<a[1][1]<<endl;
}
【例2】 有 2χn的一個長方形方格,用一個1*2的骨牌鋪滿方格。
編寫一個程序,試對給出的任意一個****n(n>0), 輸出鋪法總數。
【算法分析】
(1)面對上述問題,如果思考方法不恰當,要想獲得問題的解答是相當困難的。可以用遞推方法歸納出問題解的一般規律。
(2)當n=1時,只能是一種鋪法,鋪法總數有示x1=1。
(3)當n=2時:骨牌可以兩個並列豎排,也可以並列橫排,再無其他方法,如下左圖所示,因此,鋪法總數表示為x2=2;
(4)當n=3時:骨牌可以全部豎排,也可以認為在方格中已經有一個豎排骨牌,則需要在方格中排列兩個橫排骨牌(無重復方法),若已經在方格中排列兩個橫排骨牌,則必須在方格中排列一個豎排骨牌。如上右圖,再無其他排列方法,因此鋪法總數表示為x3=3。
由此可以看出,當n=3時的排列骨牌的方法數是n=1和n=2排列方法數的和。
(5)推出一般規律:對一般的n,要求xn可以這樣來考慮,若第一個骨牌是豎排列放置,剩下有n-1個骨牌需要排列,這時排列方法數為xn-1;若第一個骨牌是橫排列,整個方格至少有2個骨牌是橫排列(1*2骨牌),因此剩下n-2個骨牌需要排列,這是骨牌排列方法數為xn-2。從第一骨牌排列方法考慮,只有這兩種可能,所以有:
xn=xn-1+xn-2 (n>2)
x1=1
x2=2
xn=xn-1+xn-2就是問題求解的遞推公式。任給n都可以從中獲得解答。例如n=5,
x3=x2+x1=3
x4=x3+x2=5
x5=x4+x3=8
實現代碼:
下面是輸入n,輸出x1~xn的c++程序:
#include<iostream>
using namespace std;
int main()
{
int n,i,j,a[101];
cout<<"input n:"; //輸入骨牌數
cin>>n;
a[1]=1;a[2]=2;
cout<<"x[1]="<<a[1]<<endl;
cout<<"x[2]="<<a[2]<<endl;
for (i=3;i<=n;i++) //遞推過程
{
a[i]=a[i-1]+a[i-2];
cout<<"x["<<i<<"]="<<a[i]<<endl;
}
}
下面是運行程序輸入 n=30,輸出的結果:
input n: 30
x[1]=1
x[2]=2
x[3]=3
........
x[29]=832040
x[30]=1346269
問題的結果就是有名的斐波那契數。
【例3】棋盤格數
設有一個N*M方格的棋盤( l≤ N≤100,1≤M≤100)。求出該棋盤中包含有多少個正方形、多少個長方形(不包括正方形)。
例如:當 N=2, M=3時:
正方形的個數有8個:即邊長為1的正方形有6個;邊長為2的正方形有2個。
長方形的個數有10個:即21的長方形有4個:12的長方形有3個:31的長方形有2個:32的長方形有1個:
程序要求:輸入:N,M
輸出:正方形的個數與長方形的個數
如上例:輸入:2 3
輸出:8 10
【算法分析】
1.計算正方形的個數s1
邊長為1的正方形個數為n*m
邊長為2的正方形個數為(n-1)*(m-1)
邊長為3的正方形個數為(n-2)*(m-2)
…………
邊長為min{n,m}的正方形個數為(m-min{n,m}+1)*(n-min{n,m}+1)
根據加法原理得出:
2.長方形和正方形的個數之和s
寬為1的長方形和正方形有m個,寬為2的長方形和正方形有m-1個,┉┉,寬為m的長方形和正方形有1個;
長為1的長方形和正方形有n個,長為2的長方形和正方形有n-1個,┉┉,長為n的長方形和正方形有1個;
根據乘法原理
3.長寬不等的長方形個數s2
顯然,s2=s-s1=
實現代碼:
由此得出算法:
#include<iostream>
using namespace std;
int main()
{
int n,m;
cin>>m>>n;
int m1=m,n1=n,s1=m*n; //計算正方形的個數s1
while (m1!=0&&n1!=0)
{
m1--;n1--;
s1+=m1*n1;
}
int s2=((m+1)*(n+1)*m*n)/4-s1; // 計算長方形的個數s2
cout<<s1<<" "<<s2<<endl;
}
【例4】昆蟲繁殖
【問題描述】
科學家在熱帶森林中發現了一種特殊的昆蟲,這種昆蟲的繁殖能力很強。每對成蟲過x個月產y對卵,每對卵要過兩個月長成成蟲。假設每個成蟲不死,第一個月只有一對成蟲,且卵長成成蟲后的第一個月不產卵(過X個月產卵),問過Z個月以后,共有成蟲多少對?0=<X<=20,1<=Y<=20,X=<Z<=50
【輸入格式】
x,y,z的數值
【輸出格式】
過Z個月以后,共有成蟲對數
【輸入樣例】
1 2 8
【輸出樣例】
37
實現代碼:
【參考程序】
#include<iostream>
using namespace std;
int main()
{
long long a[101]={0},b[101]={0},i,j,x,y,z;
cin>>x>>y>>z;
for(i=1;i<=x;i++){a[i]=1;b[i]=0;}
for(i=x+1;i<=z+1;i++) //因為要統計到第z個月后,所以要for到z+1
{
b[i]=y*a[i-x];
a[i]=a[i-1]+b[i-2];
}
cout<<a[z+1]<<endl;
return 0;
}
【例5】位數問題
【問題描述】
在所有的N位數中,有多少個數中有偶數個數字3?由於結果可能很大,你只需要輸出這個答案對12345取余的值。
【輸入格式】
讀入一個數N
【輸出格式】
輸出有多少個數中有偶數個數字3。
【輸入樣例】
2
【輸出樣例】
73
【數據規模】
1<=N<=1000
【樣例說明】
在所有的2位數字,包含0個3的數有72個,包含2個3的數有1個,共73個
算法分析
方法一:排列組合(但需要運用動態規划)。
可以列出公式,在n個格子中放x個3(其中x為偶數,包括0).。
c(n,x)9(n-x)-c(n-1,x)*9(n-x-1) 含義為在n個格子中取x個3,且不考慮第一位的特殊情況為c(n,x)9^(n-x)。
而第一位為0的情況,為c(n-1,x)*9^(n-x-1),兩者減下,就為答案。
方法二:遞推
考慮這種題目,一般來說都是從第i-1位推導第i位,且當前位是取偶數還是取奇數的。
恍然大悟.可以用f[i][0]表示前i位取偶數個3有幾種情況,f[i][1]表示前i位取奇數個3有幾種情況。
則狀態轉移方程可以表示為:
f[i][0]=f[i-1][0]*9+f[i-1][1];f[i][1]=f[i-1][0]+f[i-1][1]*9;
邊界條件:f[1][1]=1;f[1][0]=9;
實現代碼:
【參考程序】
#include<iostream>
using namespace std;
int main()
{
int f[1001][2],n,i,x;
cin>>n;
f[1][1]=1;f[1][0]=9;
for(i=2;i<=n;i++)
{
x=f[1][0];
if(i==n)x--;
f[i][0]=(f[i-1][0]*x+f[i-1][1])%12345;
f[i][1]=(f[i-1][1]*x+f[i-1][0])%12345;
}
cout<<f[n][0];
return 0;
}
【例6】過河卒(Noip2002)
【問題描述】
棋盤上A點有一個過河卒,需要走到目標B點。卒行走的規則:可以向下、或者向右。同時在棋盤上的任一點有一個對方的馬(如C點),該馬所在的點和所有跳躍一步可達的點稱為對方馬的控制點,如圖3-1中的C點和P1,……,P8,卒不能通過對方馬的控制點。棋盤用坐標表示,A點(0,0)、B點(n, m) (n,m為不超過20的整數),同樣馬的位置坐標是需要給出的,C≠A且C≠B。現在要求你計算出卒從A點能夠到達B點的路徑的條數。
算法分析
跳馬是一道老得不能再老的題目,我想每位編程初學者都學過,可能是在學回溯或搜索等算法的時候,很多書上也有類似的題目,一些比賽中也出現過這一問題的變形(如NOIP1997初中組的第三題)。有些同學一看到這條題目就去搜索,即使你編程調試全通過了,運行時你也會發現:當n,m=15就會超時。
其實,本題稍加分析就能發現,要到達棋盤上的一個點,只能從左邊過來(我們稱之為左點)或是從上面過來(我們稱之為上點),所以根據加法原理,到達某一點的路徑數目,就等於到達其相鄰的上點和左點的路徑數目之和,因此我們可以使用逐列(或逐行)遞推的方法來求出從起點到終點的路徑數目。障礙點(馬的控制點)也完全適用,只要將到達該點的路徑數目設置為0即可。
用F[i][j]表示到達點(i,j)的路徑數目,g[i][j]表示點(i, j)有無障礙,g[i][j]=0表示無障礙,g[i][j]=1表示有障礙。
則,遞推關系式如下:
F[i][j] = F[i-1][j] + F[i][j-1]
//i>0且j>0且g[i][j]= 0
遞推邊界有4個:
F[i][j] = 0 //g[i][j] = 1
F[i][0] = F[i-1][0] //i > 0且g[i][j] = 0
F[0][j] = F[0][j-1] //j > 0且g[i][j] = 0
F[0][0] = 1
考慮到最大情況下:n=20,m=20,路徑條數可能會超過231-1,所以要用高精度。
【例7】郵票問題
【問題描述】
設有已知面額的郵票m種,每種有n張,用總數不超過n張的郵票,能從面額1開始,最多連續組成多少面額。(1≤m≤100,1≤n≤100,1≤郵票面額≤255)
【輸入格式】
第一行:m,n的值,中間用一空格隔開。
第二行:A[1..m](面額),每個數中間用一空格隔開。
【輸出格式】
連續面額數的最大值
【輸入樣例】stamp.in
3 4
1 2 4
【輸出樣例】samp.out
14
算法分析
一看到這個題目,給人的第一感覺是用回溯算法,從面額1開始,每種面額都用回溯進行判斷,算法復雜度並不高,但是當m,n取到極限值100時,程序明顯超時,因此,回溯算法在這里並不可取。 能否用遞推完成呢?我們有一個思路:從面額1開始,建立遞推關系方程,就用范例來說吧,面額1,2,4只用1張郵票行了,面額3可以表示為面額1,2的郵票和1+1=2,面額5有兩種表示方式min(面額1+面額4,面額2+面額3),照此類推,遞推關系方程不難建立,就拿郵票問題來說,以下是遞推的一種方法:
#include<iostream>
using namespace std;
int n,m,i,j,k;
int c[256]; //面額
int a[31001]; //遞推數組
bool b1;
void readfile() //讀入數據
{
cin >> m >> n;
b1 = true;
for (i = 1; i <= m; i++)
{
cin >> c[i];
if (c[i] == 1) b1 = false;
}
}
void work()
{
if (b1 == true) cout << "MAX=0"; //不存在面額1時輸出無解
else
{
i = 1; a[i] = 1;
do
{
i++;
for (j = 1; j <= m; j++)
if (((i % c[j] == 0) && ((i / c[j]) < a[i])) || (a[i] == 0))
a[i] = i / c[j]; //判斷它能否被題目給定面額整除
for (j = 1; j <= i/2; j++)
if (a[j] + a[i-j] < a[i])
a[i] = a[j] + a[i-j]; //尋找(1<=j<=i),使a[j]+a[i-j]值最小
}
while ((a[i] <= n) && (a[i] != 0));
cout << i-1; //輸出
}
}
int main ( )
{
readfile() ;
work();
return 0;
}
這種遞推方法雖然簡單,由於1<=郵票面額<=255,1<=n<=100,因此MAX值最多可達到25500,25500次循環里必定還有嵌套循環,因此算法不加優化,很難在規定時間內得出最優值。這就需要遞推的算法優化。 一味遞推不尋求算法優化,速度較之搜索提高不少,但一旦數據規模過大,很難在規定時間內得出最優值。 這種遞推方法原理是:對於某種要求得到的面額,判斷它能否被題目給定面額整除,再尋找(1<=j<=i),使A[j]+A[i-j]值最小,求出湊成某種面額最少郵票數,算法雖然簡單,但還可以進一步優化。何不將用m種面額郵票作循環,建立遞推關系式:A[i]=MAX(A[I-C[j]]+1),於是當取到極限值時,程序減少了約1.6*10^8次循環,遞推優化作用不言而喻。
下面是改進后的程序:
#include<iostream>
#include<cstring>
using namespace std;
int x[256];
int pieces[30001];
int m,n,i,j;
int main()
{
cin >> m >> n;
for (i = 1; i <= m; i++)
cin >> x[i];
memset(pieces,0,sizeof(pieces));
int maxx = 0;
do //遞推循環
{
maxx++;
for (i = 1; i <= m; i++)
if (maxx - x[i] >= 0)
{ //循環,建立遞推關系式PIECES[i]=MAX(PIECES[I-X[j]]+1)
if (pieces[maxx] == 0) pieces[maxx] = pieces[maxx-x[i]] + 1;
if (pieces[maxx]>pieces[maxx-x[i]]+1) pieces[maxx] = pieces[maxx-x[i]]+1;
}
if ((pieces[maxx] == 0) || (pieces[maxx] > n))
{
cout << maxx - 1;
break;
}
}
while (true);
return 0;
}