动态规划三部曲(简单篇)

动态规划问题,初出茅庐

认识动态规划

那什么是动态规划(dynamic programming, dp)呢?与分治法、贪心算法又有什么区别呢?

概念:动态规划在查找有很多重叠子问题的情况的最优解很有效,它有效的避免了重复计算子问题,它将子问题的解保存,当有需要的时候直接使用而非重复计算。
动态规划只能用于有最优子结构的问题,最优子结构指的是局部最优解能够决定全局最优解。在某些情况下可能不满足此要求,因此需要引入一定的近似。

我们熟知的分治法(将一个难以直接解决的大问题,分割成一些规模较小的相同问题,直到可以直接求解为止),将大问题分解为相互独立且与原问题形式相同,然后使用递归去求解这些自问题,通过回溯将各个子问题的解合并得到原问题的解。

而动态规划适用于有重叠子问题的情况,也就说各个子问题中包含公共的子问题,在这种情况下使用分治法将会重复计算很多次相同的问题。动态规划对每个子问题只会求解一次,然后将计算结果保存在数组或其它存储单元,从而避免了子问题的重复计算。

简而言之:分治法----各个子问题互相独立;动态规划----有重叠子问题;

递归是一种自顶向下的过程,动态规划是一种自底而上的过程。

贪心算法通常只需要考虑一个选择,通过局部最优解来产生一个全局最优解。贪心算法是自顶向下的过程,先产生一个最优解,然后再向下求子问题的最优解,贪心算法从某种意义上来说只是获取了局部最优解。而动态规划当前状态的最优解一定是基于子问题最优解的。

发现规律

想要掌握动态规划只有一个方法,那就是多做题,做很多题!!!我们先看一个简单的例子:

例1: 一只青蛙一次可以跳一级台阶,也可以一次跳两个台阶,求青蛙跳上n阶台阶一共有多少种跳法?

分析问题:
青蛙一次跳一阶
青蛙一次跳两阶
现在我们假设有一个数组dp[n],它表示当台阶数为n时一共有多少种跳法。
那么当n = 0时,dp[0] = 1,当n = 1时,dp[1] = 1(只有一种跳法)
当n = 2时,dp[2] = 2 (从dp[2 - 2]直接跳上来,或者从dp[2 - 1]跳上来)
既然青蛙只能跳一阶或两阶,因此当n = 3时,青蛙只能从3 - 1阶上或3 - 2阶上跳上来,因此dp[3] = dp[3 - 1] + dp[3 - 2] = 3

当台阶数为n时
dp[n] = dp[n - 1] + dp[n - 2]

用python实现如下(leetcode题目在此):

class Solution:
    def numWays(self, n: int) -> int:
        base = 1e9 + 7
        if n % base == 0 or n % base == 1:
            return 1
        dp = [0 for _ in range(n + 1)]
        dp[0] = 1
        dp[1] = 1
        for i in range(2, len(dp)):
            dp[i] = (dp[i - 1] + dp[i - 2]) % base
        return int(dp[n])

对于这道题,我们也可以直接考虑最后一个台阶n,假设dp[]数组表示跳上n阶台阶的方案数,那么dp[n] = dp[n - 1] + dp[n - 2]。我们把第一只小野怪打到了,我们已经升到了2级,我们现在继续打怪升级!

例2: 约翰想在他家后面的空地上建一个后花园,现在有两种砖,一种3 dm的高度,7 dm的高度。约翰想围成x dm的墙。如果约翰能做到,输出YES,否则输出NO。

这个题说白了就是给你一个target,你能不能只使用3和7凑成target。接下来我们分析,如果我们假设dp数组dp[i]表示当target为i时能否由3或7构成,能就True,不能就False,那么
dp[0] = False
dp[1] = False
dp[2] = False
当target < 3时,均为False;
dp[3] = True
dp[4] = False (dp[4 - 1])
dp[5] = False
dp[6] = False
dp[7] = True
dp[8] = dp[8 - 3] or dp[8 - 7] (取反的话就表示既不可以由3组成,也不可以由7组成)

