這一節我們向大家介紹「最優子結構」這個概念,具體來說就是:問題的最優解參考了子問題的最優解。
這個提法比較學術,我們還是用具體的例子和大家解釋。這道題是「力扣」上第 322 號問題:零錢兌換。
給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回
-1
。
示例 1:
輸入: coins = [1, 2, 5], amount = 11
輸出: 3
解釋: 11 = 5 + 5 + 1
示例 2:
輸入: coins = [2], amount = 3
輸出: -1
說明:
你可以認爲每種硬幣的數量是無限的。
思路:
- 看題目的問法,只問最優值是多少,沒有要我們求最優解,一般情況下就是「動態規劃」可以解決的問題。
- 最優子結構其實比較明顯,我們看示例 1:
輸入: coins = [1, 2, 5], amount = 11
湊成面值爲 11
的最小硬幣數可以由以下 者的最小值得到:
1、湊成面值爲 10
的最小硬幣數(假設已知) + 面值爲 1
的這一枚硬幣;
2、湊成面值爲 9
的最小硬幣數(假設已知) + 面值爲 2
的這一枚硬幣;
3、湊成面值爲 6
的最小硬幣數(假設已知) + 面值爲 5
的這一枚硬幣;
即 dp[11] = min (dp[10] + 1, dp[9] + 1, dp[6] + 1)
。這就是這個問題的最優子結構,在三種選擇中,選出一個最優解。
這裏需要引入一個概念:狀態。狀態其實我們在「回溯算法」裏介紹說。狀態在動態規劃裏其實含義是一樣的,依然是表示我們求解一個問題進行到哪個階段,只不過表現這個變量不想「回溯算法」那麼具體,很多時候,它是一個「概括值」。
我們這裏直接把題目的問法設計成「狀態」,有些問題不是這樣的,我們後面再說。
第 1 步:定義「狀態」
dp[i]
:湊齊總價值 i
需要的最少硬幣數,狀態就是問的問題。
第 2 步:寫出「狀態轉移方程」
所謂「狀態轉移方程」,其實就是「最優子結構」。
根據對具體例子的分析:
dp[amount] = min(1 + dp[amount - coin[i]]) for i in [0, len - 1] if coin[i] <= amount
注意的是:
1、首先硬幣的面值首先要小於等於當前要湊出來的面值;
2、剩餘的那個面值應該要能夠湊出來,例如:求 dp[11]
需要參考 dp[10]
,如果不能湊出來的話,dp[10]
應該等於一個不可能的值,可以設計爲 11 + 1
,也可以設計爲 -1
,它們的區別只是在具體的代碼編寫細節上不一樣而已。
再強調一次:新狀態的值要參考的值以前計算出來的「有效」狀態值。這一點在編碼的時候需要特別注意。
因此,不妨先假設湊不出來,因爲比的是小,所以初始化的時候應該設置爲一個不可能的數。
參考代碼:
import java.util.Arrays;
public class Solution {
public int coinChange(int[] coins, int amount) {
// 給 0 佔位
int[] dp = new int[amount + 1];
// 注意:因爲要比較的是最小值,這個不可能的值就得賦值成爲一個最大值
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i - coin >= 0 && dp[i - coin] != amount + 1) {
dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
}
}
}
if (dp[amount] == amount + 1) {
dp[amount] = -1;
}
return dp[amount];
}
}
注意:
- 要求的是恰好填滿,所以初始化的時候需要賦值爲一個不可能的值:
amount + 1
。只有在有「正常值」的時候,「狀態轉移」纔可以正常發生。
總結
可能有的朋友要問了,斐波拉契數列貌似沒有「最優子結構」,事實上的確是這樣,嚴格來說「斐波拉契數列」不是「動態規劃」問題,但它卻是理解「動態規劃」問題的一個例子,主要是通過這個例子理解「動態規劃」「自底向上」求解的思想和「重複子問題」的特徵。大家先不要去糾結這件事情。
這節我們向大家介紹了「最優子結構」。希望大家能夠體會,我們在設計「狀態」的時候,僅僅只是用一個數值表示了求解一個問題的階段,所以這個數值是一個「概括性」的數值,它不是具體解,但是它可以代表具體解。
這裏要注意:對「狀態」的定義一定要非常準確,在這裏我的建議是,如果狀態定義不是題目問的那個樣子,把我們對狀態的定義都作爲註釋寫在代碼裏。
只有「狀態」定義準確,「狀態轉移方程」纔會「準確」。
其實求解這個問題,還利用到了一個「動態規劃」問題的一個特點「無後效性」。我們在下一節向大家解釋。
練習
1、「力扣」第 279 題:完全平方數;