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]

所有的代碼都有優化空間,做的題遠遠比寫在簡書上的多,不過有些題解釋起來太麻煩,就沒寫進來,以后偶爾還會更! 不過明天起準備看下其他算法了。
上面這些代碼都有優化的空間,各位改著玩吧

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,769評論 0 33
  • 198. House Robber【Easy DP】You are a professional robber p...
    GeniusYY閱讀 1,162評論 0 0
  • 動態規劃(Dynamic Programming) 本文包括: 動態規劃定義 狀態轉移方程 動態規劃算法步驟 最長...
    廖少少閱讀 3,327評論 0 18
  • 分治方法 將問題劃分成互不相交的子問題 遞歸地求解子問題 將子問題的解組合起來 動態規劃(兩個要素:最優子結構、子...
    superlj666閱讀 515評論 0 0
  • 001踏上寫作之路 寫作的路,就像一條荒漠 多少人開始時便已經退縮,停留在原地張望 多少人倒在半路上,被風沙遮住了...
    朗月微光閱讀 684評論 3 52