leetcode例题解释动态规划思想

leetcode例题解释动态规划思想

原文链接

本文会以 House Robber 题目为例子,讲解动态规划题目的四个基本步骤。

动态规划的的四个解题步骤是:

  • 定义子问题
  • 写出子问题的递推关系
  • 确定 DP 数组的计算顺序
  • 空间优化(可选)

下面我们一步一步地进行讲解。

步骤一:定义子问题

稍微接触过一点动态规划的朋友都知道动态规划有一个“子问题”的定义。什么是子问题?子问题是和原问题相似,但规模较小的问题。例如这道小偷问题,原问题是“从全部房子中能偷到的最大金额”,将问题的规模缩小,子问题就是“从 k 个房子中能偷到的最大金额”,用 f(k)表示。

小偷问题的子问题定义

可以看到,子问题是参数化的,我们定义的子问题中有参数 k。假设一共有 n个房子的话,就一共有 n 个子问题。动态规划实际上就是通过求这一堆子问题的解,来求出原问题的解。这要求子问题需要具备两个性质:

  • 原问题要能由子问题表示。例如这道小偷问题中,k=nk=n 时实际上就是原问题。否则,解了半天子问题还是解不出原问题,那子问题岂不是白解了。
  • 一个子问题的解要能通过其他子问题的解求出。例如这道小偷问题中,f(k) 可以由 f(k−1) 和 f(k-2)求出,具体原理后面会解释。这个性质就是教科书中所说的“最优子结构”。如果定义不出这样的子问题,那么这道题实际上没法用动态规划解。

小偷问题由于比较简单,定义子问题实际上是很直观的。一些比较难的动态规划题目可能需要一些定义子问题的技巧。

步骤二:写出子问题的递推关系

这一步是求解动态规划问题最关键的一步。然而,这一步也是最无法在代码中体现出来的一步。在做题的时候,最好把这一步的思路用注释的形式写下来。做动态规划题目不要求快,而要确保无误。否则,写代码五分钟,找 bug 半小时,岂不美哉?

我们来分析一下这道小偷问题的递推关系:

假设一共有 n 个房子,每个房子的金额分别是H0,H1,…,Hn-1,子问题 f(k) 表示从前 k 个房子(即H0,H1,…,Hk-1)中能偷到的最大金额。那么,偷 k 个房子有两种偷法:

子问题的递推关系

k 个房子中最后一个房子金额是Hk-1。如果不偷这个房子,那么问题就变成在前 k−1 个房子中偷到最大的金额,也就是子问题 f(k−1)。如果偷这个房子,那么前一个房子 显然不能偷,其他房子不受影响。那么问题就变成在前 k−2 个房子中偷到的最大的金额。两种情况中,选择金额较大的一种结果。

f(k)=max{f(k−1),Hk-1+f(k−2)}

在写递推关系的时候,要注意写上k=0 和k=1 的基本情况:

  • 当k=0 时,没有房子,所以 f(0)=0
  • 当k=1 时,只有一个房子,偷这个房子即可,所以 f(1) = H0

这样才能构成完整的递推关系,后面写代码也不容易在边界条件上出错。

步骤三:确定 DP 数组的计算顺序

在确定了子问题的递推关系之后,下一步就是依次计算出这些子问题了。在很多教程中都会写,动态规划有两种计算顺序,一种是自顶向下的、使用备忘录的递归方法,一种是自底向上的、使用 dp 数组的循环方法。不过在普通的动态规划题目中,99% 的情况我们都不需要用到备忘录方法,所以我们最好坚持用自底向上的 dp 数组。

DP 数组也可以叫”子问题数组”,因为 DP 数组中的每一个元素都对应一个子问题。如下图所示,dp[k] 对应子问题 f(k),即偷前 k 间房子的最大金额。

DP 数组与子问题的对应关系

那么,只要搞清楚了子问题的计算顺序,就可以确定 DP 数组的计算顺序。对于小偷问题,我们分析子问题的依赖关系,发现每个 f(k) 依赖 f(k−1)f(k−2)。也就是说,dp[k] 依赖 dp[k-1] 和 dp[k-2],如下图所示。

DP 数组的依赖顺序

那么,既然 DP 数组中的依赖关系都是向右指的,DP 数组的计算顺序就是从左向右。这样我们可以保证,计算一个子问题的时候,它所依赖的那些子问题已经计算出来了。

确定了 DP 数组的计算顺序之后,我们就可以写出题解代码了:

int rob(int* nums, int numsSize){
    int dp[numsSize+1];//dp[k]含义为k个房子能偷窃到的最高金额
    dp[0]=0;
    dp[1]=nums[0];
    for(int i=2;i<numsSize+1;i++){//满足最优子结构,设有k个房子,当选择偷第K个房子时可以偷窃的金额为dp[k-2]+nums[k](因为不能偷取相邻的房子),不选择偷窃第K个房子可以偷窃的金额为dp[k-1],两种情况选择最大的那个即为dp[k]的值
        dp[i]=dp[i-1]>=dp[i-2]+nums[i-1]?dp[i-1]:dp[i-2]+nums[i-1];
    }
    return dp[numsSize];
}
步骤四:空间优化

空间优化是动态规划问题的进阶内容了。对于初学者来说,可以不掌握这部分内容。

空间优化的基本原理是,很多时候我们并不需要始终持有全部的 DP 数组。对于小偷问题,我们发现,最后一步计算 f(n) 的时候,实际上只用到了 f(n−1)f(n-2) 的结果。n-3 之前的子问题,实际上早就已经用不到了。那么,我们可以只用两个变量保存两个子问题的结果,就可以依次计算出所有的子问题。下面的动图比较了空间优化前和优化后的对比关系:

空间优化前后对比(动图)

这样一来,空间复杂度也从O(n) 降到了 O(1)。优化后的代码如下所示:

int rob(vector<int>& nums) {
    int prev = 0;
    int curr = 0;

    // 每次循环,计算“偷到当前房子为止的最大金额”
    for (int i : nums) {
        // 循环开始时,curr 表示 dp[k-1],prev 表示 dp[k-2]
        // dp[k] = max{ dp[k-1], dp[k-2] + i }
        int temp = max(curr, prev + i);
        prev = curr;
        curr = temp;
        // 循环结束时,curr 表示 dp[k],prev 表示 dp[k-1]
    }

    return curr;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值