Dynamic Programming-动态规划(偶尔更新)

欢迎转载,但请注明出处并附带链接
算法好久没复习了,今天看见一妹子在办公室刷Leetcode,顿时我也来了兴趣,也准备刷着玩。仔细想想以前学算法时,或多或少都有些遗漏或不足,借着这次机会都补上。
先从动态规划下手吧,记得当初在这个算法上挣扎了很久。本文先理论后实践

动态规划(DP)

该算法的核心价值就两部分:描述状态推导状态的演变过程。其余基础概念我不介绍了,网上大牛写的好文多得是。在这就说说自己的感受。
这个核心价值可以解决很多的DP问题,感觉更像是一种泛式( 这里想表达:这是一个一般性的解决方案,如果具体到某一个问题上,都有改进的空间)。很多东西学到最后都是一种思想,只是在某一具体问题上表现形式不同。
到这理论结束,下面来实战,就围绕这个核心价值转

  • 例1 LeetCode 198. House Robber
    根据之前所讲,只要找到描述状态推导状态演变过程就能解决该问题,那么住这里描述状态是什么呢? 这里其实很简单,只有一种状态(本人采用数组来记录),因为根据题目的description只需记录抢到当前屋子的最大值就ok,描述状态如下:
    res[i]:抢到第 i个屋子时的目前利益最大值
    下面就要找推导状态的演变过程。演变过程用文字描述起来太累也不清晰,采用数学公式来说明,如下(图片是自己做的,找个了在线公式编辑器):
CodeCogsEqn.gif

当i=0时,只有一个房子,为了最大值,怎么可能不抢
当i=1时,就要比较下哪个屋子更值钱,然后再抢
当i>=2时,就要根据题目要求进行下判断了,分为不抢,其中 f(i-1) 就代表不抢,所以最大利益和上一项相同,而另一个就代表抢。在不抢中找出最大值

代码如下(python实现):

class Solution(object):
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums: return 0
        # create the statement
        res = []
        res = range( len(nums) )
        # i = 0
        res[0] = nums[0]
        # i = 1
        if len(nums) > 1: res[1] = max( nums[0], nums[1] )
        # i >= 2
        for i in range( 2, len(nums) ):
            res[i] = max( res[i-1], res[i-2] + nums[i] )
        
        return res[-1]

上面代码还有改进空间,正如本人之前说的"这是一个一般性的解决方案,如果具体到某一个问题上,都有改进的空间"

  • 例2 LeetCode 309. Best Time to Buy and Sell Stock with Cooldown
    来,先找描述状态,从题目中能发现3个状态(本人用3个数组来记录),如下:
    buy[i]: 截至到第 i 天的最大值,且第 i 天执行的是buy操作
    sell[i]: 截至到第 i 天的最大值,且第 i 天执行的是sell操作
    rest[i]: 截至到第 i 天的最大值,且第 i 天执行的是rest操作
    下一步就是 推导状态的演变过程。根据题目的逻辑很轻松就能用如下3个演变过程:
    设price为第i 天的价格
// buy操作的第i天只有两种可能(**买**和**不买**)
// 不买就是buy[i-1];买就必须从rest[i-1]状态切换过来,还要再减去当天的价格
buy[i] = max( rest[i-1] - price, buy[i-1] )
// sell操作也是两种可能(**卖**和**不卖**)
// 不卖就是sell[i-1], 卖就从buy[i-1]的状态切换过来,再加上当天价格
sell[i] = max( buy[i-1] + price, sell[i-1] )
// 这个演变过程被简化过了
// 其原型是max(sell[i-1], buy[i-1], rest[i-1]), 因为rest操作什么也不做,所以就从3个状态中找最大值
// 但是根据题意,不能出现{buy, rest, buy}这种不合理得排序,就删除了buy[i-1]
rest[i] = max( sell[i-1], rest[i-1] )

python代码如下:
注意初始化buy[0]和sell[0],也挺简单的,就不详述了

