Skip to content
On this page

动态规划

求解动态规划的核心问题是穷举

首先,动态规划的穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要求解动态规划的核心问题是穷举。来优化穷举过程,避免不必要的计算。

而且,动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。

另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程」,才能正确地穷举。

以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。在实际的算法问题中,写出状态转移方程是最困难的,思考状态转移方程:

明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义

按上面的套路走,最后的结果就可以套这个框架:

# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 求最值(选择1,选择2...)

斐波那契数列

  • 暴力递归
  • 带备忘录的递归
    • 保存每次计算的结果,取值前先找之前的计算结果
  • dp数组的迭代
    • 用dp数组存储计算的值,循环到目标值停止
  • dp数组的状态压缩
    • 用状态压缩来缩小 DP table 的大小,只记录必要的数据
int fib(int n) {
    if (n < 1) return 0;
    if (n == 2 || n == 1) 
        return 1;
    int prev = 1, curr = 1;
    for (int i = 3; i <= n; i++) {
        int sum = prev + curr;
        prev = curr;
        curr = sum;
    }
    return curr;
}

凑零钱问题

dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出

class Solution {
    public int coinChange(int[] coins, int amount) {
        if(amount==0)return 0;
        // dp数组记录某金额需要的最少硬币数量
        int[] dp = new int[amount+1];
        Arrays.fill(dp,amount+1);

        dp[0] = 0;
        // 遍历所有子状态
        for(int i = 1;i<dp.length;i++){
            // 对每种状态都进行硬币的遍历,寻求最佳的结果
            for(int coin:coins){
                // 如果硬币的值比状态值还大的话,无法满足,跳过
                if(coin>i)
                    continue;
                dp[i] = Math.min(dp[i],dp[i-coin]+1);
            }
        }
        // 如果最后结果的值还是等于初始值,那么说明无解
        return dp[amount] == amount+1 ? -1 : dp[amount];
    }
}

PS:为啥 dp 数组初始化为 amount + 1 呢,因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷,便于后续取最小值。为啥不直接初始化为 int 型的最大值 Integer.MAX_VALUE 呢?因为后面有 dp[i - coin] + 1,这就会导致整型溢出。

文章:https://labuladong.gitee.io/algo/3/23/69/