
Recursion
Arvin Liu

一個方便找投影片的目錄
Syllabus - 0
內容 | 快速連結 |
---|---|
What is Recursion? 什麼是遞迴呢? | Section 1 |
Recursion Tree 用遞迴樹想像遞迴的過程 | Section 2 |
Ex O - 遞迴觀念題 | Section 3 |
Ex I - 遞迴觀念題練習 如何將數學遞迴程式遞迴? | Section 4 |
Recursion Tips 遞迴要注意的事項 | Section 5 |
Ex II - 彩帶問題 從排列組合到遞迴 | Section 6 |
Memorization - 記憶化 一個讓遞迴更快的重要技巧 | Section 7 |
Euclidean Algorithm - 輾轉相除法 | Section 8 |
Exercise III - Rhythm Doctor | Section 9 |
零和問題 嘗試用遞迴暴搜索有可能! | Section 10 |
Pruning - 剪枝一個大家都會,但很重要的技巧 | Section 11 |
Talks - 結語 | Section 12 |
重要章節
主要章節

這個投影片怎麼用呢?
Before Before Class - 0
- 上下左右切換投影片,用ESC可以看到整個投影片的大綱。
- 每個直排都是一個主題,右上角有編號,你可按右上角的 回第一頁。
- 正確觀看順序:一直往下,到底往右。

Esc後大概長這樣,你可以先Esc在上下左右選你要看哪頁投影片。

What is Recursion?
什麼是遞迴呢?

先讓我們看段影片...
What is Recursion ? - 0

我們從這段影片學到了甚麼?
What is Recursion ? - 1
- 遞迴定義:每個人遇到問題的時候,都會跑去問他的上級。
- 店員遇到解決不了的問題,就該去問經理。
- 經理遇到解決不了的問題,就該去問老闆。
- ....
- 基本情況 (Base case):God能夠解決一切。

什麼是遞迴呢?
What is Recursion ? - 2
Recursion

一個經典的例子 - 斐波那契數列
What is Recursion ? - 4
Fibonacci Sequence
- 寫成這樣也OK (在數學上)。
*otherwise: 否則的意思
- 常見的表達式長這樣:
這怎麼看呢?

一個經典的例子 - 斐波那契數列
What is Recursion ? - 5
Fibonacci Sequence
- 寫成這樣也OK (在數學上)。
*otherwise: 否則的意思
int f(int n){ if(n == 1 || n == 2) return 1; else return f(n-1) + f(n-2); }
- 程式就當作你在寫一般的函式,照著寫就可以了。

一個經典的例子 - C(n, m)
What is Recursion ? - 6
- 什麼是C呢? C(n, m) 就是從n個相異東西取出m個的取法總數。
- 舉例來說,如果從下面4顆球取出2顆球出來,總共有6種取法。
- 那麼我們 C(4, 2) 就會等於6。
C(n, m)其實是有遞迴公式的,長的像下面這樣。
有4顆相異的球

一個經典的例子 - C(n, m)
What is Recursion ? - 7
- 那麼我們 C(4, 2) 就會等於6。
C(n, m)其實是有遞迴公式的,長的像下面這樣。
- 為甚麼呢?
- C(n, m): 從n顆球取m顆的取法
- 如果我取了第一顆球,那麼總數會是 "從n-1顆球取m-1顆",C(n-1, m-1)。
- 如果我不取第一顆球,那麼總數會是 "從n-1顆球取m顆",C(n-1, m)。
- 這樣你懂為什麼了嗎?
- C(n, m): 從n顆球取m顆的取法

一個經典的例子 - C(n, m)
What is Recursion ? - 8
- 那麼我們 C(4, 2) 就會等於6。
C(n, m)其實是有遞迴公式的,長的像下面這樣。
- 如同剛剛的例子一樣,轉成程式直接照著寫就好了。
int C(int n, int m){ if(n == 0 || n == m) return 1; else return C(n-1, m-1) + C(n-1, m); }