class Solution(object):
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        if len(prices) < 2: return 0
        
        buy = [ 0 for _ in range( len(prices) )]
        sell = [ 0 for _ in range( len(prices) )]
        rest = [ 0 for _ in range( len(prices) )]
        
        buy[0] = -prices[0] # After buy, you should have -prices[0] profit. Be positive!
        sell[0] = -sys.maxint - 1 # you can sell before buy, set sell to MIN
        
        for i in range( 1, len(prices) ):
            buy[i] = max(rest[i-1]-prices[i], buy[i-1])
            sell[i] = max(buy[i-1]+prices[i], sell[i-1])
            rest[i] = max(sell[i-1], rest[i-1])
            
        return max( sell[-1], rest[-1] )

5/25/2017 更新

  • 例3 LeetCode 486. Predict the Winner
    这次描述状态并不是那么直接,分析一下得到如下:
    dp(i,j):累加 player1从第 i 个位置到第 j 个位置选择的数值(即每次选值之和),请注意这个累加值并不是整个数组每项叠加,而是根据题意得出来的(player1选择一个,player2再选择一个,一直重复下去。然后将player1每次选值相加得到dp(i,j) )
    其中用一个二维数组表示dp(i,j),即dp[ i ][ j ]。看待dp[i][j]时,不要把它想成一个下标表示的值,而是把它看成从 i 到 j 的按题意逻辑得到的累加值,这样比较好理解下面的代码,这点很重要!!!
    在用数学公式表达下:
dp(i,j) = max( sum(i,j-1) - dp(i,j-1) + nums[j], 
                 sum(i+1,j) - dp(i+1,j) + nums[i] )

简化后,得到:

dp(i,j) = max( sum(i,j) - dp(i,j-1) , sum(i,j) - dp(i+1,j) )

下面找推导状态的演变过程,其实上面那个式子就可以说是演变过程,但对于解题来说很不理想,所以这次从题目分析。
题目想问player1能不能赢,也就是问player1最后的数值是不是大于player2的数值,即player1 - player2 > 0
下面进行一些数学推导(字迹不好看,请注意看思路:-) ):

公式推导.JPG

到此描述状态推到演变过程都结束了
下面开始上代码(依旧python,代码之后还有对一些迷茫地方的讲解):

