跳转至

动态规划*

参考:

动态规划的一般形式就是求最值。核心问题是穷举,主要是存在重叠子问题,通过记录避免重复计算。

最难的是写出状态转移方程,主要的框架为:

明确状态 -> 定义 dp 数组/函数 -> 明确选择 -> 明确初态

下面将通过两个问题理解动态规划思想。

斐波那契*

最简单的斐波那契就是用递归,但是单纯的递归会重复计算子问题。我们可以通过添加备忘录来剪枝,减少子问题个数。

递归是自顶而下,而动态规划使用自底向上,因此往往通过循环迭代实现。

由此得到基于 dp 数组的斐波那契:

int fib(int n) {
    vector<int> dp(n + 1, 0);
    dp[1] = dp[2] = 1;
    for(int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

此问题的状态转移方程为:

f(x)=\left\{ \begin{aligned} 1 & , & n=1,2 \\ f(n-1)+f(n-2) & , & n>2 \end{aligned} \right.

当然,常用的还是用两个变量代替 dp 数组,空间复杂度 O(1)。

凑零钱*

给 k 种面值的硬币,每种数量无限,要求用最少的金币数凑出金额 amount。

首先这个问题是动态规划问题,因为具有最优子结构,子问题相互独立。

那么如何列出正确的状态转移方程?按照之前提出的框架,先确定状态,硬币数量无限,则唯一的状态就是目标金额 amount。然后确定 dp 含义,目标金额为 n,至少需要 dp(n) 个硬币凑出金额。然后确定选择而择优,就是对每个状态,可以做出什么选择改变当前状态。具体到此问题,就是选择一个硬币,目标金额就减少。最后明确初始态,目标金额为 0,所需硬币数量为 0。

dp(n) = \left\{ \begin{aligned} 0 & , & n = 0 \\ -1 & , & n < 0 \\ \min\{dp{n - coin} + 1\ | coin \in coins\} & , & n > 0 \end{aligned} \right.
vector<int>& coins;
int amount;

int dp(int n) {
    // base case
    if(n == 0) {
        return 0;
    }else if(n < 0) {
        return -1;
    }else {
        for()
    }
    int res = -1;
    for(auto coin : coins) {
        int subProblem = dp(n - coin);
        if(subProblem == -1) {
            continue;
        }
        res = min(res, 1 + subProblem);
    }
    return res;
}
int coinChang(vector<int>& coins, int amount) {
    // 取最小值,所以可以把初值设大一点,最多就是 amount + 1
    vector<int> dp(amount + 1, amount + 1);
    // base case:
    dp[0] = 0;
    for(int i = 1; i <= amount; i++) {
        // 内层循环求所有子问题 + 1 的最小值
        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];
}

最后更新: July 23, 2022