Recursion Tree
遞迴樹 - 遞迴要怎麼想像呢?

怎麼分析遞迴呢?
What is Recursion Tree ? - 0
Recursion
- 你大致上可以把遞迴看成是一個分支圖,舉例來說,我們呼叫了f(5):
1
2
3
4
5
6
7
8
9
- 紫色數字是呼叫的順序
- 如果有兩個f,函式會先做完第一個再去做第二個。
- 所以答案為5。

怎麼分析遞迴呢?
What is Recursion Tree ? - 1
Recursion
- 試試看畫出C(4, 2)的分析圖吧!
- 所以答案為6。
- 你會發現一件事情,在分析的時候,有些遞迴會重複,這個時候用之前算過的值就好了。
- 例如C(2, 1)有算過兩次,第二次直接看第一次算的值(=2)就好了。

為甚麼要叫遞迴樹呢?
What is Recursion Tree ? - 2
Tree

樹根
樹葉

- 起先,你以為樹是往上長的。
- 但在程式 / 資料結構裡面,樹是往下長的。
- 遞迴的分支圖,其實就是一棵樹,我們可以叫它遞迴樹。
樹葉
樹根

Exercise O
APCS的手寫遞迴題

Exercise O
APCS 手寫練習題
接下來有三題APCS手寫題。
- 在寫之前請各位拿出一張紙方便計算。
- 每一題只能寫2分鐘。
- 這裡沒有給選項。

APCS 106/03 觀念題 - 21
Exercise O - APCS遞迴手寫題1 - 0
int F (int x, int y) { if (x<1) return 1; else return F(x-y, y) + F(x-2*y, y); }
21. 若以 F(5,2) 呼叫下方 F() 函式,執行完畢後回傳值為何?
F(5, 2) = F(3, 2) + F(1, 2)
F(3, 2) = F(1, 2) + F(-1, 2)
F(1, 2) = F(-1, 2) + F(-3, 2)
= 1 + 1 = 2
= 2 + 1 = 3
= 3 + 2 = 5
→ 答案 = 5
2 mins

APCS 105/03 觀念題 - 24
Exercise O - APCS遞迴手寫題2 - 1
int f (int n) { if (n > 3) { return 1; } else if (n == 2) { return (3 + f(n+1)); } else { return (1 + f(n+1)); } }
24. 若以 g(4) 呼叫 g() 函式,執行完畢後回傳值為何?
int g (int n) { int j = 0; for (int i=1; i<=n-1; i=i+1) { j = j + f(i); } return j; }
- g(4)答案會等於多少呢?
= f(1) + f(2) + f(3)。 - 那麼f(1)又是多少呢?
好像不太容易看出來...
2 mins

APCS 105/03 觀念題 - 24
Exercise O - APCS遞迴手寫題2 - 2
int f (int n) { if (n > 3) { return 1; } else if (n == 2) { return (3 + f(n+1)); } else { return (1 + f(n+1)); } }
24. 若以 g(4) 呼叫 g() 函式,執行完畢後回傳值為何?
int g (int n) { int j = 0; for (int i=1; i<=n-1; i=i+1) { j = j + f(i); } return j; }
- g(4)答案會等於多少呢?
= f(1) + f(2) + f(3)。 - 那麼f(1)又是多少呢?
好像不太容易看出來... - f(1) = 1 + f(2)
- f(2) = 3 + f(3)
- f(3) = 1 + f(4)
- f(4) = 1
- 答案 = 13
= 6
= 5
= 2
= 6 + 5 + 2
2 mins

APCS 107/12 觀念題 - 03
Exercise O - APCS遞迴手寫題3 - 3
int K (int a[], int n) { if (n >= 0) return (K(a, n-1) + a[n]); else return 0; } int G (int n) { int a[] = {5, 4, 3, 2, 1}; return K(a, n); }
3. 給定右側 G(), K() 兩函式,執行 G(3) 後所回傳的值為何?
2 mins
- G(3)會呼叫K(a, 3)。
- K(a, 3) = K(a, 2) + a[3]
- K(a, 2) = K(a, 1) + a[2]
- K(a, 1) = K(a, 0) + a[1]
- K(a, 0) = K(a, -1) + a[0]
- K(a, -1) = 0
- 答案 = a[0] + a[1] + a[2] + a[3] = 14

