零 必读系列 3动态规划解题套路框架

目录

一、斐波那契数列

1. 暴力递归

2. 带备忘录的递归解法

3. 动态规划解法

二、凑零钱问题


动态规划问题一般是求最值问题。

动态规划三要素:重叠子问题、最优子结构、状态转移方程。

状态转移方程最困难,作者提供思维框架:

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

开始举例说明。

一、斐波那契数列

求f(n).

1. 暴力递归

int fib(int N) {
    if (N == 1 || N == 2) {
        return 1;
    }

    return fib(N - 1) + fib(N - 2);
}

递归算法时间复杂度计算:

  • 子问题个数:二叉树节点数,O(2^n).
  • 每个子问题时间:f(n-1) + f(n-2)这样一个加法运算,O(1).

因此,这个算法时间复杂度为O(2^n).

但是,这样计算会存在重叠子问题,耗费大量时间。比如计算f(20)要计算f(19)和f(18),计算f(19)又会计算f(18)和f(17),这样f(18)被计算了两次。重叠子问题即为动态规划问题的第一个性质。

2. 带备忘录的递归解法

建立个备忘录,存所有计算过的子问题,就可避免重复计算。

inf fib(int N) {
    if (N < 1) {
        return 0;
    }

    // 备忘录全初始化为0
    vector<int> memo(N + 1, 0);

    return helper(memo, N);
}

int helper(vector<int>& memo, int n) {
    // base case
    if (n == 1 || n == 2) {
        return 1;
    }

    // 已经计算过
    if (memo[n] != 0) {
        return memo[n];
    }

    memo[n] = helper(memo, n - 1) + helper(memo, n - 2);
    return memo[n];
}

时间复杂度:

  • 子问题个数:无冗余计算,O(n).
  • 每个子问题时间:O(1).

因此,该算法时间复杂度为O(n).

该算法是自顶向下,动态规划是自底向上。

3. 动态规划解法

将备忘录独立为一张表,叫dp table。自底向上计算。

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

再优化:由于当前状态仅与之前两个状态有关,因此不需要那么长的dp table。代码如下:

int fib(int n) {
    if (n == 1 || n == 2) {
        return 1;
    }

    int pre = 1;
    int cur = 1;
    for (int i = 3; i <= n; i++) {
        int sum = pre + cur;
        pre = cur;
        cur = sum;
    }

    return sum;
}

引入概念:状态转移方程

f(n)作为一个状态,状态f(n)是由f(n-1)和f(n-2)相加转移来的,仅此而已。状态转移方程即暴力解法,动态规划是使用了备忘录或dp table优化。

二、凑零钱问题

 k 种面值的硬币,面值分别为 c1, c2 ... ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。比如说 k = 3,面值分别为 1,2,5,总金额 amount = 11。那么最少需要 3 枚硬币凑出,即 11 = 5 + 5 + 1。

  1. 明确状态:原问题和子问题中变化的量。总金额amout。
  2. 确定dp函数的定义:当前的目标金额是n,至少需要dp(n)个硬币凑出。
  3. 确定选择并择优:可以做出什么选择改变状态。
  4. 确定base case:dp[0] = 0
int coinChange(vector<int>& coins, int amount) {
    // 数组大小为 amount + 1,初始值也为 amount + 1
    vector<int> dp(amount + 1, amount + 1);
    // base case
    dp[0] = 0;
    for (int i = 0; i < dp.size(); i++) {
        // 内层 for 在求所有子问题 + 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];
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值