珍宝鸭的力扣练习(8):(NP)动态规划解决问题合集

动态规划算法
在整个数组或在固定大小的滑动窗口中找到总和或最大值或最小值的问题可以通过动态规划(DP)在线性时间内解决。
有两种标准 DP 方法适用于数组:
常数空间,沿数组移动并在原数组修改。
线性空间,首先沿 left->right 方向移动,然后再沿 right->left 方向移动。 合并结果。

====================================
动态规划的解题四步骤

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

====================================

题目:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
步骤一:定义子问题
什么是子问题?子问题是和原问题相似,但规模较小的问题。例如这道小偷问题,原问题是“从全部房子中能偷到的最大金额”,将问题的规模缩小,子问题就是

“从 k 个房子中能偷到的最大金额”,用f(k) 表示。

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

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

步骤二:写出子问题的递推关系
这一步是求解动态规划问题最关键的一步。然而,这一步也是最无法在代码中体现出来的一步。在做题的时候,最好把这一步的思路用注释的形式写下来。做动态规划题目不要求快,而要确保无误。
我们来分析一下这道小偷问题的递推关系:

假设一共有 n 个房子,每个房子的金额分别是
H_0, H_1, …, H_{n-1} ,子问题 f(k)表示从前 k个房子(即 H_0, H_1, …, H_{k-1} )中能偷到的最大金额。那么,偷 k 个房子有两种偷法:k 个房子中最后一个房子是 H_{k-1} 。如果不偷这个房子,那么问题就变成在前 k-1 个房子中偷到最大的金额,也就是子问题 f(k-1)。如果偷这个房子,那么前一个房子 H_{k-2}显然不能偷,其他房子不受影响。那么问题就变成在前 k-2 个房子中偷到的最大的金额。两种情况中,选择金额较大的一种结果。

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

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

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


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

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

DP 数组也可以叫”子问题数组”,因为 DP 数组中的每一个元素都对应一个子问题。如下图所示,dp[k] 对应子问题 f(k),即偷前 k 间房子的最大金额。
那么,只要搞清楚了子问题的计算顺序,就可以确定 DP 数组的计算顺序。对于小偷问题,我们分析子问题的依赖关系,发现每个 f(k)f(k) 依赖 f(k-1)和 f(k-2)。也就是说,dp[k] 依赖 dp[k-1] 和 dp[k-2],如下图所示
那么,既然 DP 数组中的依赖关系都是向右指的,DP 数组的计算顺序就是从左向右。这样我们可以保证,计算一个子问题的时候,它所依赖的那些子问题已经计算出来了。
确定了 DP 数组的计算顺序之后,我们就可以写出题解代码了:

def rob(self, nums):
    if len(nums) == 0:
        return 0

    # 子问题:
    # f(k) = 偷 [0..k) 房间中的最大金额

    # f(0) = 0
    # f(1) = nums[0]
    # f(k) = max{ rob(k-1), nums[k-1] + rob(k-2) }

    N = len(nums)
    dp = [0] * (N+1)
    dp[0] = 0
    dp[1] = nums[0]
    for k in range(2, N+1):
        dp[k] = max(dp[k-1], nums[k-1] + dp[k-2])
    return dp[N]


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

题目2:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。

  1. 1 阶 + 1 阶
  2. 2 阶
    示例 2:
    输入: 3
    输出: 3
    解释: 有三种方法可以爬到楼顶。
  3. 1 阶 + 1 阶 + 1 阶
  4. 1 阶 + 2 阶
  5. 2 阶 + 1 阶
class Solution(object):
    def climbStairs(self, n):
       # 子问题:
       # f(k) = 爬k阶楼梯有多少种方法

       # f(0) = 0
       # f(1) = 1
       # f(k) = c(k-1) + c(k-2)*2
       if n==1:
           return 1
       dp = [0] *(n+1)
       dp[0] = 0
       dp[1] = 1
       dp[2] = 2
       for k in range(3, n+1):
           dp[k] = dp[k-1]+dp[k-2]
       return dp[n]

s=Solution()
发布了11 篇原创文章 · 获赞 0 · 访问量 136

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览