Leetcode動態規劃(python)

九章算法動態規劃總結

動態規劃分類:

  • 座標型動態規劃
  • 序列型動態規劃
  • 劃分型動態規劃
  • 最長上升子序列
  • 揹包型動態規劃
  • 區間型動態規劃
  • 綜合型動態規劃

思路:

  1. 定義狀態(根據最後一步和子問題)
  2. 寫出狀態(根據最後一步和子問題)
  3. 初始化和界內處理
  4. 計算順序、計算結果(判斷是否可以用滑動數組節省空間)

一、座標型動態規劃(最簡單的)

方法:

典型特點是以座標所在的意義作爲狀態。如一維和二維數組:dp=[m][n] or dp=[m]

  • 給定輸入爲序列或者矩陣
  • 狀態序列下標爲下標i或者(i,j);以第i個元素結尾的性質或者以i,j結尾的路徑的性質
  • 初始化設置爲f[0]或f[0][0...n-1]
  • 二維空間優化:如果f[i][j]的值只依賴於當前行和前一行,則可以用滾動數組節省時間。

 

1、不同路徑(無障礙)【leetcode62】

分析過程:

  1. f[i][j]表示到達終點的路徑的個數
  2. f[i][j] = f[i-1][j] + f[i][j](注意越界)
  3. f[0][0] = 1 ;越界處理即對f[i-1][j] + f[i][j]中ij的約束
  4. 計算順序:從左到右,從上到下;計算結果:f[m-1][n-1]
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        if m == 0 or n == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m)]
        #初始化放在for循環裏了
        for i in range(0, m):#這個循環怎麼走,應該根據狀態轉移方程來
            for j in range(0, n):
                if i == 0 or j == 0:
                    dp[i][j] = 1
                else:
                    dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[m-1][n-1]

2、不同路徑(有障礙)【leetcode63】

分析過程和第1題類似,但是需要注意的是有路障的地方dp[i][j]= 0,分成五種情況考慮

坑:如果start就有路障,那麼return 0(因此不能一開始就初始化dp[0][0] = 1)

  1. 如果start就有路障,那麼return 0(因此不能一開始就初始化dp[0][0] = 1)
  2. 初始化dp[0][0] = 1
  3. 如果有路障,那麼dp[i][j]=0 則continue
  4. f[i][j] = f[i-1][j] + f[i][j](注意越界)區分i==0和j==0
#動態規劃問題
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m = len(obstacleGrid)
        n = len(obstacleGrid[0])
        nums = obstacleGrid
        if m == 0 or n == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m)]

        #初始化放在for循環中
        for i in range(m):
            for j in range(n):
                if nums[i][j] == 1: #考慮障礙的位置,來分情況討論(初始化)
                    continue
                if i == 0 and j == 0: #初始化
                    dp[i][j] = 1
                if i > 0:
                    dp[i][j] += dp[i-1][j]
                if j > 0:
                    dp[i][j] += dp[i][j-1]                
        return dp[m-1][n-1]

3、跳躍遊戲【leetcode55】

思路:

  1. 最後一步可能能跳到i,也有可能調不到i,因此定義f(i)爲能否跳到位置i
  2. 狀態轉移方程:f[i] = f[j] (f[j] is true and nums[j] >= i-j)!!!!!!!!這個我經常很糊塗,不知道從哪直接能跳到最後一步,因此要枚舉!
  3. 初始化f[0]=0,無越界
  4. 計算順序從左到右,返回f[size-1]
class Solution:
    def canJump(self, nums) -> bool:
        size = len(nums)
        if size == 0:
            return True

        dp = [False for _ in range(size)]

        dp[0] = True

        for i in range(1, size):
            for j in range(i):
                if nums[j] >= i-j and dp[j]:
                    dp[i] = True
                    break
        return dp[size-1]
s = Solution()
print(s.canJump([2,2,1,0,4]))

4、最長連續上升子序列【lintcode397】

題目:

給定一個整數數組(下標從 0 到 n-1, n 表示整個數組的規模),請找出該數組中的最長上升連續子序列。(最長上升連續子序列可以定義爲從右到左或從左到右的序列。)

樣例

樣例 1:

輸入:[5, 4, 2, 1, 3]
輸出:4
解釋:
給定 [5, 4, 2, 1, 3],其最長上升連續子序列(LICS)爲 [5, 4, 2, 1],返回 4。

