動態規劃:第 2 節:理解「最優子結構」(自己的草稿,內容不嚴謹,與之前有重複,不用看)

這一節我們向大家介紹「最優子結構」這個概念,具體來說就是:問題的最優解參考了子問題的最優解。

這個提法比較學術,我們還是用具體的例子和大家解釋。這道題是「力扣」上第 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 的最小硬幣數可以由以下 33 者的最小值得到:

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 題:完全平方數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章