<style>
body{
background-attachment: fixed;
background-repeat: no-repeat;
background: -webkit-linear-gradient(left, #ffffff,rgba(51,51,51,0.5)), url("https://i.imgur.com/NZiHUtv.jpg") center no-repeat fixed;
};
</style>
#### MDCPP進階組講義
## 遞迴 Recursion
作者:LittlePants
----
### 何謂遞迴?
- 遞迴就是大事化小,小事化無
- 遞迴的精隨是什麼?你要相信他是對的
- 遞迴暴搜可以解決一切,但是可能必須等到世界毀滅
---
### 基本概念
- 數學上:函數直接或間接已自己定義自己
- 程式上:一個函數會直接或間接的呼叫自己
----
遞迴是一個很重要的程式結構
很多演算法都會用到遞迴的概念(DFS, DP)
但遞迴思考的方式比較特殊
藉由求出答案與子問題的關係,來回推出答案
----
但學習遞迴是非常必要且基礎的
我們會先用階乘來簡單講解一下遞迴
----
我們都知道 $N! = 1 \times 2 \times 3 ... N$
我們定義一個函數 $f(X)$ 代表 X!
----
那麼可以這樣定義 $f(N) = N * f(N-1)$ 當 $n > 1$
如果你想知道$f(3)$是多少,你必須先算出$f(1),f(2)$
----
步驟如下
$f(3) = 3 * f(2)$
$f(2) = 2 * f(1)$
$f(1) = 1$
所以要得到$f(3)$的值
我們需要先把問題化簡到1再回推
----
### 我們來用程式寫寫看
```cpp
int f(int x){
if(x == 1) return 1; // 中止條件(邊界條件)
return x * f(x-1); // 遞迴轉移
}
```
----
### 寫遞迴有兩個重點
- 遞迴如何轉移
- 遞迴中止條件
----
也就是說
只要想出上面兩點你就可以把遞迴打出來了
我們來做個簡單的練習
[簡單費氏數列](http://mdcpp.mingdao.edu.tw/problem/B001)
(請大家不要使用迴圈做出這題)
----
參考code :
``` cpp
int f(int n){
if(n < 0) return 0;
if(n == 1 or n == 0) return 1; // 中止條件
return f(n-1) + f(n-2); // 遞迴轉移
}
```
----
我們會發現
使用上面程式執行效率會非常低
有可能是因為大量呼叫函數會花掉一些時間
但主要是因為有一大部分被重複計算了
----
我們看看當我們呼叫$f(6)$的情況
![](https://i.imgur.com/Z4zLQPF.png)
圖片網址 : http://blog.csdn.net/u013309870
----
有一大部份被重複計算了
當測資越大 被重複計算的次數也會越多
這是非常沒有效率的
所以我們會使用一個~~由蔣公提出來的方法~~的有效方法
----
## 用空間換取時間(記憶化)
----
我們多開一個陣列紀錄每個數字的答案
如果已被執行過就可直接回傳答案
廢話不多說 直接上程式碼
----
```cpp
int t,n,fab[200];
int f(int n){
if(n < 0) return 0;
if(fab[n]) return fab[n]; //記憶化
return fab[n] = (f(n-1) + f(n-2)); // 本code文眼
}
signed main(){
fab[0] = fab[1] = 1;
cin >> n;
cout << f(n) << '\n';
}
```
----
相信各位到次為止已經對遞迴有了初步的認識
但遞迴的用處遠遠不只如此
它還可以做到一些迴圈做不到的事
---
## 經典應用
---
## 求解 GCD(最大公因數)
----
要求gcd我們通常會用到一個家喻戶曉的演算法
----
## 歐幾里得演算法
也就是輾轉相除法
----
這個演算法大家小學時該都有學過
我來幫大家複習一下
----
```cpp
輾轉相除法
| 540 | 840 |
1 | 300 | 540 | 1
|-----|-----|
| 240 | 300 |
4 | 240 | 240 | 1
|-----|-----|
| 0 | 60 |
```
----
假設給們兩個數a,b要求他們的最大公因數
如果 b 能整除 a 那最大公因數就是 b
否則就讓 b = a % b , a = 原本的 b
一直做直到 b 能整除 a
----
簡單來說就是
- $\gcd(a,b)=\gcd(a,b-a)=\gcd(a,b \% a)$
----
有沒有一點遞迴的感覺
終止條件是 a % b == 0
否則就一直將 a , b 變小
透過數學證明我們可以知道
歐幾里得算法的時間複雜度是$O(log(min(a,b)))$
----
阿具體要怎麼做呢?
我們來看一下程式碼
----
參考 code(遞迴)
```cpp
int gcd(int a,int b){
if(b == 0) return a;
return gcd(b,a%b);
}
```
就是這麼短
但你要相信他是對的
----
但遞迴很慢啊
有沒有其他打法
----
參考 code(非遞迴毒瘤打法)
```cpp
int gcd(int a,int b){
while( (a%=b) and (b%=a) ) ;
return a | b; // 這裡的 | 可以改成 +
}
```
這是C語言最短的打法
但C++可以更短
----
```cpp
int gcd(int a,int b){
return __gcd(a,b);
}
```
???這個__gcd()是甚麼???
這是某些版本的C++會內建的函數
他不是標準函數所以前面會加上兩個底線
---
## 經典例題講解
---
---
### [河內塔](http://mdcpp.mingdao.edu.tw/problem/B011)
----
相信各位小時候都有玩過河內塔
它跟遞迴也有一些關聯
----
每次我們要把N個圓環移到目標柱子上時
我們會先把上面N-1個圓環移到非目標柱子上
然後再把第N個圓環移到目標柱子上
在把上面N-1個圓環放到N上面
----
只要弄清楚了目標柱與非目標柱
我們就可以打出下面的程式碼
----
```cpp
#include<bits/stdc++.h>
using namespace std;
void tow(int num, string A, string B, string C){
if (num == 0 ) return ;
tow(num-1, A, C, B); // 上面N-1個圓環移到非目標柱子上
cout << "Ring " << num << " from " << A << " to " << C << "\n";
// 移動第N個圓盤到目標柱上
tow(num-1, B, A, C); // 在把上面N-1個圓環放到N上面
}
int main(){
int n;
cin >> n;
tow(n ,"A","B","C");
}
```
---
### [合成函數(APCS考題)](http://mdcpp.mingdao.edu.tw/problem/B007)
----
合成函數的意思就是他傳入的參數有可能是數字或是函數
所以這題的參數只有3種情況f,g,或是數字
----
如果輸入的是數字就可以直接使用
如果輸入的是f就需要再往後讀一格
如果輸入的是g則需要往後讀兩格
----
按照上面的想法我們直接在函數中進行輸入
參考 code:
```cpp
#include<bits/stdc++.h>
using namespace std;
string c;
int sol(){
cin >> c;
int res = 0;
if(c[0] == 'f'){
res = 2 * sol() - 1;
} else if(c[0] == 'g'){
res = sol() + 2 * sol() - 3;
} else {
return stoi(c);
}
return res;
}
signed main(void){
cout << sol() << '\n';
}
```
----
上面的code我用到了一個函數 stoi
功能就是把 string 轉成 int
同理也有 stol 和 stoul
----
回家作業
[合成函數(困難)](http://mdcpp.mingdao.edu.tw/problem/B009/)
---
### [分形](http://mdcpp.mingdao.edu.tw/problem/B003)
![](https://i.imgur.com/Kmy6myU.png)
圖片來源 : wiki
----
對於這題我們不需要研究什麼是分形
我們只需要觀察規律就好
----
我們會發現每增加一級
X的數量會變成原來的5倍
而長寬則會變成原本的3倍
----
```cpp
X // X數量為 1 邊長為 1
---
X X
X
X X // X數量為 5 邊長為 3
---
X X X X
X X
X X X X
X X
X
X X
X X X X
X X
X X X X // X數量為 25 邊長為 9
```
----
透過以上觀察
我們來遞迴定義一下這個分形
我們用$b(x)$來當作我們的函數
$b(n)$代表n級的盒子
----
我們知道 $b(1)$ = x
而 $b(n)$ 遞迴定義就是
``` cpp
b(n - 1) b(n - 1)
b(n - 1)
b(n - 1) b(n - 1)
```
----
最後我們只需要觀察出盒子之間的距離
就可以開始打了
這部份就讓同學自己思考看看吧
hint : 提供兩種方法求3的n次方
1. 可以用 pow(3, n)
2. 蓋一個表 pow[n] = 3的 n 次
----
參考 code (從中間擴散):
```cpp
#include<bits/stdc++.h>
using namespace std;
int T,n;
bool mat[2000][2000];
void sol(int x,int y,int n){
if(n == 1){
mat[x][y] = 1; //標記此格為X
return ;
}
int k = pow(3,n-2);
sol(x+k,y+k,n-1); //右下
sol(x+k,y-k,n-1); //右上
sol(x,y,n-1); //中間
sol(x-k,y+k,n-1); //左下
sol(x-k,y-k,n-1); //左上
}
signed main(){
cin >> T;
while(T--){
cin >> n;
int len = pow(3,n-1), k = (len+1)/2;
sol(k,k,n);
for(int i = 1; i <= len; i++)
for(int j = 1; j <= len; j++){
cout << (mat[i][j] == 1 ? "X" : " ");
if(j == len) cout << '\n';
}
cout << "---\n";
}
}
```
----
參考 code (從左上開始) :
by ICEYLEMON
``` cpp
#include<bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f
#define pb push_back
#define vi vector<int>
#define f first
#define s second
#define pii pair<int, int>
#define mod 1000000007
#define MOD(x) ((x)%mod + mod)%mod
#define all(x) x.begin(), x.end()
#define mem(x, a) memset(x, a, sizeof(x))
#define LOLI ios_base::sync_with_stdio(0), cin.tie(0)
#define loli
#ifdef loli
#define bug(x, n) for(int tt = 1; tt <= n; tt ++) cout << x[tt] << " ";cout << "\n";
#else
#define bug(x, n)
#endif
using namespace std;
const int maxn = 5e3 + 50;
char mp[maxn][maxn];
int power[100];
void init(){ // 蓋3的n次表
power[0] = 1;
for(int i = 1; i <= 10; i ++){
power[i] = power[i - 1] * 3;
}
}
void draw(int x, int y, int p){
for(int i = x + 1; i <= x + p; i ++){
for(int j = y + 1; j <= y + p; j ++){
mp[i][j] = mp[i - x][j - y];
}
}
}
void solve(int n){
if(n == 1){
mp[n][n] = 'X';
return;
}
solve(n - 1);
int p = power[n - 2];
draw(2 * p, 0, p);
draw(2 * p, 2 * p, p);
draw(0, 2 * p, p);
draw(p, p, p);
}
signed main(){
LOLI;
int n, T;
cin >> T;
init();
while(T --){
cin >> n;
mem(mp, ' ');
solve(n);
for(int i = 1; i <= power[n - 1]; i ++){
for(int j = 1; j <= power[n - 1]; j ++){
cout << mp[i][j];
}
cout << "\n";
}
cout << "---\n";
}
}
```
---
### 回家作業
----
#### [CF 1461D](https://codeforces.com/problemset/problem/1461/D)
可能會用到其他算法做一些優化
----
在學完DFS和回溯法之後會有更多題目
可以好好期待一下
{"metaMigratedAt":"2023-06-15T13:20:46.911Z","metaMigratedFrom":"YAML","title":"MDCPP遞迴講義","breaks":"true","image":"https://i.imgur.com/NZiHUtv.jpg","slideOptions":"{\"theme\":\"sky\",\"transition\":\"slide\"}","description":"作者:LittlePants","contributors":"[{\"id\":\"853e1517-7034-4077-803a-7bb83a49f00a\",\"add\":12429,\"del\":5051}]"}