樣例 2:

輸入:[5, 1, 2, 3, 4]
輸出:4
解釋:
給定 [5, 1, 2, 3, 4],其最長上升連續子序列(LICS)爲 [1, 2, 3, 4],返回 4。

挑戰

使用 O(n) 時間和 O(1) 額外空間來解決

  1. f[i]表示以i爲結尾的最大連續子序列的長度
  2. f[i] = f[i-1]+1 or 1
  3. f[0]=nums[0]
  4. 計算順序從左到右,結果是max(f)
class Solution:
    """
    @param A: An array of Integer
    @return: an integer
    """
    def longestIncreasingContinuousSubsequence(self, A):
        size = len(A)
        nums = A
        if size == 0:
            return 0
        
        dp = [0 for _ in range(size)]
        
        dp[0] = 1
        for i in range(1, size):
            if nums[i] > nums[i-1]:
                dp[i] = dp[i-1] + 1
            else:
                dp[i] = 1
        
        max_value = max(dp)
        for i in range(1, size):
            if nums[i] < nums[i-1]:
                dp[i] = dp[i-1] + 1
            else:
                dp[i] = 1
        return max(max_value, max(dp))

優化空間:

滑動數組法節省空間:

old, now = 0, 0
for i in range(m):
    old = now
    now = now - 1
    #接下來就是old表示i-1,now表示i即可

代碼 

class Solution:
    """
    @param A: An array of Integer
    @return: an integer
    """

    def longestIncreasingContinuousSubsequence(self, A):
        size = len(A)
        nums = A
        if size == 0:
            return 0

        dp = [0 for _ in range(2)]

        dp[0] = 1 #注意這個初始爲1纔對
        old, now = 0, 0
        max_value = dp[0]
        for i in range(1, size):
            old = now
            now = 1 - now
            if nums[i] > nums[i - 1]:
                dp[now] = dp[old] + 1
            else:
                dp[now] = 1
            max_value = max(max_value, dp[now])
        dp[0] = 1
        old, now = 0, 0
        for i in range(1, size):
            old = now
            now = 1 - now
            if nums[i] < nums[i - 1]:
                dp[now] = dp[old] + 1
            else:
                dp[now] = 1
            max_value = max(max_value, dp[now])

        return max_value

5、最長上升子序列【Leetcode300】

最後一步:以i爲結尾的子序列的最大長度,那序列i前面的值是j,j不是i-1,因此要枚舉j找出以i結尾最大的子序列的長度。

#狀態表示的意義是:dp[i]表示以nums[i]爲結尾的升序列長度,最後返回最大值
# class Solution:
#     def lengthOfLIS(self, nums: List[int]) -> int:
#         size = len(nums)
#         if size == 0:
#             return 0
        
#         dp = [0 for _ in range(size)]
#         #初始化
#         dp[0] = 1

#         for i in range(1, size):
#             dp[i] = 1
#             for j in range(i):
#                 if nums[i] > nums[j]:
#                     dp[i] = max(dp[i], dp[j] + 1)
#         return max(dp)

6、俄羅斯套娃【leetcode354】

和第5題一樣,典型的最長上升子序列,唯一的不同是需要排序。

坑:

注意我要求j裏面的最大值,那麼dp[i]應該放在循環外面,才能真正求到最大值。

#這個題用排序算法無法通過
class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort()
        # envelopes.sort(key=lambda x: x[0])
        size = len(envelopes)
        if size == 0:
            return 0
         
        dp = [0 for _ in range(size)]
        
        dp[0] = 1

        for i in range(1, size):
            dp[i] = 1
            for j in range(i):
                if envelopes[i][0] > envelopes[j][0] and envelopes[i][1] > envelopes[j][1]:
                    dp[i] = max(dp[i], dp[j] + 1)
        
        return max(dp)

5.【leetcode64】最小路徑和 

空間優化

注意必須是從第一行開始的,才能套用那個滑動數組的模板。

class Solution:
    """
    @param grid: a list of lists of integers
    @return: An integer, minimizes the sum of all numbers along its path
    """
    def minPathSum(self, grid):
        # write your code here
        m = len(grid)
        n = len(grid[0])
        if m == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(2)]
        old, now = 0, 0
        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):
            old = now
            now = 1 - now
            for j in range(n):
                dp[now][j] = float('inf')
                if i > 0:
                    dp[now][j] = min(dp[now][j], dp[old][j]+grid[i][j])
                if j > 0:
                    dp[now][j] = min(dp[now][j], dp[now][j-1]+grid[i][j])
        print(dp)
        return dp[now][n-1]

 

