動態規劃學習筆記
阿新 • • 發佈:2020-11-14
動態規劃
概念
維基:
動態規劃(英語:Dynamic programming,簡稱DP)是一種在數學、管理科學、電腦科學、經濟學和生物資訊學中使用的,通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法。
動態規劃常常適用於有重疊子問題[1]和最優子結構性質的問題,動態規劃方法所耗時間往往遠少於樸素解法。
優勢
動態規劃在查詢有很多重疊子問題的情況的最優解時有效。它將問題重新組合成子問題。為了避免多次解決這些子問題,它們的結果都逐漸被計算並被儲存,從簡單的問題直到整個問題都被解決。因此,動態規劃儲存遞迴時的結果,因而不會在解決同樣的問題時花費時間。
適用情況
- 最優子結構性質。如果問題的最優解所包含的子問題的解也是最優的,我們就稱該問題具有最優子結構性質(即滿足最優化原理)。最優子結構性質為動態規劃演算法解決問題提供了重要線索。
- 無後效性。即子問題的解一旦確定,就不再改變,不受在這之後、包含它的更大的問題的求解決策影響。
- 子問題重疊性質。子問題重疊性質是指在用遞迴演算法自頂向下對問題進行求解時,每次產生的子問題並不總是新問題,有些子問題會被重複計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只計算一次,然後將其計算結果儲存在一個表格中,當再次需要計算已經計算過的子問題時,只是在表格中簡單地檢視一下結果,從而獲得較高的效率,降低了時間複雜度。
核心
- 狀態轉移方程
- 備忘錄(DP Table)
狀態轉移方程思維框架
來自labuladong
# 初始化 base case dp[0][0][...] = base # 進行狀態轉移 for 狀態1 in 狀態1的所有取值: for 狀態2 in 狀態2的所有取值: for ... dp[狀態1][狀態2][...] = 求最值(選擇1,選擇2...)
經典問題
湊零錢問題
給你 k 種面值的硬幣,面值分別為 c1, c2 ... ck,每種硬幣的數量無限,再給一個總金額 amount,問你最少需要幾枚硬幣湊出這個金額,如果不可能湊出,演算法返回 -1 。
解1
def coinChange(coins: List[int], amount: int): # 備忘錄 memo = dict() def dp(n): # 查備忘錄,避免重複計算 if n in memo: return memo[n] # base case if n == 0: return 0 if n < 0: return -1 res = float('INF') for coin in coins: subproblem = dp(n - coin) if subproblem == -1: continue res = min(res, 1 + subproblem) # 記入備忘錄 memo[n] = res if res != float('INF') else -1 return memo[n] return dp(amount)
解2
int coinChange(vector<int>& coins, int amount) {
// 陣列大小為 amount + 1,初始值也為 amount + 1
vector<int> dp(amount + 1, amount + 1);
// base case
dp[0] = 0;
// 外層 for 迴圈在遍歷所有狀態的所有取值
for (int i = 0; i < dp.size(); i++) {
// 內層 for 迴圈在求所有選擇的最小值
for (int coin : coins) {
// 子問題無解,跳過
if (i - coin < 0) continue;
dp[i] = min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}