Exercise I
警報器 - 簡單的遞迴練習

Exercise I
程式練習題
接下來有一題請你用
程式實做遞迴的題目。
- 我會花1分鐘讀題。
- 總共計時10分鐘。
- 開好你的程式環境吧!

226 - 警報器 - 題目
Exercise I - 警報器 - 0
- 題目:請問警報器長鳴為一次需3秒,短鳴一次需1秒,每格鳴聲之間停2秒,若鳴聲時間為t秒 (開頭跟結尾必須是鳴聲,不能是間隔的2秒),
那麼請問有多少種信號?
(t為1到100的正整數。)
- 小小提示,這題答案的遞迴式長這樣,你可以實作出來嗎?
10 mins
- t = 6總共有兩種可能。
3s
2s
1s
1s
2s
3s

226 - 警報器 - 解答
Exercise I - 警報器 - 1
- 小小提示,這題答案的遞迴式長這樣,你可以實作出來嗎?
10 mins
int f(int t) { if (t == 1 || t == 3) return 1; else if (t <= 0) return 0; else return f(t-5) + f(t-3); }
- 轉成程式的話就是這樣。使用直接輸出 f(t)就可以了

226 - 警報器 - 引導
Exercise I - 警報器 - 2
- 為甚麼題目可以寫成這樣的遞迴呢?
10 mins
- 在所有長度為t的鳴聲中其實可分為兩類,把這兩種可能加起來就可以了。
- 最後一段是長鳴。
- 最後一段是短鳴。
可能數 = 長度為t-5的鳴聲種數。
可能數 = 長度為t-3的鳴聲種數。
2s
1s
2s
3s
t-5 s的組合鳴聲
t-3 s的組合鳴聲
t s的組合鳴聲

Recursion Tips
遞迴要注意的事情

遞迴的題目要怎麼思考呢?
Recursion Tips - 遞迴要注意的事情 - 0
- 想辦法把大問題拆成小問題!
- 你可能要先定義問題本身,再想辦法達成這個問題的答案 (?)
- 思考的時候,
- 不要從一開始往上推,例如n=0怎麼樣,n=1怎麼樣...
- 通常情況下,要逆向思考,例如n要怎麼用n-1表示,或者n-2表示...
- 舉例來說,思考長度為t的鳴聲怎麼做時,我們要逆向思考。
- 想想長度為t的鳴聲怎麼用t-?的鳴聲來的得到。
- 而不是思考t=1要怎麼往上推到長度為t。
-
記的要思考基礎 / 終止條件!
- 不然遞迴會無止盡的跑下去,最終吃TLE或RE

記得要寫base case!
Recursion Tips - 遞迴要注意的事情 - 1


小心無窮遞迴!
Recursion Tips - 遞迴要注意的事情 - 2

Exercise II
彩帶問題 - 從遞迴到排列組合

Exercise II
程式練習題
接下來有一題請你用程式實做遞迴的應用題。
- 我會花1分鐘讀題。
- 總共計時15分鐘。
- 想一下前面教了什麼。
- 至少第一組測資要可以AC。

227 - 彩帶問題 - 題目
Exercise II - 彩帶問題 - 0
- 題目:現在Peipei要請你做出一個漂亮的彩帶,每一段你可以選擇三種顏色,分別是紅,橘,藍三種。至於何為漂亮呢?漂亮的彩帶有以下的限制:
- 不能有連續的紅色。
- 橘色後面一定要接藍色。
- 你可以告訴我們如果彩帶的長度為n (n介於1到50到之間的整數),
有幾種可能的漂亮的彩帶呢? (Hint:參數不只一個n。)
- 如果題目輸入的n為2,答案會是6種。
15 mins

