动态规划:第 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 题:完全平方数

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