RRRecursion
Arvin Liu
RRRRRRRRRRRRRRRRRRRRR
什麼是遞迴?
什麼是遞迴?
感受一下?
什麼是遞迴?
現場來寫寫看吧!
N!
求出 N!
給你一個數字 N,請輸出 N!
那還不簡單?
不就是基礎 for 迴圈嗎?
來想想看有沒有其他寫法!
int factorial(int N) {
int ans = 1;
for (int i=1; i<=N; i++)
ans *= i;
return ans;
}
C++
def factorial(N):
ans = 1
for i in range(1, N):
ans *= i
return ans
Python
求出 N!
給你一個數字 N,請輸出 N!
假設現在有一個函數:
如果不用 "!" 該怎麼寫?
求出 N!
給你一個數字 N,請輸出 N!
直接把它轉成程式?
跑跑看!
int factorial(int N) {
return N * factorial(N-1);
}
def factorial(N):
return N * factorial(N-1)
C++
Python
求出 N!
輸出結果 :
4
3
2
1
0
-1
-2
....
跑不完!
這裡就該停了!
#include <stdio.h>
int factorial(int N) {
printf("%d\n", N);
return N * factorial(N-1);
}
int main() {
printf("%d", factorial(4));
}
def factorial(N):
print(N)
return N * factorial(N-1)
print(factorial(4))
C++
Python
求出 N!
給你一個數字 N,請輸出 N!
這樣就可以了!
int factorial(int N) {
if (N == 0) return 1;
return N * factorial(N-1);
}
def factorial(N):
if N == 0: return 1
return N * factorial(N-1)
C++
Python
怎麼寫遞迴?
怎麼寫遞迴?
-
定義遞迴函式要做什麼? 等於什麼?
- f(n) = n! 的數值
- 觀察如何將大函式用小函式完成?
- f(n) 跟 f(n-1) 有點關係
- 記得寫終止條件 (Base Case)
- f(0) = 1
- 思考的時候,不要從一開始往上推,例如 n=0 怎麼樣,n=1怎麼樣...
- 通常情況下,要逆向思考,習慣倒推。
- 例如 n 要怎麼用n-1表示,或者n-2表示..
記得寫終止條件!
N!遞迴的逐步過程
以 作為舉例
呼叫
呼叫
呼叫
回傳
回傳
回傳
回傳
實際例子可以參考另一個投影片
練習題!
- 如果題目改成 1 + 2 + ... + n,你會寫嗎?
- 有沒有覺得遞迴很沒用?不就迴圈可以寫的事情?試著想想看遞迴有什麼優點吧!
- Clumsy Factorial
小知識: 對google搜尋遞迴
按按看(?)
爬樓梯問題
東東爬樓梯 (zj d212)
東東爬階梯每次可以走一或兩階。
假設階梯有 n 階,那有幾種走法?
🐦
🐦
3 階有 3 種走法
4 階有 5 種走法
1+1+1+1
1+1+2
1+2+1
2+1+1
2+2
1+1+1
1+2
2+1
有點難?
東東爬樓梯 (zj d212)
- 函式定義:
- 如何拆解:
- Base Case:
f(n) = 走 n 階的可能數
f(n-1) f(n-2)
🐦
...
可以將到第n階的走法分成兩類
-
最後一步走1階的
- 最後一步走1階有幾種可能?
- f(n-1)
-
最後一步走2階的
- f(n-2)
+
東東爬階梯每次可以走一或兩階。
假設階梯有 n 階,那有幾種走法?
東東爬樓梯 (zj d212)
- 函式定義:
- 如何拆解:
- Base Case:
f(n) = 走 n 階的可能數
f(n-1) f(n-2)
+
輸入可能是 1 以後的正整數。
f(1) 可以用遞迴算出嗎?
f(2) 可以用遞迴算出嗎?
f(3) 可以用遞迴算出嗎?
⭕
❌
❌
f(1) = 1, f(2) = 2
東東爬階梯每次可以走一或兩階。
假設階梯有 n 階,那有幾種走法?
東東爬樓梯 (zj d212)
- 函式定義:
- 如何拆解:
- Base Case:
f(n) = 走 n 階的可能數
f(n-1) f(n-2)
+
f(1) = 1, f(2) = 2
東東爬階梯每次可以走一或兩階。
假設階梯有 n 階,那有幾種走法?
int fib(int N) {
if (N == 1) return 1;
if (N == 2) return 2;
return fib(N-1) + fib(N-2);
}
def fib(N):
if N == 1: return 1
if N == 2: return 2
return fib(N-1) + fib(N-2)
C++
Python
爬樓梯問題與費波那契數列
這就是鼎鼎大名的費波那契數列
fibonacci
- f(0) = 1
費波那契數列的遞迴過程
以 作為舉例
2
1
3
2
練習題!
- 如果題目改成東東每次可以走 1, 2, 3 階,遞迴會怎麼寫?
- 你會用迴圈寫這題嗎?
- 你覺得
- 遞迴版本的時間複雜度多少?
- 迴圈版本的時間複雜度多少?
C(n, m)
00369 - Combinations (zj d134)
請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?
有 4 顆相異的球
如果取三顆,共 4 種取法
如果取兩顆,共 6 種取法
00369 - Combinations (zj d134)
請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?
有 4 顆相異的球
如果取三顆,共 4 種取法
- 函式定義:
- 如何拆解:
- Base Case:
這個也好難....
C(n, m) = N 取 M 的可能數
00369 - Combinations (zj d134)
請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?
有 4 顆相異的球
如果取三顆,共 4 種取法
- 函式定義:
- 如何拆解:
- Base Case:
取了最
後的球
沒取最
後的球
取了 後
需要再從三顆中取兩顆
放棄 後
需要再從三顆中取三顆
C(n, m) = N 取 M 的可能數
❌
00369 - Combinations (zj d134)
請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?
有 n 顆相異的球
想要取 m 顆
- 函式定義:
- 如何拆解:
- Base Case:
取了最
後的球
沒取最
後的球
❌
...
還剩m顆要選
還剩m-1顆要選
C(n-1, m)
C(n-1, m-1)
+
C(n, m) = N 取 M 的可能數
放棄 後
需要再從n-1顆中取m顆
取了 後
需要再從n-1顆中取m-1顆
00369 - Combinations (zj d134)
請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?
- 函式定義:
- 如何拆解:
- Base Case:
f(N,M) = N 取 M 的可能數
C(n-1, m)
C(n-1, m-1)
+
C(3, 2)
C(2, 1)
C(2, 2)
C(1, 0)
C(1, 1)
- m = 0 的時候,表示不取,答案為1
- m = n 的時候,表示全取,答案為1
n 跟 m 減到哪裡該停?
想想看 C(3, 2) 的例子吧!
m = 0 或者 n = m 時為1
00369 - Combinations (zj d134)
請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?
- 函式定義:
- 如何拆解:
- Base Case:
f(N,M) = N 取 M 的可能數
C(n-1, m)
C(n-1, m-1)
+
m = 0 或者 n = m 時為1
一個看似複雜的題目,遞迴程式卻非常少!
這就是遞迴的魅力之處 (?)
int C(int n, int m){
if (m == 0 || m == n) return 1;
return C(n-1, m-1) + C(n-1, m);
}
def C(n, m):
if m == 0 or m == n: return 1
return C(n-1, m) + C(n-1, m-1)
C++
Python
練習題!
- C(n, m) 其實是有公式。
-
- 使用這個公式如果直接在 C++ 用 for 實作,其實會有些問題在,你知道為什麼嗎?
-
0/1 背包問題
祖靈好孝順 ˋˇˊ (zj a587)
給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。
請問祖靈最多可以帶價值東西回家?
拿走什麼? | 總重 | 價值 |
---|---|---|
G+C+S | 15kg | $8 |
Y+C+S+B | 8kg | $15 |
... | ... | ... |
C
B
Y
G
S
祖靈好孝順 ˋˇˊ (zj a587)
給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。
請問祖靈最多可以帶價值東西回家?
- 函式定義:
f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值
有點複雜... 試試看裸套題目定義
f(2, 5): 如果可以拿0~2號的物品,
背包限重為5,回傳所有可能中的的最大價值。
f(7, 10): 如果可以拿0~7號的物品,
背包限重為10,回傳所有可能中的最大價值。
祖靈好孝順 ˋˇˊ (zj a587)
給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。
請問祖靈最多可以帶價值東西回家?
- 函式定義:
- 如何拆解:
又卡住了 :( ...
其實背包問題跟 C(n,m) 很像!
都是 n 個物品裡面挑幾個東西出來
- C(n, m) 的限制是只能挑 m 個
- 背包問題的限制是挑出來的物品限重不能超過 W
f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值
祖靈好孝順 ˋˇˊ (zj a587)
給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。
請問祖靈最多可以帶價值東西回家?
- 函式定義:
- 如何拆解:
又卡住了 :( ...
f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值
有 n 顆相異的物品,
限重 W
取了最
後的
沒取最
後的
❌
...
💎
👑
📿
💎
💎
0/1 背包問題
有 n 顆相異的球
想要取 m 顆
取了最
後的球
沒取最
後的球
❌
...
C(n, m) 問題
祖靈好孝順 ˋˇˊ (zj a587)
給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。
請問祖靈最多可以帶價值東西回家?
- 函式定義:
- 如何拆解:
有 n 顆相異的物品,
限重 W
取了最
後的
沒取最
後的
❌
...
💎
👑
📿
💎
💎
放棄第 n 個物品,
當它不存在。
可以裝的重量剩
但多了價值
f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值
背包還剩
背包還剩
祖靈好孝順 ˋˇˊ (zj a587)
給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。
請問祖靈最多可以帶價值東西回家?
- 函式定義:
- 如何拆解:
- Base Case :
f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值
n 會一直往下減... 考慮第零號物品!
祖靈好孝順 ˋˇˊ (zj a587)
- 函式定義:
- 如何拆解:
- Base Case :
f(n, W) = 前 n 個物品中,限重 W 的情況下的最大價值
int f(int n, int w) {
if (n == 0)
return w >= W[n] ? V[n] : 0;
if (w >= W[n])
return max(
f(n-1, w),
f(n-1, w - W[n]) + V[n]
);
return f(n-1, w);
}
def f(n, w):
if n == 0:
return V[n] if w >= W[n] else 0
if w >= W[n]:
return max(
f(n-1, w),
f(n-1, w-W[n]) + V[n]
);
return f(n-1, w);
C++
Python
練習題!
- 如果將有個函數 f(n, V) 的定義為n個物品裡面選到價值為V的最低重量
( 例如f(n,10)=3 表示選到價值為10的物品時,重量最低為3 )
那麼你會寫出遞迴式嗎?
- 你要怎麼使用第一題的函數定義來找出限重為 W 的背包最高價值為何?
- 如果每個物品都可以拿無限次,請問你有辦法改寫遞迴式嗎?
記憶化
記憶化 Memoization
為什麼 fib(40) 很慢?
試試看算出 fib(40) !
理論上不該啊?
記憶化 Memoization
我們以爬樓梯的題目來看
fib(3)一樣
卻會重算!
fib(4)
fib(3)
fib(2)
fib(2)
fib(1)
fib(5)
fib(3)
fib(2)
fib(1)
記憶化 Memoization
如何避免重算?
想像有個函數...
輸入 A
輸入 B
輸入 A
第一次碰到!
算出f(A)
第一次碰到!
算出f(B)
算過 A 了!
拿出之前算過的答案
你必須在第一次碰到的時候記錄起來!
記憶化 Memoization
- 如果沒算過,就算出答案,記錄起來。
- 如果有算過,就直接從記憶裡撈出來。
用一個方法可以記錄 「問題」 -> 「答案」
例如 陣列、unordered_map、dict 等等
long long dp[1000];
long long fib(int n){
if (n == 0 || n == 1)
return 1;
if (!dp[n])
dp[n] = fib(n-1) + fib(n-2);
return dp[n];
}
dp = {}
def fib(n):
if n == 0 or n == 1 :
return 1
if n not in dp:
dp[n] = fib(n-1) + fib(n-2)
return dp[n]
C++
Python
因為 fib 都會 > 0,所以我們可以用
dp[n] 是不是 0 來決定有沒有算過。
記憶化 Memoization
來分析看看複雜度吧!
重複計算 (原本的code)
不重複計算 (記憶化)
f(n) = f(n-1) + f(n-2)
"大概"每多一個n,
就會多算一倍
算完 f(n-1) 時,
f(n-2) 已經算過了,不用重算
差超多
記憶化 Memoization
long long dp[1000];
long long f(int n){
if (n == 0 || n == 1)
return 1;
if (!dp[n])
dp[n] = f(n-1) + f(n-2);
return dp[n];
}
來用人腦跑一次看看吧!如果呼叫 f(4)...
n | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
f(n) | 1 | 1 | ? | ? | ? |
- | f(1) | f(2) | f(3) | ||
- | f(0) | f(1) | f(2) |
1
1
2
2
1
3
3
2
5
f(2) 不用再重新遞迴一次 f(1) 跟 f(0) 了!
練習題
- 請問背包問題 (祖靈好孝順 ˋˇˊ) 經過記憶化後的時間複雜度是多少?
- 我們確信問題的答案都大於零,所以才可以使用 !dp[n] 來表示沒有算過。
那麼如果答案可能是 0 怎麼辦?
- 如果參數的範圍很大,大到無法開陣列,但問題的種類數量很小 (DP為稀疏陣列),那怎麼使用記憶化?
(例如 dp[n] 的 n 有可能超大,但 dp[n] 不為0的值其實沒有那麼多,那要怎麼節省空間?)
遞迴小結
遞迴小結
-
定義遞迴函式要做什麼? 等於什麼?
- f(n) = n! 的數值
- 觀察如何將大函式用小函式完成?
- f(n) 跟 f(n-1) 有點關係
- 記得寫終止條件 (Base Case)
- f(0) = 1
- 如果遞迴有可能會重複計算,
使用記憶化 (Memoization)
遞迴小結
-
「遞迴只應天上有,凡人應當用迴圈」
- 看起來好像有些遞迴可以用迴圈寫啊?
- 那就用迴圈寫。
- 那為甚麼要用遞迴寫code?
- 有些題目用迴圈還真的寫不出來...
- 為了寫快一點! (遞迴的code通常很短)
- 注意 stack overflow.
Stack Overflow
什麼是Stack Overflow?
不是指某個回答問題的網站。
什麼是Stack Overflow?
在函式裡宣告的變數都會在stack空間。 (包括main)
當你呼叫非常非常非常多的函式...
stack
警戒線
超出stack警戒線就會導致程式執行錯誤。
(所以是Runtime Error)
怎麼解決Stack Overflow?
stack
警戒線
- 不要用遞迴寫 (這在幹話?)
- 如果可以,用for迴圈寫。
(例如fib可直接用for。)
- 如果可以,用for迴圈寫。
- 如果不行
- 使用全域變數或heap空間,用別的方法模擬遞迴 (詳見圖論投影片-bfs/dfs)
黑魔法 - 內嵌組合語言強制遞迴用heap空間
黑魔法 - 強制調用heap空間
extern int main2(void) __asm__ ("main2");
int main2() {
run();
exit(0);
}
int main() {
// 跟heap借256MB
int size = 256 << 20;
char *p = (char *)malloc(size) + size;
__asm__ __volatile__(
"movq %0, %%rsp\n"
"pushq $exit\n"
"jmp main2\n"
:: "r"(p));
}
- 要
- 把你原本程式的main改名成run。
- 把上面這份code貼在你的程式的最下面。
#include <cstdlib>
輾轉相除法
Euclidean Algorithm
同餘定理
同餘定理
在猥瑣罐頭下樓梯(zj a272) 中,
除了要寫東東爬樓梯以外,
多了一個奇怪的條件...
你可能會輸出
但在運算的過程中數字太大噴掉...
在每次運算完後直接%就可以避免溢位!
練習題
- (a * b) % M 有辦法用像 (a + b) % M 一樣的技巧嗎?
- (a ^ b) % M 呢?
- (a - b) % M 呢?
- 如果 a = 7, b = 5, M = 6
- (7 - 5) % 6 = 2
- ((7 % 6 = 1) - (5 % 6 = 5)) % 6 = (1 - 5) % 6
= -4 % 6 ???? (在C++中會是 -4,但python是2) - 你該怎麼解決這個問題?
- 如果 a = 7, b = 5, M = 6
輾轉相除法
輾轉相除法
輾轉相除法
想想看程式要怎麼寫吧!
練習題
- 可以簡化成這樣,想想看為什麼?
- Hint 1: % b 的一種看法-b-b-b...減到不能再減
- Hint 2: 參數的位置不一樣,這確定了 a 跟 b 的一個關係
- 你會求出 LCM (最小公倍數) 嗎?
int GCD(int a, int b){
if(b == 0)
return a + b;
return GCD(b, a % b);
}
2^n 暴力搜尋
n! 暴力搜尋
分治法
河內塔
遞迴的複雜度分析
!數學警告!
遞迴的複雜度分析
假設你學會了時間複雜度...
來上點難度吧!
def f(n):
if n == 0:
return
f(n-1)
這段程式碼的時間分析大概就長這樣
遞迴的複雜度分析
假設你學會了時間複雜度...
來上點難度吧!
遞迴的複雜度分析
來更難的吧!
遞迴的複雜度分析
遞迴的複雜度分析
遞迴的複雜度分析
這種函式的程式碼大概長這樣
f(l, r):
if l+1 == r:
return
m = (l + r) / 2
f(l, m)
f(m, r)
遞迴的複雜度分析
遞迴的複雜度分析
遞迴的複雜度分析
遞迴的複雜度分析
遞迴的複雜度分析 - 常見統整
合併排序
逆序數對
逆敘數對
給定一個數列 <A_n>,
請問有個對 (i, j) 滿足以下條件:
- 函式定義:
- 如何拆解:
- Base Case:
....?
這題老實說直接想是很難想出來的。
我們先來亂拆看看吧!
逆敘數對
1 | 5 | 2 | 7 | 0 | 9 | 3 | 8 | 4 | 6 |
---|
A
f(n) = f(n-1) + A_n 跟前面數字的逆序數對
跟裸做一樣😭
逆敘數對
1 | 5 | 2 | 7 | 0 | 9 | 3 | 8 | 4 | 6 |
---|
A
如果每次切一半有辦法更好嗎...?
如果我們考慮左邊跟右邊,
那麼逆序數對會有三種可能:
- 都在左側的逆序數對
- 都在右側的逆序數對
- 在兩側的逆序數對
怎麼定義函式?
逆敘數對
1 | 5 | 2 | 7 | 0 | 9 | 3 | 8 | 4 | 6 |
---|
A
暴力搜尋兩側逆對:
想這麼多還是跟裸做一樣😭
逆敘數對
想這麼多還是跟裸做一樣😭
等等!如果兩側都是排序好的呢?
0 | 1 | 2 | 5 | 7 |
---|
3 | 4 | 6 | 8 | 9 |
---|
L
R
你想得出來怎麼更有效率的做嗎?
逆敘數對
想這麼多還是跟裸做一樣😭
等等!如果兩側都是排序好的呢?
0 | 1 | 2 | 5 | 7 |
---|
3 | 4 | 6 | 8 | 9 |
---|
L
R
從 L 的每個元素看:
我們目標是要找到 R 有幾個元素比 L 還要小
二分搜!
好像很有機會喔?
逆敘數對
想這麼多還是跟裸做一樣😭
等等!如果兩側都是排序好的呢?
def f(l, r):
- 算出沒有交叉的逆對 f(l, m) + f(m, r)
-
- 排序 [l, m) 以及排序 [m, r)
- 二分搜交叉的逆對
逆敘數對
想這麼多還是跟裸做一樣😭
等等!如果兩側都是排序好的呢?
0 | 1 | 2 | 5 | 7 |
---|
3 | 4 | 6 | 8 | 9 |
---|
L
R
再等等!
好像有更好的算法!
你會發現指到的地方
永遠是遞增的!
→雙指針
逆敘數對
等等!如果兩側都是排序好的呢?
0 | 1 | 2 | 5 | 7 |
---|
3 | 4 | 6 | 8 | 9 |
---|
L
R
→雙指針
l
r
r
r
r
l
l
l
l
+0
+0
+0
+2
+3
逆敘數對
def f(l, r):
- 算出沒有交叉的逆對 f(l, m) + f(m, r)
-
- 排序 [l, m) 以及排序 [m, r)
- 二分搜交叉的逆對
如果你做 Merge Sort 就不用排序
Karatsuba 大數乘法
APCS 不會考,純介紹性質 :)
大數乘法
實作 A * B,
但數字很大。
小學直式乘法!
陣列 A
陣列 B
陣列 C
好慢...
大數乘法
實作 A * B,
但數字很大。
嘗試切一半!
A_L
A_R
B_R
B_L
A_R * B_R
A_L * B_R
A_R * B_L
A_L * B_L
大數乘法
實作 A * B,
但數字很大。
嘗試切一半!
在做 A*B 的大數乘法的時候,
會需要做四次的一半的乘法
跟裸做一樣😭
大數乘法
實作 A * B,
但數字很大。
A_L
A_R
B_R
B_L
A_R * B_R
A_L * B_R
A_R * B_L
A_L * B_L
四次乘法,一次加法→兩次加法,三次乘法,兩次減法
大數乘法
實作 A * B,
但數字很大。
四次乘法,一次加法→兩次加法,三次乘法,兩次減法
- f(A, B):
- 左邊的: f(A_L, B_L)
- 右邊的: f(A_R, B_R)
- 中間的: f(A_L + A_R, B_L + B_R) - 左邊的 - 右邊的
A_R * B_R
A_L * B_R
A_R * B_L
A_L * B_L
遞迴大結
Recursion 2024
By Arvin Liu
Recursion 2024
Recursion
- 1,581