227 - 彩帶問題 - 引導
Exercise II - 彩帶問題 - 1
-
我們試著以這樣的思維想想看:
-
在所有長度為n的漂亮彩帶的可能中,可以分成以下3個case。
- 最後一段為紅色的漂亮彩帶。
- 最後一段為橘色的漂亮彩帶。
- 最後一段的藍色的漂亮彩帶。
-
那麼在這3個case,分別會有幾種可能呢?
-
我們以最後一段為紅色做舉例
- 數量會等於長度為n-1,最後一段為藍色的漂亮彩帶的可能數。
-
我們以最後一段為紅色做舉例
-
在所有長度為n的漂亮彩帶的可能中,可以分成以下3個case。
15 mins
?
?
?
...
n-1
?
?
?
...
n-1
?
?
?
...

227 - 彩帶問題 - 引導
Exercise II - 彩帶問題 - 2
- 如果我們將長度為n,顏色為color定義成 rec(n, color)。
- rec(n, RED) = rec(n-1, BLUE)。
- rec(n, ORANGE) 和 rec(n, BLUE) 呢?
- rec(n, ORANGE) = rec(n-1, RED) + rec(n-1, BLUE)。
- rec(n, BLUE) = rec(n-1, RED) + rec(n-1, BLUE) + rec(n-1, ORANGE)。
-
終止條件呢?
- rec(1, 不管甚麼顏色) = 1
15 mins
?
?
?
...
n-1
?
?
?
...
?
?
?
...
?
?
?
...
n-1
?
?
?
...
?
?
?
...

227 - 彩帶問題 - 解答
Exercise II - 彩帶問題 - 3
// RED = 0, ORANGE = 1, BLUE = 2 long long rec(int n, int type) { if (n == 1) return 1; else if (type == RED) return rec(n-1, BLUE); else if (type == BLUE) return rec(n-1, ORANGE) + rec(n-1, BLUE) + rec(n-1, RED); else if (type == ORANGE) return rec(n-1, BLUE) + rec(n-1, RED); else // Should not reach here. return 0; }
- rec(n, RED) = rec(n-1, BLUE)。
- rec(n, ORANGE) = rec(n-1, RED), rec(n-1, BLUE)。
- rec(n, BLUE) = rec(n-1, RED) + rec(n-1, BLUE) + rec(n-1, ORANGE)。
- 終止條件:rec(1, 不管甚麼顏色) = 1。

227 - 彩帶問題 - 解答嗎(?)
Exercise II - 彩帶問題 - 4
// RED = 0, ORANGE = 1, BLUE = 2 long long rec(int n, int type) { if (n == 1) return 1; else if (type == RED) return rec(n-1, BLUE); else if (type == BLUE) return rec(n-1, ORANGE) + rec(n-1, BLUE) + rec(n-1, RED); else if (type == ORANGE) return rec(n-1, BLUE) + rec(n-1, RED); else // Should not reach here. return 0; }
- rec(n, RED) = rec(n-1, BLUE)。
- rec(n, ORANGE) = rec(n-1, RED), rec(n-1, BLUE)。
- rec(n, BLUE) = rec(n-1, RED) + rec(n-1, BLUE) + rec(n-1, ORANGE)。
- 終止條件:rec(1, 不管甚麼顏色) = 1。
怎麼會吃TLE呢??

Memoization
記憶化

遞迴怎麼可以這麼慢啊...
Memoization 記憶化 - 0
- 讓我們想想之前的遞迴樹。
- 所以答案為6。
- 你會發現一件事情,在分析的時候,有些遞迴會重複,這個時候用之前算過的值就好了。
- 例如C(2, 1)有算過兩次,第二次直接看第一次算的值(=2)就好了。
- 沒道理我們可以省略,
程式省略不了啊?

