## 定義 在函數的定義中使用函數自身的方法,在數學上可以這樣表示 \begin{cases} f(0) = 1\\ f(x) = x\ +\ f(x-1) \times 2,x\in N \end{cases} 在上面的數學例子中,$f(1) = 1 + f(0)\times2 = 1 + 1\times2 = 3$ 再舉一個實際的例子,假設我想知道我們家祖譜上的父輩祖先最早能溯源到誰 因為祖譜上只會寫**誰是某人的父親**,其他關係則**需要判斷**(例:爸爸的爸爸是爺爺) 我可以從我的爸爸 $F_0$ 先開始找,找到我爸爸後再去找我的爺爺 $F_1$ 如此往復,到最後找到**沒有記載父親的人**就是祖譜上最早的父輩祖先 而對於大部分人來說,遞迴很重要的一點是 "能否打破順序執行的思考過程" 這點在河內塔可以得到驗證,也可以快速判斷一個人是否有天賦 ## 開始前先介紹一個小工具 [Python動態顯示程式碼](https://pythontutor.com/python-debugger.html#mode=edit) 它可以一步步去生成程式碼的執行過程 可以幫助各位理解遞迴過程 如果遞迴的層數過大,它會報錯,請用**小測資幫助理解**就好 ## $n!$ (正整數的階乘) $n!$ 的定義所有小於等於該數的正整數的積,也可以簡單理解成 $n \times (n-1) \times\ ...\ \times 1$ 所以 $n! = n \times (n-1)! = n \times (n-1) \times (n-2)!$, 後面以此類推 由於這樣的特性,我們可以對階乘做遞迴,首先就是要找出數學的遞迴式 \begin{cases} \ 1! = 1\\ \ n! = n \times (n-1)!,x\in Z^+ \end{cases} 如果將 $n!$ 當成一個程式中的 function "A",$n! = n \times (n-1)!$ 其實就是在 A 中呼叫 A 也就是最一開始在定義中提到的在函式中呼叫自己的方式,實際程式碼如下: ```cpp= int rec(n) { # 明確的終止遞迴條件 if (n == 1) return 1 ; # 持續重複function(呼叫自身的部分) else if (n > 1) return n * rec(n-1) ; } ``` 很明顯多了一個"終止遞迴條件",因為數學的遞迴式當中也有同樣的東西 ($1! = 1$) 主要的原因是為了要**停止遞迴**(防止它無限遞迴下去),所以必須設定**明確的終止目標**(base case) base case 可能有很多種,最主要就是看你怎麼去定義你的遞迴式 比如在這個 case 當中你會發現,有沒有 $\times 1$ 結果都是一樣的 所以如果改寫成下面這個樣子也可以讓程式順利執行 ```cpp= int rec(n) { # 明確的終止遞迴條件 if (n <= 2) return n ; # 持續重複function(呼叫自身的部分) else if (n > 2) return n * rec(n-1) ; } ``` 這樣的程式碼在處理 $n > 2$ 的情況下只會乘到 $2$,在 $n <= 2$ 時回傳 n 因為根據定義可以發現 $1! = 1, 2! = 2$,不過這樣寫有幾個問題 1. 複雜的 base case 更容易出錯 2. 在大部分情況很難想到 因此如果在數學的領域也好,程式的領域也罷,通常都是使用最直觀或最簡單的 base case ## 例題-ZJ c547. Bert 爬樓梯 [題目連結](https://zerojudge.tw/ShowProblem?problemid=c547) 解題思路 : 這題其實跟高中學過的排列組合經典題目很像,就像下面這張圖 ![image alt](https://i.ytimg.com/vi/vqMnIPV1lxw/maxresdefault.jpg =75%x) 如果有學過就知道,$A$ 往下和往右直走的答案都是 $1$,如果不是 $A$ 能直走到的位置 就是將上方+左方的點的答案相加,就會變成該點的答案數量 舉這個例子只是想告訴各位,某個點 $X$ 的答案 = "能用一步走到 $X$ 的位置 $Y$" 所有 $Y$ 的答案總和 那對於這個題目來說,可以一次爬一階或爬兩階,反推來說就是可以退一階或退兩階 簡單講就是"只有第 $X-1$ 階和第 $X-2$ 階能夠一步走到第 $X$ 階" 加上上面的例子最後得出的結論,我們可以列出遞迴式 \begin{cases} f(1) = 1 \\ f(2) = 2 \\ f(x) = f(x-1) + f(x-2),\ n \ge 3 \end{cases} 但這題還有需要注意的地方,就是題目是重複輸入數字去找 如果每次都給最大的測資,那跑起來速度會很慢,如果我們能夠一邊計算一邊記錄結果 那之後就可以省略計算的部分,直接拿之前計算的結果來用就可以了 ```cpp= int rec(int n) { if (ways[n]) // 如果已經算過 return ways[n] ; // 直接回傳 ways[n] = (rec(n-1) + rec(n-2)) % mod ; return ways[n] ; // 記得取餘 } ``` 這裡的 base case 已經融入在 ways 陣列當中了,所以不需要特別寫出來 不過因為 $n$ 最大是 $10000$,所以一次計算完 $10000$ 階,其他也能計算完 ```cpp= #include<bits/stdc++.h> using namespace std ; #define mod 1000000007 int ways[10005] ; int rec(int n) { if (ways[n]) // 如果已經算過 return ways[n] ; // 直接回傳 return ways[n] = (rec(n-1) + rec(n-2)) % mod ; // 記得取餘 } int main() { ios::sync_with_stdio(0), cin.tie(0) ; // 前兩階用推的 其餘為遞迴式 ways[1] = 1 ; ways[2] = 2 ; rec(10000) ; // 一次計算完 int n ; while (cin >> n) cout << ways[n] << '\n' ; return 0 ; } ``` ## 例題-ZJ e357. 遞迴函數練習 [題目連結](https://zerojudge.tw/ShowProblem?problemid=e357) 解題思路 : 直接把數學的遞迴式拿來用,base case 就是 `if (x = 1) return 1 ;` ```cpp= #include<bits/stdc++.h> using namespace std ; int f(int x) { if (x == 1) return 1 ; if (x % 2 == 0) return f(x/2) ; return f(x-1)+f(x+1) ; } int main() { int x ; cin >> x ; cout << f(x) ; return 0 ; } ``` ```python= def main() : def f(x) : if (x == 1) : return 1 if (x % 2 == 0) : return f(x//2) return f(x-1) + f(x+1) x = int(input()) print(f(x)) main() ``` ## 例題-TCIRC 合成函數(2) (APCS201902) [題目連結](https://judge.tcirc.tw/problem/d002) 解題思路 : 首先要先列出遞迴式 * f(x)=2x–3 * g(x,y)=2x+y–7 * h(x,y,z)=3x–2y+z 因為範例輸入中只要把數字帶入函式算出來就好 並**不會出現無限遞迴**的情形,所以沒有 base case 而輸入的情形只有四種: "f" "g" "h" "數字" 所以我們只要用 if 就可以解出答案 不過對於大多數人來說最大的問題不是在這裡,而是在如何依照題目要求的順序運算 假設有一組測資 `g 1 2` 那當然很簡單,直接輸入 g 跟兩個數字即可 但是如果測資比較複雜,比如 `h f 5 g 3 4 3`,用數學的概念是 $h(f(5), g(3, 4), 3)$ 其實觀察可以發現最主要的困難點就是"如何判斷輸入數字/函式的層級" 在上面比較複雜的例子當中,函式 f 跟 g 都是在第二層,函式 h 則是在第一層 遞迴是可以做到同樣的效果的,假設我們有一個程式可以判斷輸入是函式還是數字,並將其計算完成 那在我遇到函式 f 時,是不是可以呼叫"一次"這個程式碼,這樣我就知道 $f(x)$ 中的 $x$,便可以算出 $f(x)$ 那推廣到三個函式跟數字,這個程式要處理什麼函式,就看它有幾個未知數需要找,就執行這個程式幾次 (如有需要可以用畫圖理解不同函式、數字之間的遞迴 level 關係,有助於學習) 實際的程式碼如下 : ```cpp= #include<bits/stdc++.h> using namespace std ; typedef long long LL ; LL rec() { string c ; cin >> c ; if (c == "f") // 遇到 f 函式 return 2*rec() - 3 ; else if (c == "g") // 遇到 g 函式 return 2*rec() + rec() - 7 ; else if (c == "h") // 遇到 h 函式 return 3*rec() - 2*rec() + rec() ; else // 遇到數字 return stoi(c) ; // stoi 是把 string 轉成數字 } int main() { ios::sync_with_stdio(0), cin.tie(0) ; cout << rec() << '\n' ; return 0 ; } ``` ```python= def main() : def rec(idx) : if list1[idx] == "f" : x, idx = rec(idx+1) return 2*x - 3, idx elif list1[idx] == "g" : x, idx = rec(idx+1) y, idx = rec(idx+1) return 2*x + y - 7, idx elif list1[idx] == "h" : x, idx = rec(idx+1) y, idx = rec(idx+1) z, idx = rec(idx+1) return 3*x - 2*y + z, idx else : return int(list1[idx]), idx idx = 0 list1 = input().split() ans, idx = rec(idx) print(ans) main() ``` 這邊同樣要注意,C++ 的輸入可以遇到空白就停止,而python只有**遇到換行(\n)才能停止** 所以我們必須**用index去指定位置** (index local 或 global 皆可) 再來,python有一個內建函式 **isdigit()** 可以判斷字串是否是數字,但是它**並不能判斷負數** 所以我直接**不判斷數字**,先判斷 f g h 的可能,剩下的那一個可能**只能是數字**,所以用 else C++ 可以用 stoi() (string to integer) 將字串轉成整數,python 用 int() 即可 ## 例題-ZJ d672. 10922 - 2 the 9s [題目連結](https://zerojudge.tw/ShowProblem?problemid=d672) 這題的題目敘述有點糟糕,所以我再重新整理一次 9-degree 的部分 9-degree 總共有幾個,要看數字各位數能加總幾次(加總直到數字 $< 10$ 以內) 最後的次數就是 9-degree,比如 $837 \rightarrow 18 \rightarrow 9$ 總共加總兩次,所以 $837$ 的 9-degree 為 $2$ 解題思路 : 這題主要看兩個輸出,第一個是 $n$ 是否為 $9$ 的倍數 這個非常簡單,我們只需要將各位數加總起來然後判斷是否為 $9$ 的倍數 ```cpp= string s ; cin >> s ; int num = 0 ; // 各位數加總的結果 for (char it: s) // 一個個位數加總 num += it - '0' ; // 字元轉數字(ASCII) if (num % 9 != 0) // 判斷為"不是" 9 的倍數 cout << s << " is not a multiple of 9.\n" ; else { // 是 9 的倍數 // 計算 9-degree } ``` ```python= s = input() if (s == "0") : return num = 0 for it in s : num += int(it) if (num % 9 != 0) : print(f"{s} is not a multiple of 9.") else : ## 計算 9-degree ``` 接下來就是用遞迴重複做加總的動作,最開始肯定要找 base case 其實題目有說只要算到 $< 10$ 就結束,就是這題的 base case 那接下來只剩下如何加總數字各位數 ```cpp= #include<bits/stdc++.h> using namespace std ; int rec(int num, int degree) { int next_n = 0 ; if (num >= 10) // 還可以繼續遞迴 degree++ ; else // base case return degree ; while (num) { // 加總各位數字 next_n += num % 10 ; num /= 10 ; } return rec(next_n, degree) ; // 進入下一層遞迴 } int main() { ios::sync_with_stdio(0), cin.tie(0) ; string s ; while (cin >> s && s != "0") { int num = 0, degree = 0 ; for (char it: s) // 一個個位數加總 num += it - '0' ; // 字元轉數字(ASCII) if (num % 9 != 0) // 判斷為"不是" 9 的倍數 cout << s << " is not a multiple of 9.\n" ; else { // 是 9 的倍數 degree = rec(num, 1) ; cout << s << " is a multiple of 9 and has 9-degree " << degree << ".\n" ; } } return 0 ; } ``` ```python= def main() : def rec(num, degree) : n = 0 if (num >= 10) : degree += 1 # 可以繼續遞迴 else : return degree # base case while (num) : # 加總各位數 n += num % 10 num //= 10 return rec(n, degree) while (True) : s = input() if (s == "0") : return # 結束輸入 num = 0 for it in s : num += int(it) if (num % 9 != 0) : print(f"{s} is not a multiple of 9.") else : degree = rec(num, 1) print(f"{s} is a multiple of 9 and has 9-degree {degree}.") main() ``` ## 例題-ZJ c129. 00572 - Oil Deposits [題目連結](https://zerojudge.tw/ShowProblem?problemid=c129) (建議先學過基礎圖論-DFS、BFS再解這題) 解題思路 : 題目說含有石油的小塊只要相連,就被分為同一個 oil deposit,要找圖中有幾個 oil deposit 其實就是當找到一塊新的 oil deposit 時,用 DFS/BFS 去將同 oil deposit 的地方消掉 因為如果不要再次遇到同個 oil deposit 的 pocket,那只有將遇過的 pocket 改成不含石油 以下是用 DFS 解題的程式碼 (BFS 不是遞迴) ```cpp= #include<bits/stdc++.h> using namespace std ; int n, m ; char G[105][105] ; void rec(int x, int y) { G[x][y] = '*' ; // 找到了就消除(改成不含石油) // 用踩地雷的相連(某格周遭8格) for (int i=-1;i<=1;i++) { for (int j=-1;j<=1;j++) { int nx = x+i, ny = y+j ; if (nx >= 0 && nx < n && ny >= 0 && ny < m) if (G[nx][ny] == '@') rec(nx, ny) ; } } return ; } int main() { ios::sync_with_stdio(0), cin.tie(0) ; while (cin >> n >> m && n && m) { // 輸入圖 for (int i=0;i<n;i++) for (int j=0;j<m;j++) cin >> G[i][j] ; int ans = 0 ; // 有幾個 pocket for (int i=0;i<n;i++) { for (int j=0;j<m;j++) { if (G[i][j] == '@') { // 新的 pocket ans++ ; rec(i, j) ; } } } cout << ans << '\n' ; } return 0 ; } ``` ## 類似題-ZJ d165. 八、草場普查 [題目連結](https://zerojudge.tw/ShowProblem?problemid=d165) 解題思路 : (建議先學過基礎圖論-DFS、BFS再解這題) 這題跟前一題一樣,草場的數量就是 oil deposit 的數量,不過這題還有某草場最大蘊藏量 簡單來說就是要在消去的同時計算數量,比如在這題中就需要在某個遞迴中回傳其延伸的草場蘊藏量 ```cpp= #include<bits/stdc++.h> using namespace std ; int n, m ; int G[105][105] ; int mm[4][2] = {{0, 1},{1, 0},{0, -1},{-1, 0}} ; // 上下左右 int rec(int x, int y) { G[x][y] = 0 ; // 記得走過要去除 int leaf = 0 ; // 上下左右延伸後的結果 for (int i=0;i<4;i++) { // 往四個方向走 int nx = x+mm[i][0], ny = y+mm[i][1] ; if (nx >= 0 && nx < n && ny >= 0 && ny < m) if (G[nx][ny]) leaf += G[nx][ny] + rec(nx, ny) ; // 延伸草場蘊藏量 } return leaf ; } int main() { ios::sync_with_stdio(0), cin.tie(0) ; while (cin >> n >> m) { for (int i=0;i<n;i++) for (int j=0;j<m;j++) cin >> G[i][j] ; int cnt = 0, ans = 0 ; // 數量、最大蘊藏量 for (int i=0;i<n;i++) { for (int j=0;j<m;j++) { if (G[i][j]) { // 新的草場 cnt++ ; ans = max(ans, G[i][j] + rec(i, j)) ; } } } cout << cnt << '\n' << ans << '\n' ; } return 0 ; } ``` ## 例題-ZJ f637. DF-expression [題目連結](https://zerojudge.tw/ShowProblem?problemid=f637) 解題思路 : 這題首先就是要釐清每個功能與其執行後的結果,功能有 $0, 1, 2$ 三個 $0$ 是代表目前的正方形(邊長 $n$)中,每個格子都是黑色(有 $n\times n$ 個格子是黑色) $1$ 是代表目前的正方形(邊長 $n$)中,每個格子都是白色(有 $n\times n$ 個格子是白色) $2$ 是代表目前的正方形(邊長 $n$)切割成四塊,然後接著依序處理這四塊的功能 因為切割成四塊,所以邊長會變成 $n/2$,但通常遞迴中兩個變數都會是 $n$ 只要記得在不同層級中的遞迴,同名變數可能代表的實際數字不同,簡單看下方表格 ![image alt](https://i.imgur.com/j2nuyaO.png) 這個表格就是字串中的數字與"遇到該數字時"的邊長、遞迴層級、黑色面積、總面積 結合表格與前面的解說,大概可以知道遇到不同數字時該如何運作 如果遇到 $0$,因為沒有增加黑色的面積,也沒有切割,所以不需要特別操作 如果遇到 $1$,增加了黑色面積,而且增加當下的邊長 $n \times n$ 格 如果遇到 $2$,要將目前的正方形切割成四塊,邊長減半,還要處理四個小塊 所以用程式碼大概會像是這樣 ```cpp= void rec(int n) { if (s[idx] == '1') // 像素 1 ans += n*n ; else if (s[idx] == '2') { for (int i=0;i<4;i++) { // 平分成四塊 idx++ ; rec(n/2) ; // 邊長/2 並一一處理 } } } ``` 因為我是用字串儲存操作的數字,所以要用一個 idx 代表 index 才能知道目前要做的操作是哪個,然後用 ans 紀錄總面積 ```cpp= #include<bits/stdc++.h> using namespace std ; string s ; // DF-expression int ans, idx ; // 像素 1 數量、index void rec(int n) { if (s[idx] == '1') // 像素 1 ans += n*n ; else if (s[idx] == '2') { for (int i=0;i<4;i++) { // 平分成四塊 idx++ ; rec(n/2) ; // 邊長/2 並一一處理 } } } int main() { ios::sync_with_stdio(0), cin.tie(0) ; int n ; // 重複輸入(可以一次輸入多個側資) // 不用重複編譯執行 while (cin >> s >> n) { ans = idx = 0 ; rec(n) ; cout << ans << '\n' ; } return 0 ; } ``` ## 例題-TCIRC 棍子中點切割 [題目連結](https://judge.tcirc.tw/problem/d003) (建議先學過排序基礎-二分搜後再開始解題) 解題思路 : 題目說機器可切割的點都是固定的,會計算出最接近中點的切割點,並照此切割點切成兩段 其實如果仔細去想,因為只有切割點可以被切割,所以切割後的棍子前後端 只有可能會是"一開始的左端"、"一開始的右端"、"指定的任意切割點" 所以如果把所有可能的左右端都放進同一個陣列,比如以下的陣列 | index | 0 | 1 | 2 | 3 | 4 | 5 | | --- | ---- | ---- | ---- | --- | --- | --- | | 左右端 | 0 | 1 | 2 | 4 | 6 | 10 | 這樣在找最接近中點的切割點時,會比較方便,因為可以用 `(A[left]+A[right])/2` 表示 要處理某個棍子就會需要依照這個順序 1. 判斷還能不能切割(兩端點中有無切割點) (base case) 2. 計算中點 3. 找出最靠近中點的切割點 4. 計算目前這根棍子的切割成本 5. 計算切割完後變成左右兩根棍子的切割成本 執行順序會像是 : $1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow 1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow 1 ...$ 用程式碼的話會像是下面這樣 ```cpp= int cp[50005] ; // cut point(左右端) int cut(int L, int R) { // left、right if (R-L <= 1) return 0 ; // 兩點中已無切點 int k = (cp[R]+cp[L]) / 2 ; // 中點 int dis = R - L, m ; // dis 與中點距離、m 最靠近中點座標 for (int i=L+1;i<R;i++) { if (abs(cp[i]-k) < dis) { dis = abs(cp[i]-k) ; m = i ; } } // 目前切割成本 + 左半邊總成本 + 右半邊總成本 return cp[R]-cp[L] + cut(L, m) + cut(m, R) ; } ``` 不過有一個問題,就是如果每次都用線性搜尋的話,時間複雜度有點太高了 簡單計算一下,如果有 n 個切點(包含左右端),大概就會是 $n\log(n)$ 因為這次的切點剛好是已排序的,並且剛好是在同一個陣列中,非常適合二分搜 二分搜通常是找 $\ge$ 中點的第一個點,因為這裡是找最靠近中點的切割點 所以還要找 $\le$ 中點的第一個點,也就是二分搜完後的那個點左邊的點 ```cpp= #include<bits/stdc++.h> using namespace std ; typedef long long LL ; LL cp[50005] ; LL cut(int L, int R) { if (R-L <= 1) return 0 ; // 兩點中已無切點 LL k = (cp[R]+cp[L]) / 2 ; // 中點 // 找出第一個 >= k 的 int m = lower_bound(cp+L, cp+R, k) - cp ; if (cp[m-1]-cp[L] >= cp[R]-cp[m]) // 左邊切點更靠近 m-- ; // 目前切割成本 + 左半邊總成本 + 右半邊總成本 return cp[R]-cp[L] + cut(L, m) + cut(m, R) ; } int main() { ios::sync_with_stdio(0), cin.tie(0) ; int N, L ; cin >> N >> L ; cp[0] = 0 ; // 兩端當作切點會比較好做 cp[N+1] = L ; for (int i=1;i<=N;i++) cin >> cp[i] ; cout << cut(0, N+1) << '\n' ; return 0 ; } ``` ## 例題-ZJ e183. 10940 - Throwing cards away II [題目連結](https://zerojudge.tw/ShowProblem?problemid=e183) 解題思路 : 這題如果要最簡單的解法是用 queue 去做模擬,但實際上這題是數論的題目 先從比較小的測資去看一下有沒有辦法整合出規律 * $n = 1\rightarrow 1$ * $n = 2\rightarrow 2$ * $n = 3\rightarrow 2,3\rightarrow 3,2\rightarrow 2$ * $n = 4\rightarrow 2,3,4\rightarrow 3,4,2\rightarrow 4,2\rightarrow 2,4\rightarrow 4$ * $n = 5\rightarrow\ ...\rightarrow 2$ * $n = 6\rightarrow\ ...\rightarrow 4$ * $n = 7\rightarrow\ ...\rightarrow 6$ * $n = 8\rightarrow\ ...\rightarrow 8$ 其實可能已經有人看出來了,大致上會變成以下這種結果 如果是偶數,比如 $n=6$,在丟棄保留幾次後會變成 $2,4,6$,剛好就是 $n=3$ 的兩倍 再看看 $n=12$,在丟棄保留幾次後會變成 $2,4,6,8,10,12$,剛好是 $n=6$ 的兩倍 如果再進一步丟棄保留,會變成 $4,8,12$,剛好是 $n=3$ 的四倍 因為丟棄保留的規則一樣,所以如果 $n$ 是偶數,結果就是 $n/2$ 結果的兩倍 如果是奇數($n > 1$),就比較特殊了,可以拿幾個例子來看,比如 $n=3,5,7,9,11,13$ $n = 3 \rightarrow 2$ $n = 5 \rightarrow 5,2,4\rightarrow 2$ $n = 7 \rightarrow 7,2,4,6\rightarrow 6$ $n = 9 \rightarrow 9,2,4,6,8\rightarrow 2$ $n = 11 \rightarrow 11,2,4,6,8,10\rightarrow 6$ $n = 13 \rightarrow 13,2,4,6,8,10,12\rightarrow 10$ 如果 $n=5$,經過幾次丟棄保留會剩下三個,但是跟 $n=6$ 不一樣,第一項變成 $5$ 觀察 $n=$ 其他奇數也一樣,經過幾次保留會變成 $n,2,4,...$,項數是 $(n+1)/2$ 既然項數一樣,是不是也可以找到規律,如果去找 $n$ 與 $(n+1)/2$ 的關係,比如 $n=5$ $n=5$ 的結果是 $2$,$n=(5+1)/2=3$ 的結果是 $2$ $n=7$ 的結果是 $6$,$n=(7+1)/2=4$ 的結果是 $4$ $n=9$ 的結果是 $2$,$n=(9+1)/2=5$ 的結果是 $2$ $n=11$ 的結果是 $6$,$n=(11+1)/2=6$ 的結果是 $4$ 應該能看出 `f(n) = 2*(f((n+1)/2)-1)`,如果真的不行也可以多舉幾個例子 ```cpp= #include<bits/stdc++.h> using namespace std ; int p[500001] ; // 算過的結果 int f(int a) { if (p[a]) return p[a] ; // 已經算過 if (a == 1) return 1 ; if (a%2 == 0) // 偶數 return p[a] = 2*f(a/2) ; else // 奇數 return p[a] = 2*(f((a+1)/2)-1) ; } int main(){ ios::sync_with_stdio(0), cin.tie(0) ; int n ; while(cin >> n && n) // 重複輸入 cout << f(n) << '\n' ; return 0 ; } ``` 有興趣的可以去看 Josephus problem,應該會有更詳細的推論跟更廣泛的問題討論 ## 河內塔 河內塔是遞迴中滿重要的一個例子,按照維基百科的敘述 有三根杆子 $A,B,C$,$A$ 杆上有 $N(N>1)$ 個穿孔圓盤,盤的尺寸由下到上依次變小 要按下列規則將所有圓盤從 A 杆移至 C 杆,問最少須多少步驟 1. 每次只能移動一個圓盤 2. 大盤不能疊在小盤上面 (圖片等請自行查詢,網路上有很多靜態或動態的表現都很不錯) 我們可以先看圓盤比較少的情況,先討論 $1~3$ 個圓盤 先看 $1$ 個圓盤,直接從 $A \rightarrow C$ 即可 $2$ 個圓盤(小、大),先把小圓盤從 $A \rightarrow B$,再把大圓盤從 $A \rightarrow C$,最後把小圓盤從 $B \rightarrow C$ $3$ 個圓盤,這個就比較特殊了,如果我們拆開來想,把最大的圓盤跟上面兩個分開 這樣我們只要想辦法把上面兩個圓盤先移到 $B$,然後把最大的圓盤移到 $C$ 最後只要把上面兩個圓盤從 $B \rightarrow C$ 其實如果你觀察力不錯的話會發現兩個圓盤也是這個概念 把 "最下方的圓盤" 跟 "其餘圓盤" 拆開做移動,不過可能會有人有疑問 如何把兩個圓盤從 $B$ 移到 $C$,其實跟 $A \rightarrow C$ 一樣,只是位置稍微修改而已 那對於 $N \ge 2$ 的情況,其實都能用拆分移動的方法 拆分後的上方就是討論移動 $N-1$ 從 $A \rightarrow B$ 跟從 $B \rightarrow C$ 的情況 拆分後的下方就是討論移動第 $N$ 個圓盤從 $A \rightarrow C$ 的情況 所以如果需要一個遞迴式去計步數,應該會像是下面這樣 $\begin{cases} f(a,b,c,1) = 1 \\ f(a,b,c,n) = f(a,c,b,n-1)+f(b,a,c,n-1)+1 \end{cases}$ 要注意這裡的 $a,b,c$ 是指 "起點,暫存點,終點",暫存點就是可能會暫時放在該位置 但最後一定都是 "原本都在起點" 到 "全都在終點",再來看看程式碼 ```cpp= #include<bits/stdc++.h> using namespace std ; int rec(char a, char b, char c,int n) { if (n == 1) { // n == 1 直接從 A 到 C cout << "move 1 from " << a << " to " << c << '\n' ; return 1 ; } // 拆分成上下兩部分 int cnt = 1 ; // 先把下方從 A 到 C 的移動加進去 cnt += rec(a, c, b, n-1) ; // 上方從 A 到 B // 下方從 A 到 C cout << "move " << n << " from " << a << " to " << c << '\n' ; cnt += rec(b, a, c, n-1) ; // 上方從 B 到 C return cnt ; } int main() { ios::sync_with_stdio(0), cin.tie(0) ; int n ; cin >> n ; cout << rec('A', 'B', 'C', n) << '\n' ; return 0 ; } ``` 通常河內塔的問題最基礎也會問移動的步驟,以上程式碼就是移動步驟+移動次數 如果只問移動次數的,其實可以用數學做推導,只要把遞迴式通解算出來就好 河內塔其實還有一些延伸的概念,但不在遞迴的概念內或太難的我就跳過了