代碼:

class Solution:
    """
    @param grid: a list of lists of integers
    @return: An integer, minimizes the sum of all numbers along its path
    """
    def minPathSum(self, grid):
        # write your code here
        m = len(grid)
        n = len(grid[0])
        if m == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m)]
        # dp[0][0] = grid[0][0]
        for i in range(m):
            for j in range(n):
                if i == 0 and j == 0:
                    dp[i][j] = grid[i][j]
                    continue
                dp[i][j] = float('inf')
                if i > 0:
                    dp[i][j] = min(dp[i][j], dp[i-1][j]+grid[i][j])
                if j > 0:
                    dp[i][j] = min(dp[i][j], dp[i][j-1]+grid[i][j])
        return dp[m-1][n-1]

 6、炸彈襲擊【leetcode553】

描述

給定一個二維矩陣, 每一個格子可能是一堵牆 W,或者 一個敵人 E 或者空 0 (數字 '0'), 返回你可以用一個炸彈殺死的最大敵人數. 炸彈會殺死所有在同一行和同一列沒有牆阻隔的敵人。 由於牆比較堅固,所以牆不會被摧毀.

你只能在空的地方放置炸彈.

您在真實的面試中是否遇到過這個題?  是

題目糾錯

樣例

樣例1

輸入:
grid =[
     "0E00",
     "E0WE",
     "0E00"
]
輸出: 3
解釋:
把炸彈放在 (1,1) 能殺3個敵人

思路:

  1. 分成上下左右四個方向就容易了,這個題的難點就是沒想到分成上下左右四個方向
  2. 注意在分方向的時候計算順序也會改變 
class Solution:
    """
    @param grid: Given a 2D grid, each cell is either 'W', 'E' or '0'
    @return: an integer, the maximum enemies you can kill using one bomb
    """
    def maxKilledEnemies(self, grid):
        # write your code here
        m = len(grid)
        if m == 0:
            return 0
        n = len(grid[0])
        if n == 0:
            return 0
        
        
        up = [[0 for _ in range(n)] for _ in range(m)]
        for i in range(m):
            for j in range(n):
                if i == 0 and grid[i][j] == 'E':
                    up[i][j] = 1
                    continue
                if grid[i][j] == 'W':
                    up[i][j] = 0
                    continue
                if i > 0:
                    up[i][j] = up[i-1][j]
                    if grid[i][j] == 'E':
                        up[i][j] += 1
        down = [[0 for _ in range(n)] for _ in range(m)]
        for i in range(m-1, -1, -1):
            for j in range(n):
                if i == m-1 and grid[i][j] == 'E':
                    down[i][j] = 1
                    continue
                if grid[i][j] == 'W':
                    down[i][j] = 0
                    continue
                if i < m-1:
                    down[i][j] = down[i+1][j]
                    if grid[i][j] == 'E':
                        down[i][j] += 1
        left = [[0 for _ in range(n)] for _ in range(m)]
        for i in range(m):
            for j in range(n):
                if j == 0 and grid[i][j] == 'E':
                    left[i][j] = 1
                    continue
                if grid[i][j] == 'W':
                    left[i][j] = 0
                    continue
                if j > 0:
                    left[i][j] = left[i][j-1]
                    if grid[i][j] == 'E':
                        left[i][j] += 1
        right = [[0 for _ in range(n)] for _ in range(m)]
        for i in range(m):
            for j in range(n-1, -1, -1):
                if j == n-1 and grid[i][j] == 'E':
                    right[i][j] = 1
                    continue
                if grid[i][j] == 'W':
                    right[i][j] = 0
                    continue
                if j < n-1:
                    right[i][j] = right[i][j+1]
                    if grid[i][j] == 'E':
                        right[i][j] += 1
        max_value = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '0':
                    max_value = max(max_value, up[i][j]+down[i][j]+right[i][j]+left[i][j])
        return max_value

 

位操作動態規劃

和位操作有關的動態規劃題一般用值作爲狀態。

此題難點在於狀態轉換方程 f[i] = f[i>>1] + i mod 2