class Solution(object):
    def PredictTheWinner(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """       
        # store the maximum score player1 picking from i to j
        dp = [ [0 for _ in range(len(nums))] for _ in range(len(nums))]
        
        # initializing
        for i in range( len(nums) ): dp[i][i] = nums[i]
        
        for i in range( 1, len(nums) ):
            for j in range( 0, len(nums)-i ):
                dp[j][j+i] = max( nums[j+i] - dp[j][j+i-1], nums[j] - dp[j+1][j+i] )
                
        return dp[0][-1] >= 0
  1. 可能有人会问“初始化那一行在做什么?”。首先,在任何dp问题中,我们都需要初始化某些值(就是那种能从题目中得到的确定值,在动态规划开始之前它们就已经存在了),在这道题目中能确定的只有dp[0][0] = nums[0], dp[1][1] = nums[1], dp[2][2] = nums[2]...等等。dp[0][0]就是开头说的dp(0,0),即从第0个开始到第0个结束所能得到的累加值,这里只有nums[0]这一个值,后面几个dp[1][1],dp[2][2]同理
  2. 还有人可能问那个双重循环在干什么?。我们要把全部的case列举出来,放张图举个例子(长度为6的数组):
举例(1).JPG

今天就说到这了


6/5/2017 更新

  • 例4 377. Combination Sum IV
    先看描述状态,这里很明显需要一个数组来记录当前共有多少种组合,即:
    dp[i]: 代表数值 i 一共有多少种组合方式
    下面看推导状态的演变过程,这个过程用个举例来说明:
    现在有一个array = [1,2,3]当做已知条件,target为4,请问当前的array可以提供多少种组合组成target?可以列出所有组合:
    4 = dp[3] + 1
    4 = dp[2] + 2
    4 = dp[1] + 3
    只有以上3种方式组成数字4,所以其组合总数为:
    dp[4] = dp[3] + dp[2] + dp[1]
    dp[4] = dp[4-1] + dp[4-2] + dp[4-3]
    然后推导出公式
    dp[target] = dp[ target - nums[0] ] + dp[target-nums[1]] + ……+ dp[target - nums[ nums.length() - 1] ]
    dp[target] = sum( dp[ target - nums[i] ] ), 0 <= i < nums.length() 且 target >= nums[i]
    上面最后一步就是推导状态的演变过程
    接着上代码(python):
class Solution(object):
    def combinationSum4(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        dp = [ 0 for _ in range(target+1)]
        dp[0] = 1
        
        for i in range(target+1):
            for j in range( len(nums) ):
                if i >= nums[j]:
                    dp[i] += dp[ i - nums[j] ]
        
        return dp[target]

这道题挺简单的,敢兴趣的可以自己做优化,欢迎交流!

6/6/2017 更新

  • 例5 64. Minimum Path Sum
    先找描述状态,基本上一眼就能看出来:
    dp[i][j]: 记录当前最小步数
    推导状态的演变过程,这个也很简单,从题目中就能知道是个2选1问题,要么move down要么move right,从它俩找出最小值, 其公式为:
    dp[i][j] = min ( dp[i-1][j], dp[i][j-1] ) + grid[i][j]
    python代码:
class Solution(object):
    def minPathSum(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        if not grid: return
    
        m = len(grid)
        n = len(grid[0])
        dp = [ [ 0 for _ in range(n)] for _ in range(m) ]
        
        dp[0][0] = grid[0][0]
        for i in range(1,n): dp[0][i] = dp[0][i-1] + grid[0][i]
        for i in range(1,m): dp[i][0] = dp[i-1][0] + grid[i][0]
        
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
                
        return dp[m-1][n-1]

不过上面这个算法实在太笨了,说下优化吧。
从逻辑上就能知道 i 要把整个二维数组走一遍,并且是按顺序一行一行的走,所以我们就用一个一维数组替代二维数组,当这个循环结束时,我们只关心最后一个位置(即右下角的位置),其他的记不记录都不无所谓,优化后的代码如下:

class Solution(object):
    def minPathSum(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        if not grid: return
        m, n = len(grid), len(grid[0])
        
        dp = [ 0 for _ in range(n) ]
        dp[0] = grid[0][0]
        
        for i in range(1, n): dp[i] = dp[i-1] + grid[0][i]
        
        for i in range(1, m):
            dp[0] += grid[i][0]
            for j in range(1, n):
                dp[j] = min( dp[j-1], dp[j] ) + grid[i][j]
        
        return dp[-1]

空间复杂度降为 O(n), 比之前那个快了不少~~

  • 例6 62. Unique Paths
    这题简直和上一题是好基友,不废话了,先找描述状态
    dp[]: 走到当前位置共有几种组合
    下面是推导状态的演变过程,如果明白上面那题,这题基本上瞬间解决,直接上公式了:
    dp[i][j] = dp[i][j-1] + dp[i-1][j],其中 i,j >= 1
    python代码:
class Solution(object):
    def uniquePaths(self, m, n):
        """
        :type m: int
        :type n: int
        :rtype: int
        """
        dp = [ [0 for _ in range(n)] for _ in range(m) ]
        
        for i in range(n): dp[0][i] = 1
        for i in range(m): dp[i][0] = 1
        
        for i in range(1,m):
            for j in range(1,n):
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
                
        return dp[m-1][n-1]

所有的代码都有优化空间,做的题远远比写在简书上的多,不过有些题解释起来太麻烦,就没写进来,以后偶尔还会更! 不过明天起准备看下其他算法了。
上面这些代码都有优化的空间,各位改着玩吧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,723评论 0 33
  • 198. House Robber【Easy DP】You are a professional robber p...
    GeniusYY阅读 1,135评论 0 0
  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,256评论 0 18
  • 分治方法 将问题划分成互不相交的子问题 递归地求解子问题 将子问题的解组合起来 动态规划(两个要素:最优子结构、子...
    superlj666阅读 492评论 0 0
  • 001踏上写作之路 写作的路,就像一条荒漠 多少人开始时便已经退缩,停留在原地张望 多少人倒在半路上,被风沙遮住了...
    朗月微光阅读 674评论 3 52