什麼是記憶化?
Memoization 記憶化 - 1
- 就是把以前算過的答案,丟在陣列記起來。
如果下次用到的時候直接給答案,不要再重算了! - 記在哪裡?全域變數就可以啦!
- 舉個例子,記憶化的斐波那契數列會長的像右下角這樣:
- 用visit陣列來表示有沒有算過。
- 用DP陣列紀錄答案。
-
所以,實作的時候,如果遞迴時發現...
- 曾經算過(visit = true):
- 直接回傳紀錄的答案。
- 沒算過(visit = false):
- 遞迴算答案,紀錄起來,並標示算過。
- 曾經算過(visit = true):
Memorization
bool visit[MAXN]; int DP[MAXN]; int f(int n){ if(n == 1 || n == 2) return 1; else if (visit[n]) return DP[n]; else { visit[n] = true; DP[n] = f(n-1) + f(n-2); return DP[n]; } }
- MAXN表示最大的N,是個常數。

更複雜一點記憶化 - C(n, m)
Memorzation 記憶化 - 2
- 在有兩個參數的遞迴中,記憶化也是可以使用的!
- 舉例來說,C(n, m)的記憶化會長的像下面這樣:
- 有沒有覺得記憶化的招數都一樣啊? 還真的都一樣。
Memoization
bool visit[MAXN][MAXM]; int DP[MAXN][MAXM]; int C(int n, int m){ if(n == 0 || n == m) return 1; else if (visit[n][m]) return DP[n][m]; else { // visit[n][m] == false visit[n][m] = true; DP[n][m] = C(n-1, m-1) + C(n-1, m); return DP[n][m]; } }
- MAXN,MAXM表示
最大的N和最大的M,
是個常數。

自己實作記憶化! - 彩帶問題
Memoization 記憶化 - 3
- 那麼,你會把彩帶問題的遞迴記憶化嗎?
Memoization
// RED = 0, ORANGE = 1, BLUE = 2 long long rec(int n, int type) { if (n == 1) return 1; else if (type == RED) return rec(n-1, BLUE); else if (type == BLUE) return rec(n-1, ORANGE) + rec(n-1, BLUE) + rec(n-1, RED); else if (type == ORANGE) return rec(n-1, BLUE) + rec(n-1, RED); else // Should not reach here. return 0; }
5 mins

自己實作記憶化! - 彩帶問題
Memoization 記憶化 - 4
- 那麼,你會把彩帶問題的遞迴記憶化嗎?
Memoization
// RED = 0, ORANGE = 1, BLUE = 2 bool visit[MAXN][MAXTYPE]; long long DP[MAXN][MAXTYPE]; long long rec(int n, int type) { if (n == 1) return 1; else if (visit[n][type]) return DP[n][type]; else if (type == RED) DP[n][type] = rec(n-1, BLUE); else if (type == BLUE) DP[n][type] = rec(n-1, ORANGE) + rec(n-1, BLUE) + rec(n-1, RED); else if (type == ORANGE) DP[n][type] = rec(n-1, BLUE) + rec(n-1, RED); else // Should not reach here. return 0; visit[n][type] = true; return DP[n][type]; }
5 mins

Euclidean Algorithm
輾轉相除法

我們先來想想看...
Euclidean Algorithm 輾轉相除法 - 0
- 怎麼算出GCD(5, 12)呢?
最大公因數
- 因式分解後找相同,但有沒有更快的方法呢?
- 兩個數字一直互減,就會減到剩下最大公因數。
(另一個就會變成是0)

輾轉相除法的基本算法 & 小小驗證
Euclidean Algorithm
- 兩個數字一直互減,就會減到剩下最大公因數。
(另一個就會變成是0)
- 如果 ,那麼下面這個式子成立
- 如果 是 的公因數,令 。
- 你會發現 也同時會是 和 的公因數。
Euclidean Algorithm 輾轉相除法 - 1

輾轉相除法的基本算法 & 小小驗證
Euclidean Algorithm
- 如果 ,那麼下面這個式子成立。
- 那麼下面也會成立。(因為%就是減到不能再減。)
- 如果說已經減到其中一個為0呢?
- 同理, ,那麼下面式子也會成立。
- 到這裡,你會寫輾轉相除法了嗎?
Euclidean Algorithm 輾轉相除法 - 2

輾轉相除法的程式實作
Euclidean Algorithm
- 如果 ,那麼下面這個式子成立。
- 同理, ,那麼下面式子也會成立。
int GCD(int a, int b){ if(a == 0 || b == 0) return a + b; if (b >= a) return GCD(a, b % a); else return GCD(a % b, b); }
GCD的小小程式
Euclidean Algorithm 輾轉相除法 - 3
- 如果說已經減到其中一個為0呢?

輾轉相除法的更簡單程式實作
Euclidean Algorithm
- 其實GCD可以寫成右上角的小小小小程式。
- 為什麼?我們來想想看:
- 如果a比b大,GCD(a, b)=GCD(b, a%b)
- 如果b比a大,GCD(a, b)=GCD(b, a%b)=GCD(b, a)
-
你發現了嗎? 不管a,b大小怎麼樣,下一次遞迴呼叫後,比較大的數字都會在前面。
- 如果a比b大, b > a%b。
- 如果b比a大,b > a。
- 所以,只要移動一下下一次遞迴的參數位置,就可以確保比較大的都在前面。
- 為什麼?我們來想想看:
int GCD(int a, int b){ if(a == 0 || b == 0) return a + b; if (b >= a) return GCD(a, b % a); else return GCD(a % b, b); }
GCD的小小程式
int GCD(int a, int b){ if(a * b == 0) return a + b; return GCD(b, a % b); }
GCD的小小小小程式
Euclidean Algorithm 輾轉相除法 - 4

Exercise III
Rhythm Doctor - 最大公因數的應用問題

Exercise III
程式練習題
接下來有一題請你用程式實做遞迴的應用題。
- 我會花1分鐘讀題。
- 總共計時10分鐘。
- 計時之前,好好複習一下GCD的程式。

229 - Rhythm Doctor - 工商
Exercise III - Rhythm Doctor - 0
- 分享一個 Rhythm Doctor 我最喜歡的一首。
10 mins

229 - Rhythm Doctor - 題目
- 題目:每個人的生命中都有各自的拍數。假設大家的拍子都從第1秒開始,
請問下一次大家剛好在同個時間打拍子是什麼時候?
(保證答案在long long範圍內。)
- 舉例來說,如果有兩個人AB,A和B的拍數分別為3和4,
那麼下次同時打拍子會在第12秒。
10 mins
Exercise III - Rhythm Doctor - 1
A
B
3s
3s
3s
3s
4s
4s
4s

229 - Rhythm Doctor - 解析
- 題目:每個人的生命中都有各自的拍數。假設大家的拍子都從第1秒開始,
請問下一次大家剛好在同個時間打拍子是什麼時候?
(保證答案在long long範圍內。)
- ㄚ就是多個數字的LCM(最小公倍數) 啊?
- 記得要把code改成long long。
10 mins
Exercise III - Rhythm Doctor - 2
long long GCD(long a, long b){ if(a * b == 0) return a + b; return GCD(b, a % b); }
long long LCM(long long a, long long b){ return a / GCD(a, b) * b; }

229 - Rhythm Doctor - 解析
- 題目:每個人的生命中都有各自的拍數。假設大家的拍子都從第1秒開始,
請問下一次大家剛好在同個時間打拍子是什麼時候?
(保證答案在long long範圍內。)
- ㄚ就是多個數字的LCM(最小公倍數) 啊?
- 就一直拿數字做LCM就好了。
10 mins
Exercise III - Rhythm Doctor - 3
int main() { long long n, ans = 1, x; cin >> n; for (int i=0; i<n; i++) { cin >> x; ans = LCM(ans, x); } cout << ans << endl; }

Exercise IV
零和問題 - 暴力搜尋所有可能

Exercise IV
程式練習題
接下來有一題請你用程式實做遞迴的應用題。
- 我會花1分鐘讀題。
- 總共計時10分鐘。
- 這題很不簡單,不寫出來沒關係,但先想想看你要怎麼寫以及遞迴的過程。

228 - 零和問題 - 題目
Exercise IV - Subset Sum Problem 零和問題 - 0
> 20 mins
- 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k?
如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
- 舉例來說,如果有五個數字[3, 7, 11, 9, 4],請問可不可以湊到數字16呢?
- 輸出YES,因為 7 + 9 = 16。
- 好像很難也?Hint: 我們來畫畫看這題的遞迴樹。

228 - 零和問題 - 提示
> 20 mins
- 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k?
如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
(0, 0)
(1, 3)
(1, 0)
(2, 10)
(2, 7)
(2, 3)
(2, 0)
(3, 21)
(3, 14)
(3, 10)
(3, 3)
(4, 30)
(4, 19)
(4, 21)
(4, 10)
(5, 34)
(5, 25)
(5, 30)
(5, 21)
前
四
種
可
能
(看第幾個數字,
現在加到哪裡)
選
不選
+3
+7
+7
+11
+11
+9
+9
+4
+4
- 舉例來說,如果有五個數字[3, 7, 11, 9, 4],請問可不可以湊到數字16呢?
Exercise IV - Subset Sum Problem 零和問題 - 1

228 - 零和問題 - 引導
> 20 mins
- 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k?
如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
- 根據我們所寫的遞迴樹,我們會知道怎麼暴力搜尋。
- 一開始看第0個,接下來我們分成兩個case來看:
- 選第0個數字 (3),接下來看第1個數字,分成兩個case來看:
- 選第1個數字 (10),接下來看第2個數字,分成兩個case來看...
- 不選第1個數字 (3),接下來看第2個數字,分成兩個case來看...
-
不選第0個數字 (0),接下來看第1個數字,分成兩個case來看:
- 選第1個數字 (7),接下來看第2個數字,分成兩個case來看...
- ....
- 選第0個數字 (3),接下來看第1個數字,分成兩個case來看:
- 一開始看第0個,接下來我們分成兩個case來看:
- 舉例來說,如果有五個數字[3, 7, 11, 9, 4],請問可不可以湊到數字16呢?
Exercise IV - Subset Sum Problem 零和問題 - 2

228 - 零和問題 - 引導
> 20 mins
- 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k?
如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
- 遞迴參數:現在看到第幾個數字 i,以及現在總和 S。
- 要暴搜rec(i, S)的所有可能,可以分成兩個case暴搜:
- 選第i個數字:rec(i+1, S+A[i])
- 不選第i個數字:rec(i+1, S)
- 終止條件?
- 所有數字都看完了。
- i = n。
- 所有數字都看完了。
- 要暴搜rec(i, S)的所有可能,可以分成兩個case暴搜:
Exercise IV - Subset Sum Problem 零和問題 - 3

228 - 零和問題 - 解答
> 20 mins
- 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k?
如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
bool rec (int i, int S) { if (i == n) return S == k; else return rec(i+1, S+A[i]) || rec(i+1, S); }
- 遞迴參數:現在看到第幾個數字 i,以及現在總和 S。
- 要暴搜rec(i, S)的所有可能,可以分成兩個case暴搜:
- 選第i個數字:rec(i+1, S+A[i]) / 不選第i個數字:rec(i+1, S)
- 終止條件?所有數字都看完了,也就是i = n。
- 要暴搜rec(i, S)的所有可能,可以分成兩個case暴搜:
Exercise IV - Subset Sum Problem 零和問題 - 4

228 - 零和問題 - 比較詳細的解答
> 20 mins
#include <iostream> #define MAXN 30 using namespace std; int k, A[MAXN], n; bool rec (int i, int S) { if (i == n) return S == k; else return rec(i+1, S+A[i]) || rec(i+1, S); } int main () { cin >> n >> k; for (int i=0; i<n; i++) cin >> A[i]; cout << (rec(0, 0) ? "YES" : "NO") << endl; }
- 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k?
如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
Exercise IV - Subset Sum Problem 零和問題 - 5

Pruning
剪枝

什麼是剪枝?
Pruning 剪枝 - 1
- 就是不要讓遞迴跑的太多次!
- 簡單來說,如果接下來一定不會找到答案,我們就別再找下去了,
直接停掉避免浪費時間。 - 在遞迴樹上砍掉一些點 → 所以這個技巧被稱作剪枝(Pruning)。
- 簡單來說,如果接下來一定不會找到答案,我們就別再找下去了,
Pruning


在零和問題剪枝?
- 來看看剛剛遞迴樹怎麼剪枝吧!
Pruning
Pruning 剪枝 - 2
- 舉例來說,如果有五個數字[3, 7, 11, 9, 4],請問可不可以湊到數字16呢?
- 把不可能的遞迴跳掉!
(0, 0)
(1, 3)
(1, 0)
(2, 10)
(2, 7)
(2, 3)
(2, 0)
(3, 21)
(3, 14)
(3, 10)
(3, 3)
(4, 19)
(4, 10)
(5, 25)
(5, 10)
(看第幾個數字,
現在加到哪裡)
選
不選
+3
+7
+7
+11
+11
+9
+4
(4, 23)
(4, 14)
+9
(5, 19)
(5, 14)
+4

在零和問題剪枝?
- 把不可能的遞迴跳掉!
- 還可以減掉更多枝嗎?
- 如果題目數字有可能是負數,還可以這樣做嗎?
Pruning
Pruning 剪枝 - 2
bool rec (int i, int S) { if (S > k) return false; if (i == n) return S == k; else return rec(i+1, S+A[i]) || rec(i+1, S); }

Talks
關於遞迴的小小雜談

題目表 - 遞迴
題目表 - 0

Top-down & Bottom-up
Talks - 1
- 其實你隱約知道了,有些遞迴是可以用for迴圈寫的。
- 我們來複習用for迴圈實作斐波那契數列?
- 你會發現,用for迴圈通常都是慢慢堆答案上去的,
- 這類的方法稱作Bottom-up,也就是堆上去的方法。
- 那麼遞迴呢?通常遞迴都是從大問題去呼叫小問題的,
這類的方法稱作為Top-down。
- 「遞迴只應天上有,凡人應當用迴圈」
int fib[100] = {0, 1, 1}; for (int i=3; i<=n; i++) { fib[i] = fib[i-1] + fib[i-2]; }

Talks - 2
- 想要知道Top-down & Bottom-up的後續嗎?
- 去看看算法班的動態規劃 DP (Dynamic Programming)吧! (蛋餅的投影片)
- 但並不是所有題目都適合用Bottom-up,所以還是要會遞迴喔~
Top-down & Bottom-up


更多的遞迴...
Talks - 3
- 其實最後一個問題 - 零和問題對初心者來說很難啦,
所以可以回去在複習一次OuO。 - 雖然零和問題很難,但是這才是遞迴的剛開始而已!
- 如果你對遞迴很有興趣,歡迎參考我在資訊之芽的遞迴講義,
- 這個講義有四分之一我們剛剛講過了,
但後半段比我們這堂課還要難很多喔! 例如我們有提及...- 河內塔
- 字串枚舉
- 數獨
- 搶數字問題
- 這個講義有四分之一我們剛剛講過了,
- 這是我們最後一堂課了QQ,祝你們明天模擬考順利!

End
Q&A?

其實...
Talks - 0
- 在APCS實作題裡面,其實幾乎沒有裸遞迴題。
- 但是遞迴非常的重要!
- 在算法班的課程裡,除了資料結構 / 貪心以外,一定會用到遞迴
(其他不是用不到,也可能會用到遞迴) - 遞迴的概念對新手來說很不好理解,要慢慢適應遞迴的思考方法。

APCS camp - Recursion
By Arvin Liu
APCS camp - Recursion
APCS Camp Recursion
- 1,633