#動態規劃
# class Solution:
#     def countBits(self, num: int) -> List[int]:

#         dp = [0 for _ in range(num + 1)] #最後要返回這個數組
#         dp[0] = 0

#         for i in range(1, num + 1):
#             dp[i] = dp[i>>1] + i % 2
        
#         return dp

class Solution:
    def countBits(self, num: int) -> List[int]:

        dp = [0 for _ in range(num + 1)] #最後要返回這個數組
        dp[0] = 0

        for i in range(1, num + 1):
            dp[i] = dp[i>>1] + (i & 1)
        
        return dp

二、序列型動態規劃

當思考動態規劃最後一步時,這一步的選擇依賴於前一步的某種狀態

初始化f[0]表示前0天的性質

計算時,f[i]表示前i個元素(0...i-1)的某種性質。

套路:(序列+狀態)

  • 給定一個序列
  • 序列型動態規劃f[i]的下標表示前i個元素(A0, ... , Ai-1)具有某種性質,而座標型動態規劃f[i]表示以Ai結尾的某種性質
  • 序列型初始化時f[0]具有空序列的性質, 而座標型動態規劃f[0]表示以A0爲結尾的某種性質

1、粉刷房子1(三種顏色)【lintcode515】

思路:

  1. 最後一步:由於題目說房子只能刷紅藍綠三種顏色,那麼最後一個房子不能和上一個房子顏色一樣,上一個房子可能是紅色、藍色或綠色(分情況討論,找出最小的)。如果上一個房子是紅色,那麼最後一個房子只能刷綠色或者藍色,求出來最小的那個就是上一個房子刷紅色的花費值。由於fi和顏色有關,我們將顏色加入f[i][j],表示前i個房子(第i-1刷j色)的花費
  2. f[i][j] = min(k!=j)(f[i-1][k] + costs[i-1][j]) 開數組(dp[i][j] = [m+1][n])
  3. 初始化第一行爲0
  4. 計算順序從左到右從上到下,結果就是min(dp[m])
  5. 關鍵在於最後一個房子和上一個房子的顏色有關,因此把顏色也放在dp裏面。
class Solution:
    """
    @param costs: n x 3 cost matrix
    @return: An integer, the minimum cost to paint all houses
    """
    def minCost(self, costs):
        # write your code here
        m = len(costs)
        if m == 0:
            return 0
        n = len(costs[0])
        if n == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m+1)]
        
        for i in range(m+1):
            for j in range(n):
                if i == 0 :
                    dp[i][j] = 0
                    continue
                dp[i][j] = float('inf')
                for k in range(n):
                    if k != j:
                        dp[i][j] = min(dp[i][j], dp[i-1][k]+costs[i-1][j])
        return min(dp[m])

2、粉刷房子(有k種顏色)【lintcode516】

難點:將複雜度從O(mn2)減到O(mn)

方法:先計算前一行的最小值和次小值並記錄最小值和次小值的索引,如果當前行的顏色和最小值的顏色相同,那麼當前行這個顏色的代價最小即爲次小值+cost,除此之外當前行的這個顏色的代價應該是最小值+cost

優化時間代碼如下:【模板】

        for i in range(1, m+1):
            min_v = float('inf')
            sec_v = float('inf')
            min_i = 0
            sec_i = 0
            for j in range(n):
                if dp[i-1][j] < min_v:
                    sec_v = min_v
                    sec_i = min_i
                    min_v = dp[i-1][j]
                    min_i = j
                    continue
                if dp[i-1][j] < sec_v:
                    sec_v = dp[i-1][j]
                    sec_i = j

總體代碼如下:

class Solution:
    """
    @param costs: n x k cost matrix
    @return: an integer, the minimum cost to paint all houses
    """
    def minCostII(self, costs):
        # write your code here
        m = len(costs)
        if m == 0:
            return 0
        n = len(costs[0])
        if n == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m+1)]
        for i in range(n):
            dp[0][i] = 0
        
        for i in range(1, m+1):
            min_v = float('inf')
            sec_v = float('inf')
            min_i = 0
            sec_i = 0
            for j in range(n):
                if dp[i-1][j] < min_v:
                    sec_v = min_v
                    sec_i = min_i
                    min_v = dp[i-1][j]
                    min_i = j
                    continue
                if dp[i-1][j] < sec_v:
                    sec_v = dp[i-1][j]
                    sec_i = j

            for j in range(n):
                if j != min_i:
                    dp[i][j] = min_v + costs[i-1][j]
                if j == min_i:
                    dp[i][j] = sec_v + costs[i-1][j]
        
        return min(dp[m])