从上面分析可以看出,target的组成只与3或者7有关,因此通过or运算符即可得到正确的结果。
python代码实现(lintcode链接在此

class Solution:
    """
    @param x: the wall's height
    @return: YES or NO
    """
    def isBuild(self, x):
        # write you code here
        if x < 3:
            return "NO"
        dp = [False for i in range(x + 1)]
        for i in range(3, len(dp)):
            if i == 3 or i == 7:
                dp[i] = True
            else:
            	# i - 7 < 0的时候是False
                dp[i] = dp[i - 3] or dp[i - 7]
        return "YES" if dp[x] else "NO"

江湖险恶,我们还是得多多打怪升级才有自保之力!GO ON~

例3: 给定一个整数数组,找到一个具有最大和的子数组(连续的),返回其最大和。注意:子数组最少包含一个数字。
样例:
输入:[−2,2,−3,4,−1,2,1,−5,3]
输出:6
解释:符合要求的子数组为[4,−1,2,1],其最大和为 6。

我们还是先定义dp数组的含义,如果你已经一步一步看到这里,并且手动实现了上述代码,你会发现,对于上述的每一道题目,我们都是先定义dp数组的含义,那么这道题的dp[i]表示什么呢?
对于上述样例,我们是怎样找到最大和子数组的呢?先看第一个元素是-2,嗯,是个负数,在看第二个元素是2,两个加起来是0,还不如本身大呢,所以这最大子数组肯定就是2了呗。
在看第三个元素-3,这和前面最大和2加起来是-1,嗯,比自身要强一些,那当前位置的最大子数组和就是-1了。
在看第四个元素4,嗯,这和前面第三元素的最大和加起来等于3,嗯,还是不如自身大,所以第四个元素的最大和子数组是4。
第五个元素呢-1,因此它的最大和子数组是4 + (-1) = 3
第六个元素2,最大和子数组值为3 + 2 = 5(就是前面那一位的值加上本身值)
第七个元素1,5 + 1 = 6
第8个元素-5, -5 + 6 = -1
第九个元素3, -1 + 3 = 2
所以最大值为6。
从上面的分析过程,我们可以总结出以下规律,我们只需要计算出当前元素的最大和子数组的值,然后从中找到最大值即可。那么dp数组的含义也很明显了,dp[i]就表示在位置i处的最大和子数组的值,其递推方程表示如下:

dp[i] = max(dp[i - 1] + nums[i], nums[i])

用python实现如下(lintcode链接在此

class Solution:
    """
    @param nums: A list of integers
    @return: A integer indicate the sum of max subarray
    """
    def maxSubArray(self, nums):
        # write your code here
        lens = len(nums)
        dp = [0 for _ in range(lens)]
        dp[0] = nums[0]
        max_ = dp[0]
        
        for i in range(1, lens):
            dp[i] = max(dp[i - 1] + nums[i], nums[i])
            if max_ < dp[i]:
                max_ = dp[i]
		return max_

通过上述分析,我们可以发现简单级动态规划的问题还是很简单的,我们只需要考虑清楚dp数组的含义即可,这也是初学者最为困惑的地方。中等级、困难级的动态规划问题难就难在如何定义dp数组的含义。
… … 山谷中,鸟语花香,有一只小猿猴似乎在指引着什么 … …跟着它… …
咦?白猿肚子里有东西?似乎是一本秘籍… …费力从中取出一本古籍,上面有七个大字:动态规划之优化。神功秘籍开篇提到,动态规划问题通常可以优化空间复杂度 下面我们开始进行修炼。对于例3来说,每一次求当前位置的最大和子数组值时,只与前一个位置的最大和子数组的值有关,因此我们只需要两个变量保存数值,就能够实现上述的功能,完全不需要过多的内存空间。优化后的代码如下:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值