Ruby @ Sprout 2022
Ruby@Sprout
遞迴二
Ruby@Sprout
背包問題
Knapsack Problem
記憶化
Memoization
淹水問題
Flood Fill Problem
記憶化
Ruby@Sprout
遞迴二
Memoization
遞迴二 > 記憶化
二項式定理 Binomial Theorem
Ruby@Sprout
二項式定理 Binomial Theorem
遞迴二 > 記憶化
Ruby@Sprout
二項式定理 Binomial Theorem
int binomial(int n, int m) {
if (n == m || m == 0) return 1;
return binomial(n-1, m) + binomial(n-1, m-1);
}
遞迴二 > 記憶化
Ruby@Sprout
樹 Tree
遞迴二 > 記憶化
葉子
根部
根節點
Root Node
葉節點
Leaf Nodes
生活中的樹
Real Life Tree
樹資料結構
Tree Data Structure
Ruby@Sprout
二項式定理 Binomial Theorem
遞迴二 > 記憶化
回顧:當呼叫 時,會發生什麼?
binomial(5, 3)
1
1
1
1
1
1
1
1
1
1
Ruby@Sprout
發現問題:重複計算
遞迴二 > 記憶化
回顧:當呼叫 時,會發生什麼?
binomial(5, 3)
1
1
1
1
1
1
1
1
1
1
耗費大量資源計算重複結果
bad
Ruby@Sprout
解決方案 Solution
遞迴二 > 記憶化
記憶化 (Memoization) 記憶化是電腦科學常用的最佳化技術,主要用來加速方法或函式之間的呼叫,將呼叫過的函式回傳結果暫存,即可避免重複計算。
長話短說
把算過的東西記起來。
Ruby@Sprout
記憶化實作 Implementation of Memoization
遞迴二 > 記憶化
int binom[101][101] = { 0 };
int memoizedBinomial(int n, int m) {
if (binom[n][m] != 0) return binom[n][m];
if (n == m || m == 0) return binom[n][m] = 1;
return (
binom[n][m]
= memoizedBinomial(n-1, m)
+ memoizedBinomial(n-1, m-1)
);
}
Code
Ruby@Sprout
記憶化二項式函式 Memoized Binomial Function
遞迴二 > 記憶化
當呼叫 時
memoizedBinomial(5, 3)
1
1
1
1
1
3
4
2
3
2
3
6
10
Ruby@Sprout
記憶化二項式函式 Memoized Binomial Function
遞迴二 > 記憶化
1
1
1
1
1
3
4
2
3
2
3
6
10
當呼叫 時
memoizedBinomial(5, 3)
Ruby@Sprout
記憶化實作 Implementation of Memoization
遞迴二 > 記憶化
int binom[101][101] = { 0 };
int memoizedBinomial(int n, int m) {
if (binom[n][m] != 0) return binom[n][m];
if (n == m || m == 0) return binom[n][m] = 1;
int res = memoizedBinomial(n-1, m)
+ memoizedBinomial(n-1, m-1);
return binom[n][m] = binom[n][n-m] = res;
}
Even Better Code
Ruby@Sprout
記憶化二項式函式 Memoized Binomial Function
遞迴二 > 記憶化
1
1
1
1
3
4
2
3
6
10
3
當呼叫 時
memoizedBinomial(5, 3)
nice
Ruby@Sprout
記憶化
Ruby@Sprout
遞迴二
Memoization
遞迴二
Ruby@Sprout
背包問題
Knapsack Problem
記憶化
Memoization
淹水問題
Flood Fill Problem
背包問題
Ruby@Sprout
遞迴二
Knapsack Problem
遞迴二 > 背包問題
什麼是背包問題? What is the Knapsack Problem?
情境:你是一個醫生,某天...
(當作放鬆三分鐘吧!)
Ruby@Sprout
什麼是背包問題? What is the Knapsack Problem?
經過簡單搜索,你清點了所有在那位自殺軍官身上的隨身物品。
願他安息,但現在最重要的是要活下去。所以你需要這些物品,
以最提升自己生存的機會。不過你發現,你無法帶走所有物品。
遞迴二 > 背包問題
Ruby@Sprout
什麼是背包問題? What is the Knapsack Problem?
因此,你測量了所有物品的重量並評估能夠帶來的生存價值。試圖在背包負重限制內,帶走使生存機會提高最多的物品組合。
遞迴二 > 背包問題
你,該怎麼選?
[重量] 4 KG
[價值] 6
[重量] 1 KG
[價值] 1
[重量] 1 KG
[價值] 2
[重量] 5 KG
[價值] 6
[重量] 4 KG
[價值] 3
[重量] 5 KG
[價值] 5
負重 10 KG
Ruby@Sprout
選擇策略 Choosing Strategy
遞迴二 > 背包問題
[重量] 4 KG
[價值] 6
[重量] 1 KG
[價值] 1
[重量] 1 KG
[價值] 2
[重量] 5 KG
[價值] 6
[重量] 4 KG
[價值] 3
[重量] 5 KG
[價值] 5
負重 10 KG
一、從輕的開始選,選到不能選
[總價值] 12
Ruby@Sprout
選擇策略 Choosing Strategy
遞迴二 > 背包問題
[重量] 4 KG
[價值] 6
[重量] 1 KG
[價值] 1
[重量] 1 KG
[價值] 2
[重量] 5 KG
[價值] 6
[重量] 4 KG
[價值] 3
[重量] 5 KG
[價值] 5
負重 10 KG
二、從價值大的開始選,選到不能選
[總價值] 14
Ruby@Sprout
選擇策略 Choosing Strategy
遞迴二 > 背包問題
最佳解
[總價值] 14
從輕的選
[總價值] 12
從價值大的選
[總價值] 14
Ruby@Sprout
選擇策略 Choosing Strategy
遞迴二 > 背包問題
以下三選擇策略
1. 從輕的開始選 2. 從價值大的開始選 3. 從價值與重量比值大的開始選
都可以被稱作
貪婪演算法 (Greedy Algorithm)
不保證每次選擇的物品組合都是最佳解!
Ruby@Sprout
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V) (100, 100), (99, 99), (1, 99)
從價值大的選:[] 真正的最佳解:[]
TestCase #2: (W, V, V/W) (1, 50, 50), (100, 100, 1), (2, 4, 2)
從比值大的選:[] 真正的最佳解:[]
Ruby@Sprout
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V)(100, 100), (99, 99), (1, 99)
從價值大的選:[1] 真正的最佳解:[]
TestCase #2: (W, V, V/W) (1, 50, 50), (100, 100, 1), (2, 4, 2)
從比值大的選:[] 真正的最佳解:[]
Ruby@Sprout
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V) (100, 100), (99, 99), (1, 99)
從價值大的選:[1] 真正的最佳解:[]
TestCase #2: (W, V, V/W) (1, 50, 50), (100, 100, 1), (2, 4, 2)
從比值大的選:[] 真正的最佳解:[]
Ruby@Sprout
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V) (100, 100),(99, 99),(1, 99)
從價值大的選:[1] 真正的最佳解:[2, 3]
TestCase #2: (W, V, V/W) (1, 50, 50), (100, 100, 1), (2, 4, 2)
從比值大的選:[] 真正的最佳解:[]
Ruby@Sprout
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V) (100, 100), (99, 99), (1, 99)
從價值大的選:[1] 真正的最佳解:[2, 3]
TestCase #2: (W, V, V/W) (1, 50, 50), (100, 100, 1), (2, 4, 2)
從比值大的選:[] 真正的最佳解:[]
Ruby@Sprout
totalV = 100
totalV = 198
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V) (100, 100), (99, 99), (1, 99)
從價值大的選:[1] 真正的最佳解:[2, 3]
TestCase #2: (W, V, V/W)(1, 50, 50), (100, 100, 1), (2, 4, 2)
從比值大的選:[1] 真正的最佳解:[]
Ruby@Sprout
totalV = 100
totalV = 198
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V) (100, 100), (99, 99), (1, 99)
從價值大的選:[1] 真正的最佳解:[2, 3]
TestCase #2: (W, V, V/W)(1, 50, 50), (100, 100, 1),(2, 4, 2)
從比值大的選:[1, 3] 真正的最佳解:[]
Ruby@Sprout
totalV = 100
totalV = 198
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V) (100, 100), (99, 99), (1, 99)
從價值大的選:[1] 真正的最佳解:[2, 3]
TestCase #2: (W, V, V/W) (1, 50, 50),(100, 100, 1), (2, 4, 2)
從比值大的選:[1, 3] 真正的最佳解:[2]
Ruby@Sprout
totalV = 100
totalV = 198
貪婪演算法失效 Underperformance of Greedy
遞迴二 > 背包問題
WEIGHT_LIMIT := 100 TestCase #1: (W, V) (100, 100), (99, 99), (1, 99)
從價值大的選:[1] 真正的最佳解:[2, 3]
TestCase #2: (W, V, V/W) (1, 50, 50), (100, 100, 1), (2, 4, 2)
從比值大的選:[1, 3] 真正的最佳解:[2]
Ruby@Sprout
totalV = 100
totalV = 198
totalV = 54
totalV = 100
選擇策略 Choosing Strategy
遞迴二 > 背包問題
如何保證每次選擇的物品組合都是最佳解?
枚舉 (Enumeration)
枚舉(列舉、窮舉)所有可以選擇的物品組合,並選擇所有組合中價值最大的。
但,我們該如何枚舉?
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
一、枚舉每個物品的「選與不選」
將物品編號,並以遞迴枚舉所有選法,再檢查有沒有超重
想法
1
2
2
3
3
3
3
選
不選
123
12
13
1
23
2
3
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
一、枚舉每個物品的「選與不選」
int V[N] = { 12, 23, ... };
int W[N] = { 11, 25, ... };
int maxValue = -1;
void choose(int i, int v, int w) {
if (i == N-1) {
if (w <= WEIGHT_LIMIT)
maxValue = max(maxValue, v);
return;
}
choose(i+1, v+V[i], w+W[i]); /* 選 */
choose(i+1, v, w); /* 不選 */
}
choose(0, 0, 0);
cout << maxValue << endl;
實作
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
一、枚舉每個物品的「選與不選」
簡單剪枝
將物品編號,並以遞迴枚舉所有選法,枚舉過程中每一步都檢查有沒有超重。
將物品編號,並以遞迴枚舉所有選法,再檢查有沒有超重
剪枝:在枚舉過程中,提前終止不滿組條件(不合法)的狀態。
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
一、枚舉每個物品的「選與不選」
簡單剪枝
剪枝:在枚舉過程中,提前終止不滿組條件(不合法)的狀態。
1
2
2
3
3
3
3
選
不選
123
12
13
1
23
2
3
例如:物品1本身已超出負重
超重
超重
超重
超重
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
一、枚舉每個物品的「選與不選」
剪枝:在枚舉過程中,提前終止不滿組條件(不合法)的狀態。
簡單剪枝
1
2
3
3
選
不選
23
2
3
超重
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
一、枚舉每個物品的「選與不選」
int V[N] = { 12, 23, ... };
int W[N] = { 11, 25, ... };
int maxValue = -1;
void choose(int i, int v, int w) {
if (i == N-1) {
if (w <= WEIGHT_LIMIT)
maxValue = max(maxValue, v);
return;
}
choose(i+1, v+V[i], w+W[i]); /* 選 */
choose(i+1, v, w); /* 不選 */
}
簡單剪枝
Before
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
一、枚舉每個物品的「選與不選」
int V[N] = { 12, 23, ... };
int W[N] = { 11, 25, ... };
int maxValue = -1;
void choose(int i, int v, int w) {
if (w > WEIGHT_LIMIT) return;
if (i == N-1) {
maxValue = max(maxValue, v);
return;
}
choose(i+1, v+V[i], w+W[i]); /* 選 */
choose(i+1, v, w); /* 不選 */
}
簡單剪枝
After
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
一、枚舉每個物品的「選與不選」
1
2
2
3
3
3
3
選
不選
123
12
13
1
23
2
3
複雜度分析
每個物品有兩個狀態 [選|不選]
若有 n 個物品,則共有 2^n 種狀態
bad
Ruby@Sprout
枚舉 Enumeration
遞迴二 > 背包問題
二、嘗試記憶化
重新設計
Ruby@Sprout
但第一種枚舉方式難以實現記憶化,所以我們需要重新設計函式。
設計一函式 maxValue(i, w) ,回傳: 可以拿 0~i 號物品的情況下,限重 w 可裝下的最大價值
有回傳值,就可記憶參數所對應的回傳結果
例如: 0~2號重量5以內物品組合中最大價值=maxValue(2, 5)
0~7號重量9以內物品組合中最大價值=maxValue(7, 9)
枚舉 Enumeration
遞迴二 > 背包問題
二、嘗試記憶化
重新設計
Ruby@Sprout
我們該如何以遞迴實作出 maxValue 函式?
maxValue(i, w)
將問題切割為數個子問題
動態規劃 (Dynamic Programming)
maxValue(i-1, w)
maxValue(i-1, w-W[i])+V[i]
不選
選
取最大值
枚舉 Enumeration
遞迴二 > 背包問題
二、嘗試記憶化
重新設計
Ruby@Sprout
終止條件:
i < 0
maxValue(i, w)
maxValue(i-1, w)
maxValue(i-1, w-W[i])+V[i]
不選
選
取最大值
枚舉 Enumeration
遞迴二 > 背包問題
二、嘗試記憶化
重新設計
Ruby@Sprout
int W[N] = { 3, 4, 5, 8, ... };
int V[N] = { 1, 6, 6, 11, ... };
int maxValue(int i, int w) {
if (i < 0) return 0;
int res = maxValue(i-1, w);
if (w >= W[i])
res = max(res, maxValue(i-1, w-W[i])+V[i]);
return res;
}
Original
枚舉 Enumeration
遞迴二 > 背包問題
二、嘗試記憶化
重新設計
Ruby@Sprout
int W[N] = { 3, 4, 5, 8, ... };
int V[N] = { 1, 6, 6, 11, ... };
int mv[N][N] = { -1, ..., -1 };
int maxValue(int i, int w) {
if (i < 0) return 0;
if (mv[i][w] != -1) return mv[i][w];
int res = maxValue(i-1, w);
if (w >= W[i])
res = max(res, maxValue(i-1, w-W[i])+V[i]);
return mv[i][w] = res;
}
Memoized
枚舉 Enumeration
遞迴二 > 背包問題
二、嘗試記憶化
Ruby@Sprout
每個狀態只會計算一次
W 為重量限制
複雜度分析
小節 Conclusion
遞迴二 > 背包問題
Ruby@Sprout
閱讀並理解題目
有公式解嗎?
直接套用公式
嘗試枚舉
通過時間限制了嗎?
恭喜!
加上剪枝及記憶化
一、
小節 Conclusion
遞迴二 > 背包問題
Ruby@Sprout
二、
將問題切割為數個子問題
動態規劃 (Dynamic Programming)
活用
寫遞迴並不難,難的是將問題切割為子問題,並找到其之間關係。
多練習、多觀察。
小試身手 Exercise
遞迴二 > 背包問題
Ruby@Sprout
[不算分] 課堂練習:
10 min
背包問題
Ruby@Sprout
遞迴二
Knapsack Problem
遞迴二
Ruby@Sprout
背包問題
Knapsack Problem
記憶化
Memoization
淹水問題
Flood Fill Problem
淹水問題
Ruby@Sprout
遞迴二
Flood Fill Problem
遞迴二 > 深度優先搜尋
淹水問題 Flood Fill Problem
Ruby@Sprout
遞迴二 > 深度優先搜尋
淹水問題 Flood Fill Problem
Ruby@Sprout
1
2
3
4
遞迴二 > 深度優先搜尋
淹水問題 Flood Fill Problem
Ruby@Sprout
Depth-First Search (DFS)
深度優先搜尋
Breadth-First Search (BFS)
廣度優先搜尋
How to flood fill?
該如何淹水?
今日關注的方法
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
深度優先搜尋,顧名思義:
以深度為優先進行「淹水擴散」
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
2
3
4
1
1
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
2
4
2
4
3
1
2
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
3
1
2
3
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
3
1
2
3
4
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
4
1
2
3
4
5
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
4
1
2
3
4
5
6
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
1
7
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
1
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
2
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
3
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
4
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
1
2
3
4
5
6
7
8
9
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
以 3x3 庭院示範:
1
2
3
4
5
6
7
8
9
Done!
遞迴二 > 深度優先搜尋
深度優先搜尋 Depth-First Search
Ruby@Sprout
實作
int grid[N][N] = { EMPTY, ..., EMPTY };
/* grid[y][x] ∈ { EMPTY, WATER, WALL } */
void dfs(int x, int y) {
grid[y][x] = WATER;
if (y-1 >= 0 && grid[y-1][x] == EMPTY)
dfs(x, y-1); /* 向上擴散 */
if (x+1 < N && grid[y][x+1] == EMPTY)
dfs(x+1, y); /* 向右擴散 */
if (y+1 < N && grid[y+1][x] == EMPTY)
dfs(x, y+1); /* 向下擴散 */
if (x-1 >= 0 && grid[y][x-1] == EMPTY)
dfs(x-1, y); /* 向左擴散 */
}
遞迴二 > 深度優先搜尋
小試身手 Exercise
Ruby@Sprout
[算分] 課堂練習:
10 min
遞迴二 > 深度優先搜尋
小試身手 Exercise
Ruby@Sprout
參考解答:
[想法] 遍歷整個 grid ,每見到一格水,就把答案 +1 ,並且在該點做 dfs 把每個連續的水方塊做標記,這樣才不會重複計算水池數量。
[程式碼]
淹水問題
Ruby@Sprout
遞迴二
Flood Fill Problem
遞迴二
Ruby@Sprout
背包問題
Knapsack Problem
記憶化
Memoization
淹水問題
Flood Fill Problem
謝謝大家
Brought to you by Ruby
Ruby@Sprout