3.打家劫舍1【leetcode198】

思路:

  1. 最後一步是偷房子i-1還是不偷呢,如果偷房子i,那麼和它相鄰的前一個房子一定不能偷,因此用f[i]表示前i個房子最大偷錢值。
  2. f[i] = max(f[i-1], f[i-2]+A[i-1]
  3. 初始化f[0],f[1]
  4. 計算順序從左到右,結果是f[size]

空間優化:

        old, now = 0, 0
        
        
        for i in range(2, len(nums1)+1):
            old = now
            now = 1 - now
            dp1[old] = max(dp1[now], dp1[old]+nums1[i-1])

上述代碼最後一句,注意old是當前還是now是當前即可

代碼:空間優化

#動態規劃時間複雜度O(n),空間複雜度O(n)
class Solution:
    """
    @param A: An array of non-negative integers
    @return: The maximum amount of money you can rob tonight
    """
    def houseRobber(self, A):
        # write your code here
        size = len(A)
        if size == 0:
            return 0
        
        dp = [0 for _ in range(2)]
        
        dp[0] = 0
        dp[1] = A[0]
        old, now = 0, 1
        
        for i in range(2, size+1):
            old = now
            now = 1 - now
            dp[now] = max(dp[now]+A[i-1], dp[old])
            
        
        return dp[now]
#時間複雜度O(n),空間複雜度O(1)

 沒有空間優化:

#動態規劃時間複雜度O(n),空間複雜度O(n)
# class Solution:
#     def rob(self, nums: List[int]) -> int:
#         size = len(nums)
#         if size == 0:
#             return 0
        
#         dp = [0 for _ in range(size+1)]

#         dp[0] = 0

#         for i in range(1, size+1):
#             if i == 1:
#                 dp[i] = nums[i-1]
#             else:
#                 dp[i] = max(dp[i-1], dp[i-2]+nums[i-1])
#         # print(dp)

#         return dp[-1]

4.打家劫舍2【leetcode213】

思路:這個房子圍成一圈了,因此我們把這一圈拆成兩個序列,一個含首不含尾,一個含尾不含首。拆的時候注意如果[1]一個元素,會發生越界。其餘思路同打家劫舍1

class Solution:
    """
    @param nums: An array of non-negative integers.
    @return: The maximum amount of money you can rob tonight
    """
    def houseRobber2(self, nums):
        # write your code here
        size = len(nums)
        nums1 = nums[0:size-1]
        if size == 0:
            return 0
        if size == 1:
            return nums[0]
        
        dp1 = [0 for _ in range(2)]
        
        dp1[0] = 0
        dp1[1] = nums1[0]
        old, now = 0, 0
        
        
        for i in range(2, len(nums1)+1):
            old = now
            now = 1 - now
            dp1[old] = max(dp1[now], dp1[old]+nums1[i-1])
        max_value = dp1[old]
        
        
        nums2 = nums[1:size]
        
        dp2 = [0 for _ in range(2)]
        
        dp2[0] = 0
        dp2[1] = nums2[0]
        old, now = 0, 0
        
        for i in range(2, len(nums2)+1):
            old = now
            now = 1 - now
            dp2[old] = max(dp2[now], dp2[old]+nums2[i-1])
        max_value = max(max_value, dp2[old])  
        
        return max_value
            
            

無空間優化:

# class Solution:
#     def rob(self, nums) -> int:
#         size = len(nums)
#         if size == 0:
#             return 0
#         if size == 1:
#             return nums[0]

#         dp = [0 for i in range(size)] 

#         dp[0] = 0
#         nums1 = nums[:size-1]
#         for i in range(1, size):
#             if i == 1:
#                 dp[i] = nums1[0]
#             else:
#                 dp[i] = max(dp[i - 1], dp[i - 2] + nums1[i - 1])
#         max_value = dp[-1]
#         nums2 = nums[1:]
#         for i in range(1, size):
#             if i == 1:
#                 dp[i] = nums2[0]
#             else:
#                 dp[i] = max(dp[i - 1], dp[i - 2] + nums2[i - 1])

#         max_value = max(max_value, dp[-1])
#         return max_value

 

5.股票問題1【leetcode121】

題意:只能買賣一次股票,求獲利最大是多少。

思路:

  1. 最後一步:如果最後一步之前已經賣了,那麼f[i]=f[i-1],如果最後一步賣f[i]=price[i-1]-min_val f[i]表示前i天獲利
  2. 轉換方程就是上述最大
  3. dp[0]= 0
  4. 計算順序從左到右,計算結果最後一天。
class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        dp = [0 for _ in range(size+1)]
        
        dp[0] = 0
        min_value = float('inf')
        for i in range(1, size+1):
            dp[i] = dp[i-1]
            min_value = min(min_value, prices[i-1])
            if min_value < prices[i-1]:
                dp[i] = max(dp[i], prices[i-1]-min_value)
            
        
        return dp[-1]

空間優化:滾動數組

class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        dp = [0 for _ in range(size+1)]
        
        dp[0] = 0
        min_value = float('inf')
        old, now = 0, 0
        for i in range(1, size+1):
            old = now
            now = 1 - now
            dp[now] = dp[old]
            min_value = min(min_value, prices[i-1])
            if min_value < prices[i-1]:
                dp[now] = max(dp[now], prices[i-1]-min_value)
            
        
        return dp[now]

股票問題2【leetcode122】

題意:可以無數次交易

典型的貪心問題:只要當前比之前大就賣出去

class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        res = 0
        size = len(prices)
        if size == 0:
            return res
        
        for i in range(1, size):
            if prices[i] > prices[i-1]:
                res += prices[i] - prices[i-1]
        
        return res

用動態規劃來做:

# class Solution:
#     def maxProfit(self, prices: List[int]) -> int:
#         size = len(prices)
#         if size == 0:
#             return 0
        
#         dp = [0 for _ in range(size+1)]
#         dp[0] = 0
#         dp[1] = 0
#         min_value = float('inf')
#         for i in range(2, size+1):
#             min_value = min(min_value, prices[i-2])
#             if prices[i-1] >= min_value:
#                 dp[i] = prices[i-1] - min_value
#                 min_value = prices[i-1]
#         return sum(dp)

 

股票問題3【leetcode123】

題意:只能買賣兩次

如果序列+狀態才能解決問題,如果有五個狀態最好0, 1, 2, 3, 4這樣表示,防止越界。

思路:思考最後一步是第一次賣之後還是第二次賣之後還是沒買過之後,不知道,因此把狀態寫入動態規劃

  1. f[i][j]表示前i天處在j狀態的獲利情況。j分成五個狀態,分別爲0:第一次買之前;1:第一次買之後(持股);2:第一次賣之後第一次買之前;3:第二次買之後(持股);4:第二次賣之後。最後一步只能是處於0, 2,4狀態,因此答案只能是在這三個狀態下求最大值即max(f[size][0],f[size][2],f[size][4])
  2. 狀態轉移方程對於狀態0, 2, 4來說f[i][j] = max(f[i-1][j], f[i-1][j-1]+prices[i-1]-prices[i-2](買的當天不賺錢,持股或者賣那天才會賺錢)狀態轉移:可以保持當前狀態,可以在前一狀態轉到這個狀態,因此有兩種情況;對於1,3來說,本身是持股,但是在動態規劃中也是需要狀態轉移的,要不是一直是持股狀態,要不是前一天是沒股狀態,因此f[i][j]=max(f[i-1][j]+prices[i-1][i-2], f[i-1][j-1])
  3. 初始化dp[0][0,4]=0,注意j的出界處理。
  4. 計算順序從左到右,從上到下,結果爲max(f[size][0],f[size][2],f[size][4])
class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        
        dp = [[0 for _ in range(5)] for _ in range(size+1)]
        
        #初始化都是第一行第一列都是0
        for i in range(1, size+1):
            for j in range(5):
                if j == 0 or j == 2 or j == 4:
                    dp[i][j] = dp[i-1][j]
                    if i > 1 and j > 0:#注意j>1這個邊界條件
                        dp[i][j] = max(dp[i][j], dp[i-1][j-1]+prices[i-1]-prices[i-2])
                if j == 1 or j == 3:
                    dp[i][j] = dp[i-1][j-1]
                    if i > 1:
                        dp[i][j] = max(dp[i][j], dp[i-1][j] + prices[i-1]-prices[i-2])
        # print(dp)
        return max(dp[size][0], dp[size][2], dp[size][4])

空間優化:

class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        
        dp = [[0 for _ in range(5)] for _ in range(size+1)]
        old, now = 0, 0
        #初始化都是第一行第一列都是0
        for i in range(1, size+1):
            old = now
            now = 1 - now
            for j in range(5):
                if j == 0 or j == 2 or j == 4:
                    dp[now][j] = dp[old][j]
                    if i > 1 and j > 0:#注意j>1這個邊界條件
                        dp[now][j] = max(dp[now][j], dp[old][j-1]+prices[i-1]-prices[i-2])
                if j == 1 or j == 3:
                    dp[now][j] = dp[old][j-1]
                    if i > 1:
                        dp[now][j] = max(dp[now][j], dp[old][j] + prices[i-1]-prices[i-2])
        # print(dp)
        return max(dp[now][0], dp[now][2], dp[now][4])

股票問題4【leetcode188】

題意:規定只能進行k次交易

思路:

  1. 如果K>=N/2,那麼就是相當於進行了無初次股票交易,即貪心問題
  2. 如果K < N/2,那麼相當於股票問題3,分成2k+1個狀態。偶數狀態爲賣出狀態,奇數爲持股狀態。

因爲當前狀態只與上一狀態有關,因此可以用滾動數組來節省空間。

class Solution:
    """
    @param K: An integer
    @param prices: An integer array
    @return: Maximum profit
    """
    def maxProfit(self, K, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        if K >= size:
            res = 0
            for i in range(1, size):
                if prices[i] > prices[i-1]:
                    res += prices[i] - prices[i-1]
            return res
        dp = [[0 for _ in range(2*K+1)] for _ in range(size+1)]
        
        for i in range(2*K+1):
            dp[0][i] = 0
        
        for i in range(1, size+1):
            for j in range(2*K+1):
                if j % 2 == 0:
                    dp[i][j] = dp[i-1][j]
                    if i > 1 and j > 0:
                        dp[i][j] = max(dp[i][j], dp[i-1][j-1]+prices[i-1]-prices[i-2])
                if j % 2 == 1:
                    dp[i][j] = dp[i-1][j-1] #不越界因爲j一直大於等於1
                    if i > 1:
                        dp[i][j] = max(dp[i][j], dp[i-1][j]+prices[i-1]-prices[i-2])
        max_value = 0
        for i in range(2*K+1):
            if i % 2 == 0:
                max_value = max(dp[size][i], max_value)
        return max_value

 如果分成五個狀態【0 1 2 3 4 5】要注意狀態的有效性,得從1狀態開始,0是無效的一定要記住。因此這個界要處理好j-1>=1

#滑動數組優化記住模板
class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        size = len(prices)
        if size == 0:
            return 0
        if k > size // 2: #注意這個舉措是如果k很大,相當於prices不限制購買次數。
            dp = 0
            # min_value = float('inf')
            for i in range(1, size):
                if prices[i] > prices[i-1]:
                    dp += prices[i] - prices[i-1]
            return dp

        dp = [[0 for _ in range(2*k+1+1)] for _ in range(2)]
        old, now = 0, 0 #節省空間還不用初始化
        
        for i in range(1, size+1):
            old = now
            now = 1 - now
            for j in range(1, 2*k+1+1):
                if j % 2 == 1:
                    dp[now][j] = dp[old][j]
                    if i > 1 and j > 1:#注意這個邊界條件,令j-1>=1,並且i-2>=0 是因爲其餘的狀態都是無效的。
                        dp[now][j] = max(dp[now][j], dp[old][j-1] + prices[i-1]-prices[i-2])
                if j % 2 == 0:
                    if j > 1:#注意邊界條件
                        dp[now][j] = dp[old][j-1]
                        if i > 1:
                            dp[now][j] = max(dp[now][j], dp[old][j] + prices[i-1]-prices[i-2])
        max_value = 0
        for j in range(1, 2*k+1+1):
            if j % 2 == 1:
                max_value = max(max_value, dp[now][j])
    
        return max_value

三、劃分型動態規劃

坑:要注意枚舉的邊界

如果最後一段可以是0那麼j就可以枚舉到i

1、完全平方數[leetcode279]

思路:

  1. 根據題意是把i分成最小的平方數斷,因此屬於劃分型動態規劃問題。最後一步就看最後一段,最後一段是完全平方數j2,因爲不知道最後一段是哪個數,因此要枚舉j求最大值。f[i]表示完全平方數組成i的最小值。
  2. f[i] = f[i-j2] + 1 (0<=j2<=i) [注意什麼時候取到i要根據題意]
  3. 初始化f[0] = 0
  4. 計算順序從左到右 結果爲f[n]
class Solution:
    """
    @param n: a positive integer
    @return: An integer
    """
    def numSquares(self, n):
        # write your code here
        dp = [0 for _ in range(n+1)]
        dp[0] = 0
        
        for i in range(1, n+1):
            j = 1
            dp[i] = float('inf')
            while j*j <= i:
                dp[i] = min(dp[i], dp[i-j*j] + 1)
                j += 1
        
        return dp[-1]

2、分割回文串2【leetcode132】

思路:

  1. 分割回文串一看就是一個劃分型動態規劃問題。那麼最後一步就是最後一段,f[i]表示前i個字符劃分成幾個迴文串。由於最後一段左邊界不知道何處劃分,因此要枚舉,現在我就要問了那枚舉範圍是什麼啊。0<=j<i,j不能等於i,因爲我最後一段得有值呀對不對。
  2. f[i] = f[j] + 1(並且f[j]得是迴文串)
  3. 技巧就是先得到一個迴文串判斷矩陣(中心擴散法)有2n-1箇中心點。【奇偶互爲補充】
  4. 計算順序從左到右,計算結果爲f[size]-1
class Solution:
    def minCut(self, s: str) -> int:
        size = len(s)
        if size == 0:
            return 0
        def isPalin(s): #先用動態規劃算出是不是迴文串表
            size = len(s)
            dp = [[False for _ in range(size)] for _ in range(size)]
            for j in range(size):
                i = j
                while i >= 0 and j < size and s[i] == s[j]:
                    dp[i][j] = True
                    i -= 1
                    j += 1
            for j in range(1, size):
                i = j - 1
                while i >= 0 and j < size and s[i] == s[j]:
                    dp[i][j] = True
                    i -= 1
                    j += 1

            return dp
        dp1 = isPalin(s)

        dp2 = [0 for _ in range(size+1)]
        dp2[0] = 0

        for j in range(1, size+1):
            min_value = float('inf')
            for i in range(j):
                if dp1[i][j-1]:#注意要和isPalin得出的矩陣對應
                    min_value = min(min_value, dp2[i] + 1)
            dp2[j] = min_value

        return dp2[size]-1

3、書籍複印【lintcode437】

題意:由於每個人只能連續抄寫書,因此是一個分段問題。規定了段數爲k,因此k個人同時抄寫。狀態爲f[k][i],表示前i本書用前k個人需要的最少時間

  1. 狀態爲f[k][i],表示前i本書用前k個人需要的最少時間,最後一段表示最後一個人完成了幾本書【枚舉】
  2. 轉移方程f[k][i] = min(f[k][i], max(f[k-1][j],A[j...i-1]) )
  3. 初始化第0行第0列。邊界情況在枚舉的時候要注意,可以枚舉到i因爲最後一段可以是0,所以0<=j<=i
  4. 技巧:枚舉j的時候從後往前枚舉,這樣就不用每次重新計算A[j...i-1]了。
class Solution:
    """
    @param pages: an array of integers
    @param k: An integer
    @return: an integer
    """
    def copyBooks(self, pages, k):
        # write your code here
        size = len(pages)
        if size == 0:
            return 0
        if k >= size:
            k = size
        
        dp = [[0 for _ in range(size)] for _ in range(k+1)]
        dp[0][0] = 0
        for i in range(1, size+1):
            dp[0][i] = float('inf')
        
        for kc in range(1, k+1):#k個人
            for i in range(1, size+1):#i本書
                dp[kc][i] = float('inf')
                s = 0
                j = i
                while j >= 0: #j可以從0取到i
                    dp[kc][i] = min(dp[kc][i], max(dp[kc-1][j], s))
                    if j > 0:#爲了防止j-1出界
                        s += pages[j-1]
                    j -= 1
                
        # print(dp)
        return dp[k][size]
                    
                

 

 

 

 

 

 

 

 

發佈了15 篇原創文章 · 獲贊 1 · 訪問量 1366
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章