九章算法動態規劃總結
動態規劃分類:
- 座標型動態規劃
- 序列型動態規劃
- 劃分型動態規劃
- 最長上升子序列
- 揹包型動態規劃
- 區間型動態規劃
- 綜合型動態規劃
思路:
- 定義狀態(根據最後一步和子問題)
- 寫出狀態(根據最後一步和子問題)
- 初始化和界內處理
- 計算順序、計算結果(判斷是否可以用滑動數組節省空間)
一、座標型動態規劃(最簡單的)
方法:
典型特點是以座標所在的意義作爲狀態。如一維和二維數組:dp=[m][n] or dp=[m]
- 給定輸入爲序列或者矩陣
- 狀態序列下標爲下標i或者(i,j);以第i個元素結尾的性質或者以i,j結尾的路徑的性質
- 初始化設置爲f[0]或f[0][0...n-1]
- 二維空間優化:如果f[i][j]的值只依賴於當前行和前一行,則可以用滾動數組節省時間。
1、不同路徑(無障礙)【leetcode62】
分析過程:
- f[i][j]表示到達終點的路徑的個數
- f[i][j] = f[i-1][j] + f[i][j](注意越界)
- f[0][0] = 1 ;越界處理即對f[i-1][j] + f[i][j]中ij的約束
- 計算順序:從左到右,從上到下;計算結果: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)
- 如果start就有路障,那麼return 0(因此不能一開始就初始化dp[0][0] = 1)
- 初始化dp[0][0] = 1
- 如果有路障,那麼dp[i][j]=0 則continue
- 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】
思路:
- 最後一步可能能跳到i,也有可能調不到i,因此定義f(i)爲能否跳到位置i
- 狀態轉移方程:f[i] = f[j] (f[j] is true and nums[j] >= i-j)!!!!!!!!這個我經常很糊塗,不知道從哪直接能跳到最後一步,因此要枚舉!
- 初始化f[0]=0,無越界
- 計算順序從左到右,返回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) 額外空間來解決
- f[i]表示以i爲結尾的最大連續子序列的長度
- f[i] = f[i-1]+1 or 1
- f[0]=nums[0]
- 計算順序從左到右,結果是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個敵人
思路:
- 分成上下左右四個方向就容易了,這個題的難點就是沒想到分成上下左右四個方向
- 注意在分方向的時候計算順序也會改變
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】
思路:
- 最後一步:由於題目說房子只能刷紅藍綠三種顏色,那麼最後一個房子不能和上一個房子顏色一樣,上一個房子可能是紅色、藍色或綠色(分情況討論,找出最小的)。如果上一個房子是紅色,那麼最後一個房子只能刷綠色或者藍色,求出來最小的那個就是上一個房子刷紅色的花費值。由於fi和顏色有關,我們將顏色加入f[i][j],表示前i個房子(第i-1刷j色)的花費
- f[i][j] = min(k!=j)(f[i-1][k] + costs[i-1][j]) 開數組(dp[i][j] = [m+1][n])
- 初始化第一行爲0
- 計算順序從左到右從上到下,結果就是min(dp[m])
- 關鍵在於最後一個房子和上一個房子的顏色有關,因此把顏色也放在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】
思路:
- 最後一步是偷房子i-1還是不偷呢,如果偷房子i,那麼和它相鄰的前一個房子一定不能偷,因此用f[i]表示前i個房子最大偷錢值。
- f[i] = max(f[i-1], f[i-2]+A[i-1]
- 初始化f[0],f[1]
- 計算順序從左到右,結果是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】
題意:只能買賣一次股票,求獲利最大是多少。
思路:
- 最後一步:如果最後一步之前已經賣了,那麼f[i]=f[i-1],如果最後一步賣f[i]=price[i-1]-min_val f[i]表示前i天獲利
- 轉換方程就是上述最大
- dp[0]= 0
- 計算順序從左到右,計算結果最後一天。
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這樣表示,防止越界。
思路:思考最後一步是第一次賣之後還是第二次賣之後還是沒買過之後,不知道,因此把狀態寫入動態規劃
- f[i][j]表示前i天處在j狀態的獲利情況。j分成五個狀態,分別爲0:第一次買之前;1:第一次買之後(持股);2:第一次賣之後第一次買之前;3:第二次買之後(持股);4:第二次賣之後。最後一步只能是處於0, 2,4狀態,因此答案只能是在這三個狀態下求最大值即max(f[size][0],f[size][2],f[size][4])
- 狀態轉移方程對於狀態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])
- 初始化dp[0][0,4]=0,注意j的出界處理。
- 計算順序從左到右,從上到下,結果爲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次交易
思路:
- 如果K>=N/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]
思路:
- 根據題意是把i分成最小的平方數斷,因此屬於劃分型動態規劃問題。最後一步就看最後一段,最後一段是完全平方數j2,因爲不知道最後一段是哪個數,因此要枚舉j求最大值。f[i]表示完全平方數組成i的最小值。
- f[i] = f[i-j2] + 1 (0<=j2<=i) [注意什麼時候取到i要根據題意]
- 初始化f[0] = 0
- 計算順序從左到右 結果爲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】
思路:
- 分割回文串一看就是一個劃分型動態規劃問題。那麼最後一步就是最後一段,f[i]表示前i個字符劃分成幾個迴文串。由於最後一段左邊界不知道何處劃分,因此要枚舉,現在我就要問了那枚舉範圍是什麼啊。0<=j<i,j不能等於i,因爲我最後一段得有值呀對不對。
- f[i] = f[j] + 1(並且f[j]得是迴文串)
- 技巧就是先得到一個迴文串判斷矩陣(中心擴散法)有2n-1箇中心點。【奇偶互爲補充】
- 計算順序從左到右,計算結果爲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個人需要的最少時間
- 狀態爲f[k][i],表示前i本書用前k個人需要的最少時間,最後一段表示最後一個人完成了幾本書【枚舉】
- 轉移方程f[k][i] = min(f[k][i], max(f[k-1][j],A[j...i-1]) )
- 初始化第0行第0列。邊界情況在枚舉的時候要注意,可以枚舉到i因爲最後一段可以是0,所以0<=j<=i
- 技巧:枚舉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]