遞迴+記憶化搜尋
邊界條件與遞迴方程是遞迴函式的兩個要素。
1)階乘函式
直接打板子:
Int fac(int n)
{
If (n==0) return 1;
Else return n*fac(n-1);
}
這裡,第一句的if是邊界條件,第二句是遞迴方程。0的階乘為1,n的階乘為(n-1)的階乘再乘n。
2)漢諾塔問題
本來覺得它是個不可解決的難題,實則不然,區區遞迴即可解決:
Void move(int n,int a,int b,int c)
{
If (n==1) cout<<a<<”->”<<c<<endl;
Else
{
Move(n-1,a,c,b);
Cout<<a<<”->”<<c<<endl;
Move(n-1,b,a,c);
}
這個問題,是個問題。首先,明確一下這裡形參的含義:n表示現在有n個盤需要移動,a表示盤子現在在哪裡,b表示中間的媒介,c表示盤子要去哪裡。然後,這裡的if語句還是邊界,就是當n=1的時候,也就是現在只有一個盤子的時候,直接移動並輸出路徑。底下else語句中的是主體:因為當前有n個盤子需要移動,而最終目的是讓第n號盤子從a移動到c,因此我們分為三步:①把上面的n-1個盤子看做一個整體,從a先移動到b;②把第n號盤子從a移動到c,輸出路徑;③把那n-1個盤子再從b移動到c。
遞迴函式的執行流程分為兩個階段:遞迴前進段、遞迴返回段。
這可以說一個很神奇的東西。總的來說就是:遞迴前進段就是你按它函式的順序執行下去,直到碰到邊界;遞迴返回段就是當你碰到了邊界,你把剛剛所有遞迴巢狀的函式值算出來,一步一步返回值,返回給最初的值。
這裡有一個我很容易搞錯的東西,也很難理解的東西:沒每遞迴呼叫一次函式,系統就會生成一個新的函式例項。這些函式例項有同名的引數和區域性變數,但各自獨立,互不干擾。流程執行到哪一層,那一層的變數就起作用,返回上一層,就釋放掉低層的同名變數。這個需要深刻理解一下。
3)斐波那契數列
板子很簡單:
Int f(int n)
{
If (n<=2) return 1;
Else return f(n-2)+f(n-1);
}
接下來學一個記憶化搜尋(是個難點)
來看一下斐波那契數列的板子:
Int f[max]={0};
Int f(int n)
{
If (f[n]!=0) return f[n];
If (n<=2) return f[n]=1;
Else return f[n]=f(n-2)+f(n-1);
}
可以這麼理解:你先打一個不用記憶化搜尋的板子,然後把它轉換為記憶化搜尋。記憶化搜尋的話,要多加一個數組,用來儲存已經求過的函式值。然後,你再在遞迴函式裡面先加一句話——if (這個陣列的值不為0) return 這個值;另外,後面的遞迴返回值裡在前面多加一個——“陣列=”遞迴返回值。也不知道看不看得懂,反正我大概知道是怎麼用的了。
還是斐波那契數列,板子還可以這樣打:
Int t1,t2,r;
T1=t2=r=1;
For (int i=3;i<=n;++i)
{
R=t1+t2;
T1=t2;
T2=r;
}
Cout<<r;
這種方法連陣列都不用。
遞迴構建有三個條件:1)引數;2)邊界;3)範圍。
據此來分析遞迴過程如何寫。
1) 引數:明確引數的意義以及當前的值;
2) 邊界:一個遞迴函式一定要有邊界,而且邊界一定要考慮全面,不能漏,否則它就可能死迴圈;
3) 範圍:就是你在遞迴時的選擇往哪兒走,也就是說,你的遞迴呼叫的函式返回值。
然後我們現在再來看一下記憶化搜尋:
①定義好一個數組,用來儲存遞迴所求出來的值;②在主程式裡,memset一下,一般都是賦初值為-1,然後把這個陣列的邊界值設定好;③在遞迴函式裡,首先加一句:if (這個陣列的值>=0) return 這個值【如果賦初值為-1的話,一般都是>=0】;其次,在後面的遞迴呼叫中,先給這個陣列賦值,再return。
這樣就差不多了。