# 遞迴
----
遞迴是指在函式中直接或間接的呼叫自己,
以達到重複執行的目的。
---
# 基礎遞迴架構
----
首先,要知道怎麼遞迴。
再來,列出終止條件。
最後,把遞迴式放入程式。
---
## 例題 河內塔([ZJ i322](https://zerojudge.tw/ShowProblem?problemid=i322))
有三個柱子與n個環,從小到大
由上而下堆在柱子A上。
我們想要把所有的環都搬到柱子C上,
但是過程中,大環不能在小環上面。
----
## 遞迴式
假設我們要把前i個環從柱子x搬到柱子y
首先,我們要先把上面的環都從柱子x搬到柱子z
接著,把第i個環搬到柱子y
最後,把原本上面的環從柱子z搬到柱子y
----
```cpp=
#include <bits/stdc++.h>
using namespace std;
void hanoi(int l, char c1, char c2, char c3){
//l:第幾個環 c1:從哪裡, c2:到哪裡, c3:剩下那個
if(l==0) return;
hanoi(l-1, c1, c3, c2); //先把上面的都搬到剩下那個
cout << "Move disc " << l << " from " << c1
<< " to " << c2 << '\n'; // 這個環搬到目標處
hanoi(l-1, c3, c2, c1); //再把上面的從剩下那邊搬到目標處
}
int main()
{
int n;
cin >> n;
hanoi(n, 'A', 'C', 'B'); //從A搬到C,共n層
return 0;
}
```
---
# 改善遞迴
我們可以透過一些方法減少遞迴需要的時間
----
## 例題 費氏數列([ZJ e794](https://zerojudge.tw/ShowProblem?problemid=e794))
費氏數列的前兩項都是1,而後面每一項
都是前兩項的和。
----
## 遞迴式
${fib}_0=1$
${fib}_1=1$
${fib}_n = {fib}_{n-1} + {fib}_{n-2},\ n \ge 2$
要輸出${fib}_{n-1}和{fib}_n$
----
## 例題解答
```cpp=
#include <bits/stdc++.h>
using namespace std;
long long fib(int n){
if(n<0) return 0; // 以免發生輸入錯誤
if(n==1 || n==0) return 1; // 停止條件
return fib(n-1)+fib(n-2); // 遞迴式
}
int main(){
int n;
cin >> n;
cout << fib(n-1) << ":" << fib(n) << '\n';
return 0;
}
```
----
但是這個方法時間會花很久(會TLE)。
這是因為我們會重複算同樣的東西好多次。
因此我們需要改善這個方法。
---
## 改善1 - 儲存算過的值
我們可以發現,有不少的值會一直被重複呼叫。
因此,我們可以把算過的存起來,不要重複計算。
----
## 改善程式1
```cpp=
#include <bits/stdc++.h>
using namespace std;
long long fi[5001]; //先宣告最大需求的陣列
long long fib(int n){
if(n<0) return 0; // 以免發生輸入錯誤
if(fi[n]) return fi[n];
// 停止條件改為遇到有值的(0和1已經是1)
fi[n] = fib(n-1) + fib(n-2); // 遞迴式
return fi[n];
}
int main(){
int n;
fi[0] = 1; // 初始條件
fi[1] = 1;
cin >> n;
cout << fib(n-1) << ":" << fib(n) << '\n';
return 0;
}
```
---
## 改良2 - 迴圈化
我們有時候可以把這個遞迴式變成迴圈,
因為迴圈不用跳到不同層,執行上比較快,
而且也不用擔心堆疊溢位的問題。
雖然這樣已經不算是遞迴了,但是它
還是用遞迴式推出來的。
----
我們現在知道只要我們有${fib}_{n-1}$和${fib}_{n-2}$,
我們就可以算出${fib}_{n}$,
因此我們可以從下往上算。
----
## 改善程式2
```cpp=
#include <bits/stdc++.h>
using namespace std;
long long fi[5001]; //先宣告最大需求的陣列
int main(){
int n;
fi[0] = 1; //初始條件
fi[1] = 1;
cin >> n;
for(int i=2; i<=n; i++){
fi[i] = fi[i-1] + fi[i-2]; // 遞迴式
}
cout << fi[n-1] << ":" << fi[n] << '\n';
return 0;
}
```
---
## 改良3 - 縮減儲存
有時候我們甚至可以不用存所有的值,
只要存我們會用到的就好。
因為有時候要存的陣列太大,可能會出問題。
或是為了避免MLE(記憶體不夠)。
----
在費氏數列,我們只需要知道最後兩項,就可以推出下一項。
因此我們只要存兩項就好。
----
## 改善程式3
```cpp=
#include <bits/stdc++.h>
using namespace std;
int main(){
int n;
long long prev1 = 1, prev2 = 1, next; // 只要知道前兩個就好。
cin >> n;
for(int i=2; i<=n; i++){
next = prev1 + prev2; // 遞迴式
prev1 = prev2; // 存新的「最後兩個」
prev2 = next;
}
cout << prev1 << ":" << prev2 << '\n';
return 0;
}
```
---
## 改良方法2 - 提前停止
有時候,我們用遞迴來窮舉方法。
這時,如果可以在跑到停止條件前,
先知道這個後面的都不可行的話,
那麼就可以直接退出,不用繼續跑下去。
---
# 總結
步驟1: 列出遞迴式
步驟2: 寫出遞迴程式
步驟3: 修正、改良
---
# 推薦題目
[ZJ f928](https://zerojudge.tw/ShowProblem?problemid=f928)
[ZJ i644](https://zerojudge.tw/ShowProblem?problemid=i644)
[ZJ d443](https://zerojudge.tw/ShowProblem?problemid=d443)
{"title":"遞迴","contributors":"[{\"id\":\"00ad9127-6491-4b3d-829b-7847a217f8e5\",\"add\":6107,\"del\":2875}]","description":"遞迴是指在函式中直接或間接的呼叫自己,以達到重複執行的目的"}