- 動態規劃
- 斐波那契數列-EASY
- 爬樓梯-EASY
- 使用最小花費爬樓梯-EASY
- 不同路徑-Middle
- 不同路徑II-Middle
- 不同路徑 III-HARD
- 整數拆分-MID*
- 不同的二叉搜尋樹-MID
- 揹包問題-理論基礎
- 分割等和子集-EASY
- 最後一塊石頭的重量 II-MID
- 目標和-MID *
- 一和零-MID*
- 53-最大子陣列和-中等
- 918-環形子陣列的最大和-中等
- 一維動態規劃
- 70-爬樓梯-簡單
- 198-打家劫舍-中等-記住
- 139-單詞拆分-中等
- 322-零錢兌換-中等
- 300-最長遞增子序列-中等
- 多維動態規劃
- 120-三角形最小路徑和
- 64-最小路徑和-中等
- 63-不同路徑Ⅱ-中等
- 5-最長迴文子串-中等
- 97-交錯字串-中等
- 72-編輯距離-中等
- 123-買賣股票的最佳時機Ⅲ-困難
- 股票問題通用解法
- 122-買賣股票的最佳時機Ⅱ-不限交易次數
- 309-買賣股票的最佳時機 - 含冷凍期
- 188-買賣股票的最佳時機Ⅳ - 至多交易k次
- 恰好/至少交易k次,如何初始化
- 121-買賣股票的最佳時機 - 只能交易一次 - 簡單
- 122-買賣股票的最佳時機Ⅱ-不限制交易次數-中等
- 123-買賣股票的最佳時機Ⅲ-最多交易兩次-困難
- 188-買賣股票的最佳時機Ⅳ-最多交易k次-困難
- 309-買賣股票的最佳時機-含冷凍期, 不限制交易次數-中等-再看看
- 714-買賣股票的最佳時機-含手續費-中等
動態規劃
- 基礎類題目:斐波那契數列、爬樓梯
- 揹包問題
- 打家劫舍,最後一道樹形 DP
- 股票問題
- 子序列問題,編輯距離
注意:dp 陣列的定義和下標的含義 & 遞推公式 & dp 陣列如何初始化 & 遍歷順序 & 列印 dp 陣列
斐波那契數列-EASY
分析:
斐波那契數列 1 1 2 3 5 8
- 確定 dp 陣列的含義:dp[i], 下標 i, 第 i 個斐波那契數的值為 dp[i]
- 確定遞推公式:dp[i] = dp[i-1] + dp[i-2]
- dp 陣列如何初始化:dp[0] = 1, dp[1] = 1
- 確定遍歷順序:從前向後遍歷
- 列印 dp 陣列
程式碼
class Solution:
def fib(self, n: int) -> int:
if n <=1:
return n
dp = [0 for _ in range(n+1)]
dp[0], dp[1] = 0, 1
for i in range(2, n+1, 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
問題:n=45時答案錯誤
- 當
n
的值非常大時,使用列表來儲存所有的斐波那契數可能會導致記憶體使用問題。 - 如果只需要最後一個斐波那契數(即
dp[n]
),則不需要儲存整個序列。
最佳化程式碼
class Solution:
def fib(self, n: int) -> int:
if n <=1:
return n
a, b = 0, 1
for _ in range(2, n+1):
a, b = b, a+b
return b
還是在 n=45 時出錯
大整數溢位
可以使用取模運算來避免整數過大,如在計算斐波那契數時使用% 1000000007
。
class Solution:
def fib(self, n: int) -> int:
if n <=1:
return n
a, b = 0, 1
for _ in range(2, n+1):
a, b = b, (a+b) % 1000000007
return b
時間複雜度logn的演算法
爬樓梯-EASY
程式碼
class Solution:
def climbStairs(self, n: int) -> int:
# 定義 dp 陣列
dp = [0 for _ in range(n+1)]
# dp[i] 表示 到達第 i 階有多少種不同的方法
dp[0] = 1
dp[1] = 1
for i in range(2, n+1):
dp[i] =dp[i-1] + dp[i-2]
return dp[n]
思考
為什麼爬樓梯不需要防止大整數溢位?
斐波那契數列的一個關鍵性質是它的項數隨著指數增長,但是其值卻以非常快的速率增長,很快就會超過大多數程式語言能夠表示的整數範圍。然而,對於爬樓梯問題,到達第 n
階樓梯的方法數實際上總是一個正整數,並且這個數總是小於或等於 \(2^{n}\)。這是因為在最壞的情況下,每一步都只走 1 階,這樣就有 \(n\)種 方法;而每一步都走 2 階,則最多有 \(\frac{n}{2} + \frac{n}{2}\)種方法,這在數值上不會超過 \(2^{n}\)。
使用最小花費爬樓梯-EASY
到達不花費,向上跳才花費。
程式碼
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
min_cost = math.inf
# 階梯的層數
n = len(cost)
# dp陣列 dp[i]表示到達第i層的最低花費
dp = [math.inf for _ in range(n+2)]
# 頂樓為 n+1
dp[0], dp[1] = 0, 0
for i in range(2, n+1):
dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
return dp[n]
不同路徑-Middle
程式碼
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# 每次向下或向右一步
# 定義二維 dp 陣列
# di[i][j]表示到達(i, j)的不同路徑數
dp = [[0 for _ in range(n)] for _ in range(m)]
# 如果 i<m-1, 可以向下走
# 如果 j<n-1,可以向右走
# dp 陣列初始化
# 第一行,全為 1
# 第一列,全為 1
for j in range(n):
dp[0][j] = 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][j-1] + dp[i-1][j]
return dp[m-1][n-1]
深搜程式碼 & 超時
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
def dfs(i, j):
if i > m or j > n: # 越界了
return 0
if i == m and j == n: # 找到一種方法
return 1
return dfs(i + 1, j) + dfs(i, j + 1)
return dfs(1, 1)
不同路徑II-Middle
程式碼
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
# 每次向下或者向後,到右下角
# 網格中有障礙物,障礙物網格
# 定義 dp 陣列,二維
m, n = len(obstacleGrid), len(obstacleGrid[0])
dp = [[0 for _ in range(n)] for _ in range(m)]
# dp陣列初始化
# 障礙物處為 0
# 第一行如果有障礙物,則右側均為 0
# 第一列如果有障礙物,則下方均為 0
obs_flag = False
for j in range(n):
if obstacleGrid[0][j] == 0 and not obs_flag:
dp[0][j] = 1
if obstacleGrid[0][j] == 1:
obs_flag = True
if obs_flag:
dp[0][j] = 0
obs_flag = False
for i in range(m):
if obstacleGrid[i][0] == 0 and not obs_flag:
dp[i][0] = 1
if obstacleGrid[i][0] == 1:
obs_flag = True
if obs_flag:
dp[i][0] = 0
# 遍歷
# 如果當前位置上方有障礙,只能從左側過來
# 如果當前位置左側有障礙,只能從右側過來
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] != 1:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
else:
dp[i][j] = 0
return dp[m-1][n-1]
不同路徑 III-HARD
回溯程式碼 & 四個方向 & 路徑數
class Solution:
def uniquePathsIII(self, grid:List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
def dfs(x:int, y:int, left:int) -> int:
if x<0 or x>=m or y<0 or y>=n or grid[x][y]<0:
return 0 # 不合法
# 到達終點時必須要訪問所有的無障礙方格
if grid[x][y] == 2:
return left == 0
# 標記為已訪問過
grid[x][y] = -1
# 對當前位置的上下左右四個方向進行遞迴搜尋,並計算路徑數。
ans = dfs(x-1, y, left-1) + dfs(x, y-1, left-1) + dfs(x+1, y, left-1) + dfs(x, y+1, left-1)
# 恢復現場
grid[x][y] = 0
return ans
cnt0 = sum(row.count(0) for row in grid)
for i, row in enumerate(grid):
for j, v in enumerate(row):
if v == 1:
return dfs(i, j, cnt0+1)
整數拆分-MID*
程式碼
class Solution:
def integerBreak(self, n: int) -> int:
# 拆分為 k 個正整數的和,且乘積最大化
# 返回最大乘積
# 定義 dp 陣列,dp 陣列的含義
# dp[i] 將 i 拆分為 k 個正整數且乘積最大化
dp = [0]*(n+1)
dp[0] = 0
dp[1] = 0
dp[2] = 1
for i in range(3, n+1):
for j in range(1, int(i/2)+1):
dp[i] = max(dp[i], j * dp[i-j], j*(i-j))
print(dp)
return dp[n]
思考
- 對於所求 dp[i],及要拆分的正整數 i
- 將 i 拆分為 j 和 i-j,此時有兩種方案
- i-j 不再拆分
- i-j 再次進行拆分,拆分之後的乘積依賴於dp[i-j]
不同的二叉搜尋樹-MID
程式碼
class Solution:
def numTrees(self, n: int) -> int:
# 整數 n
# n 個節點,1-n
# 互不相同的二叉搜尋樹
# 類似整數拆分
# 11 個節點,左邊 5 個,右邊 5 個;左邊 4 個,右邊 6 個;
# n = 0, 0
# n = 1, 1 種
# n = 2, 2 種
dp = [0] * (n+1)
dp[0] = 1
dp[1] = 1
if n <=1:
return n
dp[2] = 2
for i in range(3, n+1):
for j in range(0, i):
# i個節點,i-1 在子樹,拆分為j和i-1-j
dp[i] += dp[j] * dp[i-1-j]
return dp[n]
思考
- 對 i 進行拆分,根節點佔用一個值,實際拆分的是 i-1
- 拆分為 j 和 i-1-j,左側 j 個依賴於dp[j],右側 i-1-j 個,依賴於 dp[i-1-j]
- 總的種數為左右兩側相乘
- dp 陣列輸出化時,dp[0] = 1,0 個節點組成的二叉樹不存在,但左側子樹 0 個節點時,算作一種,且需要與右側種數相乘,故為 1
揹包問題-理論基礎
01 揹包:n 種物品每種物品只有一個。
完全揹包:n 種物品每種物品無限個。
多重揹包:n 種物品每種物品個數各不相同。
01 揹包問題
回溯演算法暴力搜尋
每個物品兩種狀態,取 & 不取。
二維 dp 陣列解法
dp 陣列的含義:[0, i]物品任取放入容量為 j 的揹包
遞推公式,對於 \(dp[i, j]\) 及物品 i:存在兩種狀態
- 不放物品 i:依賴於\(dp[i-1, j]\)
- 放物品 i:依賴於 \(dp[i-1, j-w[i]] + v[i]\)
dp陣列初始化:行為物品,列為容量
- 第一行初始化,物品 0,如果容量大於物品 0 的容量,最大價值為物品 0 的價值,否則價值為 0.
- 第一列初始化,容量 0,如果物品小於容量 0,則價值為物品的價值,否則價值為 0.
遍歷順序:在這裡二維 dp 陣列先遍歷物品再遍歷揹包或者先遍歷揹包再遍歷物品都可以。
二維 dp 陣列降至一維 dp 陣列 & 滾動陣列
每一行依賴於上一行的結果,將上一層資料複製下來。
dp 陣列定義:dp[j] 表示容量為 j 的揹包所能裝下的最大價值為 dp[j]
遞推公式:還是根據物品 i 來考慮的
- 不放物品 i:就是 dp[j]
- 放物品 i:\(dp[j-weight[i]] + value[i]\)
dp 陣列的初始化:
- dp[0] = 0
- 非 0 下標時,也要初始化為 0,因為遞推公式要對兩個價值取最大值,這裡應該初始化為一個小的值。
遍歷順序:先遍歷物品,再遍歷揹包,且揹包倒序遍歷,防止物品重複放入。
面試題目
- 01 揹包問題,用二維dp陣列實現,兩個 for 迴圈能不能顛倒
- 再用 1 維 dp 陣列實現,兩個 for 迴圈可不可以顛倒順序,容量 for 迴圈為什麼從後向前遍歷,二維 dp 陣列中為什麼不用從後向前遍歷
- 1 維 dp 陣列是重複利用的
分割等和子集-EASY
一維 dp 陣列 程式碼
class Solution:
def canPartition(self, nums: List[int]) -> bool:
# 等分為兩部分
if sum(nums) % 2 != 0:
return False
target = sum(nums) // 2
# 定義一維 dp 陣列
# 含義:dp[i] 容量為 i 能夠容納的最大價值
dp = [0] * (target+1)
# 初始化,dp[0] = 0,非 0 時也為 0,後續有 max 操作
# 主要這裡對物品進行遍歷
for num in nums:
# 對從0-target的所有容量進行遍歷
for j in range(target, num-1, -1):
dp[j] = max(dp[j], dp[j-num] + num)
return dp[target] == target
思路
- 抽象為 01 揹包問題
- 揹包容量是從 0 開始的
二維 dp 陣列 程式碼
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums) % 2 != 0:
return False
target = sum(nums) // 2
dp = [[False for _ in range(target + 1)] for _ in range(len(nums)+1)]
# 初始化第一列
for i in range(len(nums)+1):
dp[i][0] = True
for i in range(1, len(nums)+1):
for j in range(1, target+1):
if j < nums[i-1]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i-1]]
return dp[len(nums)][target]
最後一塊石頭的重量 II-MID
思考
- 回溯暴力應該可以做
- 揹包問題
- 兩兩石頭塊進行粉碎,求最終的最小值,也可以轉化為分成兩堆,其和儘量接近,這樣相減得到的值最小。
一維 dp 陣列程式碼
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
# 同樣分成兩堆,要求儘可能接近,這樣碰撞的差值最小
target = sum(stones) // 2
# 定義一維 dp 陣列
dp = [0] * (target+1)
# 含義 容量為 0 的揹包,能裝的最大價值
# 初始化
# 遍歷順序
for i in range(len(stones)):
# 內層迴圈保證了容量 j 都比 stone[i]要大
for j in range(target, stones[i]-1, -1):
dp[j] = max(dp[j], stones[i] + dp[j - stones[i]])
return sum(stones) - dp[target]*2
目標和-MID *
思路
- 對於每一個元素,要麼加要麼減,類似於要麼選,要麼不選,所以回溯暴力演算法應該可以解。
- 加和減也可以看做兩個集合,(分割等和子集是兩個集合相等,最後一塊石頭的重量是兩個集合儘可能接近),這道題要求兩個集合的總和為指定的 target。
- 給定的 nums 為非負整數陣列,可以拆分為兩個集合,兩個集合相減最終得到的值為 target。
如果一個集合,被減數為 A
\(A - (sum-A) = target, A-sum + A = target\)
\(A = \frac{sum + target}{2}\)
故 \(sum + target\)一定能夠整除 2.
dp 陣列含義:dp[j] 表示容量為 j 的揹包得到價值為 add 有 dp[j]種方法,總的方法為 dp[0] + dp[1] + … + dp[j]
問題就是在集合nums中找出和為 add 的組合。
確定遞推公式
- 要求 dp[j],如果加入 nums[i],則取決於dp[j - nums[i]]
- 類似的組合問題
dp[j] += dp[j - nums[i]]
一維 dp 陣列程式碼
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
# 題目拆解:運算結果等於 target == 揹包容量為 target 時,容納的最大價值為 target
# 每個元素兩種狀態,加或減
# 加集合,減集合
# 要求加集合 - 減集合 = target
# 問題抽象:加集合,揹包問題,容量為加集合的揹包所能容納的物品價值恰好等於容量,且減去減集合後為 target
# 加集合 = add
if sum(nums) < abs(target):
return 0
add = (sum(nums) + target) // 2
if (sum(nums) + target) % 2 != 0:
return 0
# 定義 dp 陣列
dp = [0] * (add + 1)
# dp陣列初始化
# 揹包容量為 0,裝滿揹包有 1 中方法
dp[0] = 1
# 考慮物品 num
for num in nums:
for j in range(add, num-1, -1):
dp[j] += dp[j-num]
return dp[add]
二維 dp 陣列程式碼
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total_sum = sum(nums)
if total_sum < abs(target):
return 0
if (target + total_sum) % 2 != 0:
return 0
# 目標和
target_sum = (target + total_sum) // 2
#建立二維 dp 陣列,行表示選取的元素數量,列表示累加和
dp = [[ 0 for _ in range(target_sum + 1)] for _ in range(len(nums)+1)]
# 初始化:0 個物品裝滿容量為 0的揹包,方法有一種
dp[0][0] = 1
# 遍歷
for i in range(1, len(nums)+1):
# 容量從 0-target_sum
for j in range(target_sum+1):
# 不選取當前物品
dp[i][j] = dp[i-1][j]
if j >= nums[i-1]:
# 選當前物品
dp[i][j] += dp[i-1][j - nums[i-1]]
return dp[len(nums)][target_sum]
- 當前元素為 \(nums[i]\),上一個元素為 \(nums[i-1]\)
- 不選取當前物品時 ✅
- 選取當前物品:如果當前容量為 j,要考慮當前容量是否大於上一個元素 \(nums[i-1]\),如果大於則 \(dp[i][j]\) 可以依賴於 \(dp[i-1][j-nums[j-1]]\)
- +=
一和零-MID*
子集需要滿足:最多 m 個 0 ,最多 n 個 1
求:這樣的子集最多有多少個元素
思路
m個 0,n 個 1 的容器,裝滿這個容器,最多有多少個元素。
容器==揹包,兩個維度的揹包,之前的揹包只有一個容量的維度,現在相當於是一個容量裝 0,一個容量裝 1,兩個維度。
裝滿這個揹包最多有多少個物品。
零一揹包,每個物品只能使用一次。
3 個變數,m,n,最多多少個物品。
每個物品的重量其實就是每個字串包含 x 個 0 和 y 個 1.
定義二維 dp 陣列
\(dp[i][j]\) 定義為 m=i,n=j 的揹包最多裝多少個物品。
遞推公式
當前物品的重量:x 個 0,y 個 1
選中當前物品之後,數量要+1
\(當前物品 重量(Num_0, Num_1) = (x, y)\)
\(dp[i-x][j-j] + 1\)
二維 dp 陣列程式碼
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
# 二進位制字串陣列
# 最大子集長度
# 最大子集最多包含 m 個 0,n 個 1
# 該子集看做一個揹包,兩個維度的揹包 m 和 n
# 定義二維 dp 陣列
dp = [[0 for _ in range(n+1)] for _ in range(m+1)]
# 含義 dp[i][j]
# m = i,n = j 的揹包,最多裝的物品數為 dp[i][j]
# 初始化
dp[0][0] = 0
# 物品個數
num = len(strs)
# 0維度重量
weight_0 = [0] * num
weight_1 = [0] * num
for idx, ss in enumerate(strs):
for ch in ss:
if ch=='1':
weight_1[idx] += 1
else:
weight_0[idx] += 1
# print(weight_0)
# print(weight_1)
# 初始化
# 非 0 索引肯定初始化為 0 沒問題,要取 max 操作
# 第一行 & 第一列 是否需要額外處理
# 遍歷順序
# 遍歷物品
for idx in range(num):
# 遍歷揹包 揹包是二維的
# 容量和物品重量的關係在 range 中進行限制
for i in range(m, weight_0[idx]-1, -1):
for j in range(n, weight_1[idx]-1, -1):
dp[i][j] = max(dp[i][j], dp[i-weight_0[idx]][j-weight_1[idx]]+1)
return dp[m][n]
53-最大子陣列和-中等
動態規劃
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
'''
最大和,連續子陣列
動態規劃
'''
arr = nums.copy()
for i in range(1, len(arr)):
arr[i] = max(arr[i], arr[i-1]+arr[i])
return max(arr)
918-環形子陣列的最大和-中等
class Solution:
def maxSubarraySumCircular(self, nums: List[int]) -> int:
'''
環形陣列
最大子陣列的兩種情況:
1. 包含首尾:中間即為最小子陣列,求最小子陣列和
2. 不包含首尾
維護最大字首和 & 最小字首和
'''
n = len(nums)
max_ = float('-inf')
min_ = float('inf')
# 無環
pre = 0 # 以nums[i]結尾的最大字首和
for i in range(n):
if pre > 0:
pre = pre + nums[i]
else:
pre = nums[i]
max_ = max(max_, pre)
# 如果最大子陣列和小於0,說明陣列全是負數,返回最大的負數即可
if max_<0:
return max(nums)
# 有環
pre = 0 # 以nums[i]結尾的最小字首和
for i in range(n):
if pre > 0:
pre = nums[i]
else:
pre = pre + nums[i]
min_ = min(min_, pre)
return max(max_, sum(nums)-min_)
一維動態規劃
70-爬樓梯-簡單
看示例,我們當前應該是處於第0階
class Solution:
def climbStairs(self, n: int) -> int:
dp = [1] * (n+1)
for i in range(2, n+1):
dp[i] = dp[i-1]+ dp[i-2]
return dp[n]
198-打家劫舍-中等-記住
class Solution:
def rob(self, nums: List[int]) -> int:
'''
相鄰的不能偷
'''
n = len(nums)
dp = [0] * (n+1)
# dp[i]表示偷竊前i個房子滿足條件獲得的最大值
# dp[i]考慮當前nums[i-1]偷還是不偷
dp[0] = 0
dp[1] = nums[0]
for i in range(2, n+1):
dp[i] = max(dp[i-1], dp[i-2]+nums[i-1])
return dp[n]
空間最佳化
class Solution:
def rob(self, nums: List[int]) -> int:
'''
很多時候我們並不需要始終持有全部的 DP 陣列
對於小偷問題,我們發現,最後一步計算 f(n) 的時候,實際上只用到了 f(n−1) 和 f(n−2) 的結果
只用兩個變數儲存兩個子問題的結果,就可以依次計算出所有的子問題
'''
prev = 0
curr = 0
for i in nums:
# 迴圈開始時,curr表示dp[i-1],prev表示dp[i-2]
prev, curr = curr, max(curr, prev+i)
# 迴圈結束時,curr表示dp[i],prev表示dp[i-1]
return curr
139-單詞拆分-中等
動態規劃
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
'''
回溯演算法超時
動態規劃,表示字串s的前i個字母是否能夠被表示
'''
n = len(s)
dp = [False] * (n+1)
dp[0] = True
for i in range(n): # 起點
for j in range(i+1, n+1):
if dp[i] and s[i:j] in wordDict:
dp[j] = True
return dp[-1]
記憶化回溯
對剩餘字串進行遞迴呼叫,將剩餘字串的呼叫結果進行快取
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
'''
記憶化回溯
使用裝飾器,裝飾器可以儲存函式的執行結果,快取
'''
import functools
# 使用lru_cache裝飾器來快取back_track函式的結果,None參數列示快取所有呼叫的結果,有助於避免重複計算
@functools.lru_cache(None)
def back_track(s: str):
# s為剩餘需要匹配的字串
if not s:
return True
res = False
for i in range(1, len(s)+1):
if s[:i] in wordDict:
res = back_track(s[i:]) or res
return res
return back_track(s)
322-零錢兌換-中等
動態規劃
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
'''
不同面額的硬幣
總金額
湊成總金額所需的最少金幣數
'''
dp = [math.inf] * (amount+1)
dp[0] = 0
for i in coins:
if i<=amount:
dp[i] = 1
for i in range(1, amount+1):
if dp[i] != math.inf:
continue
for j in coins:
if i-j >=1:
dp[i] = min(dp[i], dp[i-j]+1)
else:
continue
if dp[-1] == math.inf:
return -1
return dp[-1]
完全揹包問題
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
n = amount
dp = [math.inf] * (n+1)
dp[0] = 0
# 遍歷物品
for coin in coins:
# 遍歷揹包
for i in range(coin, n+1):
dp[i] = min(dp[i], dp[i-coin]+1)
if dp[-1] == math.inf:
return -1
return dp[-1]
300-最長遞增子序列-中等
動態規劃
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
'''
最長增長子序列
整數陣列,嚴格遞增 子序列 相對順序不改變
'''
n = len(nums)
dp = [1] * n
# dp[i] 表示到i為止最長的遞增子序列
for i in range(n):
for j in range(i, -1, -1):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j]+1)
return max(dp)
多維動態規劃
120-三角形最小路徑和
動態規劃,填充主對角線左側三角區域
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
'''
只能移動到下一行的相鄰結點
二維dp陣列
'''
n = len(triangle)
dp = [[math.inf for _ in range(n)] for _ in range(n)]
dp[0][0] = triangle[0][0]
# 從第二行開始處理
for i in range(1, n):
for j in range(i+1):
# 當前座標 (i, j)
if j-1>=0:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
else:
dp[i][j] = dp[i-1][j] + triangle[i][j]
return min(dp[-1])
64-最小路徑和-中等
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
'''
每次只能向右或向下
'''
m = len(grid)
n = len(grid[0])
dp = [[math.inf for _ in range(n)]for _ in range(m)]
# 初始化第一行
for i in range(n):
dp[0][i] = grid[0][i]
if i>0:
dp[0][i] += dp[0][i-1]
# 初始化第一列
for i in range(m):
dp[i][0] = grid[i][0]
if i>0:
dp[i][0] += dp[i-1][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[-1][-1]
63-不同路徑Ⅱ-中等
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
'''
向下或向右
二維dp陣列記錄到大當前位置有幾種路徑
'''
# 如果起始位置有石頭,直接返回0
if obstacleGrid[0][0] == 1:
return 0
m = len(obstacleGrid)
n = len(obstacleGrid[0])
dp = [[0 for _ in range(n)]for _ in range(m)]
dp[0][0] = 1
# 初始化第一行
i = 1
while i<n:
if obstacleGrid[0][i] == 1:
break
dp[0][i] = 1
i += 1
while i<n:
dp[0][i] = 0
i += 1
# 初始化第一列
i = 1
while i<m:
if obstacleGrid[i][0] == 1:
break
dp[i][0] = 1
i += 1
while i<m:
dp[i][0] = 0
i += 1
# 遍歷
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 1:
dp[i][j] = 0
elif dp[i-1][j] != 0 and dp[i][j-1] != 0:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
5-最長迴文子串-中等
❌錯誤的中心擴散法
這樣選定中心點,同時向兩邊擴散,只能處理迴文子串為奇數的情況,無法處理偶數的情況,如上述bb
class Solution:
def longestPalindrome(self, s: str) -> str:
'''
中心擴散法
'''
max_len = 1
res = s[0]
n =len(s)
for i in range(n):
left = i-1
right = i+1
while left >=0 and right <= n-1:
if s[left] != s[right]:
break
if right-left+1>max_len:
max_len = right-left+1
res = s[left:right+1]
left -= 1
right += 1
return res
✔ 正確的中心擴散法
考慮當前中心點時,應將左右指標分別移動到不等於中心點處,再向左右擴充套件
class Solution:
def longestPalindrome(self, s: str) -> str:
if not s:
return ''
n = len(s)
cur_len = 1 # ! 初始化為1
max_len = 1
res = s[0]
for i in range(n):
left = i-1
right = i+1
# 向左擴充套件迴文子串
while left>=0 and s[left] == s[i]:
cur_len += 1
left -= 1
# 向右擴充套件迴文子串
while right<=n-1 and s[right] == s[i]:
cur_len += 1
right += 1
# 上面的程式碼解決當中心點左右兩側與中心點字元相同的情況
# 將left和right移動到與中心點不同的位置
# 左右向兩側擴充套件
while left >=0 and right <=n-1 and s[left]==s[right]:
cur_len += 2
left -= 1
right += 1
if cur_len > max_len:
max_len = cur_len
res = s[left+1: right]
cur_len = 1
return res
動態規劃
class Solution:
def longestPalindrome(self, s: str) -> str:
'''
dp[l][r] 表示字元從l到r是否為迴文串
如果要判斷dp[l-1][r+1]是否為迴文串,必須dp[l][r]是迴文,且s[l-1]==s[r+1]
'''
max_len = 0
n = len(s)
if n<2:
return s
res = s[0]
dp = [[False for _ in range(n)]for _ in range(n)]
# 初始化,對角線處為True
for i in range(n):
dp[i][i] = True
# 遍歷
# 先列舉子串的長度
for L in range(2, n+1):
# 左邊界
for l in range(n):
# 右邊界
r = l+L-1
if r>n-1:
break
if s[l] != s[r]:
dp[l][r] = False
else:
if r-l<3: # 🐖注意這裡的處理!
dp[l][r] = True
else:
dp[l][r] = dp[l+1][r-1]
if dp[l][r] and r-l+1 > max_len:
max_len = r-l+1
res = s[l:r+1]
return res
97-交錯字串-中等
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
'''
d[i][j] 表示s1的前i個和s2的前j個字元能夠構成s3的前i+j個
'''
m = len(s1)
n = len(s2)
if m+n != len(s3):
return False
dp = [[ False for _ in range(n+1)]for _ in range(m+1)]
# 初始化
dp[0][0] = True
# 初始化第一行,s2的前i個能否構成s3的前i個
for i in range(1, n+1):
if s2[:i] == s3[:i]:
dp[0][i] = True
# 初始化第一列,s1的前i個能否構成s3的前i個
for i in range(1, m+1):
if s1[:i] == s3[:i]:
dp[i][0] = True
# 遍歷
for i in range(1, m+1):
for j in range(1, n+1):
# dp[i][j]
# s1的前i個字元與s2的前j-1個字元可以構成s3的前i+j-1個字元
# 當前s2的第j個字元也等於s3的第i+j個字元
# 則s1的前i個字元與s2的前j個字元可以構成s3的前i+j個字元
if dp[i][j-1] and s2[j-1] == s3[i+j-1]:
dp[i][j] = True
if dp[i-1][j] and s1[i-1] == s3[i+j-1]:
dp[i][j] = True
return dp[-1][-1]
72-編輯距離-中等
二維DP-錯誤程式碼❌
只考慮了修改和插入兩個操作
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
'''
編輯距離,word1轉換為word2所需的最小運算元
二維dp
dp[i][j] 表示 word1的前i個字元轉換為word2的前j個字元需要的最小運算元
三種操作:插入,刪除,替換
'''
m = len(word1)
n = len(word2)
dp = [[math.inf for _ in range(n+1)] for _ in range(m+1)]
# 初始化
dp[0][0] = 0
# 第一行,word1為空,轉換為word2需要的最小次數
for i in range(1, n+1):
dp[0][i] = i
# 第一列
for i in range(1, m+1):
dp[i][0] = i
# 遍歷
for i in range(1, m+1):
for j in range(1, n+1):
# dp[i][j]
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
if j>i:
dp[i][j] = dp[i][j-1]+1
elif j<i:
dp[i][j] = dp[i-1][j]+1
else:
dp[i][j] = dp[i-1][j-1]+1
return dp[-1][-1]
二維DP-正確程式碼
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
'''
編輯距離,word1轉換為word2所需的最小運算元
dp[i][j] 表示 word1的前i個字元轉換為word2的前j個字元需要的最小運算元
三種操作:插入,刪除,替換
如果word1和word2其中一個為空時,即全增加或全刪除的情況
插入:dp[i][j] = dp[i][j-1]+1
刪除:dp[i][j] = dp[i - 1][j] + 1
修改:dp[i][j] = dp[i - 1][j - 1] + 1
'''
m = len(word1)
n = len(word2)
dp = [[math.inf for _ in range(n+1)] for _ in range(m+1)]
# 初始化
dp[0][0] = 0
# 第一行,word1為空,轉換為word2需要的最小次數
for i in range(1, n+1):
dp[0][i] = i
# 第一列
for i in range(1, m+1):
dp[i][0] = i
# 遍歷
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
# 取增刪改的最小值 修改 刪除 插入
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])+1
return dp[-1][-1]
123-買賣股票的最佳時機Ⅲ-困難
暴力-超時程式碼
假設第i天買入,找到所有收益為正向的天數
從收益為正的天再次出發買入,找之後天賣出的最大收益
class Solution:
def maxProfit(self, prices: List[int]) -> int:
'''
買賣股票的最佳時機
獲取最大利潤
最多買賣兩次
二維DP,第i天買,第j天賣
'''
n = len(prices)
dp = [[0 for _ in range(n)] for _ in range(n)]
# 主對角線均為0,代表當天買當天賣為0
for i in range(n):
for j in range(i+1, n):
# 第i天買第j天賣
dp[i][j] = prices[j]-prices[i] if prices[j]-prices[i]>0 else 0
# 需要從中找兩個買賣的端點
max_prof = 0
for i in range(n):
# 找利潤大於0的索引
idx_list = []
for idx, val in enumerate(dp[i]):
if val == 0:
continue
idx_list.append(idx)
for j in idx_list:
cur_prof = 0
# 第i天賣,第j天賣,第一次交易
cur_prof += dp[i][j]
# 第二次交易,在第j天之後尋找
sec_prof = 0
for k in range(j, n):
sec_prof = max(sec_prof, max(dp[k]))
cur_prof += sec_prof
if cur_prof > max_prof:
max_prof = cur_prof
return max_prof
動態規劃-正確程式碼
class Solution:
def maxProfit(self, prices: List[int]) -> int:
'''
最多兩筆買賣
任意一天結束後,會處於以下狀態之一:
1. 未進行任何操作
2. 只進行過一次買
3. 一次買,一次賣,完成一次交易
4. 一次交易完成,再次買入
5. 完成兩次交易
第一種狀態利潤為0
剩下的四種狀態,分別將最大利潤記為 buy1, sell1, buy2, sell2
如果知道了第i-1天結束後的這四種狀態,如何透過狀態轉移方程得到第i天結束後的四種狀態
1. 對於buy1,第i天結束可以不進行任何操作保持不變,也可以以價格prices[i]買入
buy1 = max(buy1, -prices[i])
2. 對於sell1,第 i 天我們可以不進行任何操作,保持不變,也可以在只進行過一次買操作的前提下以 prices[i] 的價格賣出股票
sell1 = max(sell1, buy1+prices[i])
3. 對於buy2,
buy2 = max(buy2, sell1-prices[i])
4. sell2 = max(sell2', buy2+prices[i])
邊界條件:
第 i=0 天時的四個狀態:
以 prices[0] 的價格買入股票 buy1=-prices[0]
sell1即為在同一天買入並且賣出,因此 sell1=0
buy2 同一天買入賣出再以prices[0]買入 buy2=-prices[0]
sell2 買入再賣出 sell2=0
將這四個狀態作為邊界條件,從 i=1 開始進行動態規劃,即可得到答案。
在動態規劃結束後,由於我們可以進行不超過兩筆交易,因此最終的答案在0, sell1, sell2中的最大值
在最有情況下,恰好進行一筆交易,也可以歸為同一天賣出再買入的情況,最後返回sell2
'''
n = len(prices)
# 第i=0天時買入
buy1 = -prices[0]
sell1 = 0
buy2 = -prices[0]
sell2 = 0
# 理解這四個轉移方程
for i in range(1, n):
buy1 = max(buy1, -prices[i])
sell1 = max(sell1, buy1+prices[i])
buy2 = max(buy2, sell1-prices[i])
sell2 = max(sell2, buy2+prices[i])
return sell2
股票問題通用解法
122-買賣股票的最佳時機Ⅱ-不限交易次數
給你一個整數陣列 prices
,其中 prices[i]
表示某支股票第 i
天的價格。
在每一天,你可以決定是否購買和/或出售股票。你在任何時候最多隻能持有一股股票。你也可以先購買,然後在 同一天 出售。
返回 你能獲得的 最大 利潤 。
為了後面改為遞迴,從最後一天向前思考。
啟發思路:最後一天發生了什麼?
從第0天開始到第5天結束時的利潤 = 從第0天開始到第4天結束時的利潤 + 第5天的利潤
1.)如果第5天什麼也不做,利潤為0
2.)如果第5天買入股票,利潤就是-4
3.)如果第5天賣出股票,利潤就是+4
在第i-1天結束時,如果買入股票,那在第i天開始時的狀態就是持有股票
在第i-1天結束時,如果賣出股票,那在第i天開始時的狀態就是未持有股票
如果什麼也不做,那麼狀態不變
這種表示狀態之間轉換關係的圖叫狀態機
從這張圖中可以看出,狀態轉移有這4中情況
定義 dfs(i, 0)
表示到第i天結束時,未持有股票的最大利潤
定義 dfs(i, 1)
表示到第i天結束時,持有股票的最大利潤
由於第i-1天的結束就是第i天的開始
dfs(i-1, •)
也表示到第i天開始時的最大利潤
\(\begin{array}{l} d f s(i, 0)=\max (d f s(i-1,0), d f s(i-1,1)+\operatorname{prices}[i]) \\ d f s(i, 1)=\max (d f s(i-1,1), d f s(i-1,0)-\operatorname{prices}[i]) \end{array}\)
第i天結束時未持有股票的利潤 = max( 第i-1天結束時未持有股票的利潤,第i-1天結束時持有股票的利潤 + 當前賣出的獲得的利潤 )
第i天結束時持有股票的利潤 = max( 第i-1天結束時持有股票的利潤, 第i-1天結束時未持有股票的利潤 - 當前買入獲得的利潤)
遞迴邊界,
dfs(-1, 0) = 0
第0天開始未持有股票,利潤為0
dfs(-1, 1) = -∞
第0天開始時不可能持有股票
遞迴入口
max( dfs(n-1, 0), dfs(n-1,1))
如果在最後一天還持有股票就賣不出去了,所以 dfs(n-1, 1)
是不會比dfs(n-1, 0)
大的。
所以遞迴入口是 dfs(n-1, 0)
不使用cache裝飾器會超時,改為記憶化搜尋
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
# 需要改為記憶化搜尋, cache裝飾器
@cache
def dfs(i, hold):
if i<0:
return -inf if hold else 0
if hold:
# 第i天結束時持有
return max(dfs(i-1, True), dfs(i-1, False)-prices[i])
# 第i天結束時未持有
return max(dfs(i-1, False), dfs(i-1, True)+prices[i])
return dfs(n-1, 0)
如果將遞迴翻譯未遞推的形式
插入一個狀態表示i=-1
# 擊敗5%
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
f = [[0]*2 for _ in range(n+1)]
f[0][0] = 0
f[0][1] = -inf
for i, p in enumerate(prices):
f[i+1][0] = max(f[i][0], f[i][1] + p)
f[i+1][1] = max(f[i][1], f[i][0] - p)
return f[-1][0]
空間最佳化
# 擊敗95%
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
f0 = 0
f1 = -inf
for p in prices:
new_f0 = max(f0, f1+p)
f1 = max(f1, f0-p)
f0 = new_f0
return f0
309-買賣股票的最佳時機 - 含冷凍期
給定一個整數陣列prices
,其中第 prices[i]
表示第 i
天的股票價格 。
設計一個演算法計算出最大利潤。在滿足以下約束條件下,你可以儘可能地完成更多的交易(多次買賣一支股票):
賣出股票後,你無法在第二天買入股票 (即冷凍期為 1 天)。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
思考,除了是否持有股票的狀態,現在新增了冷凍期的狀態
買入股票的時候,前一天不能有賣出的操作,那麼從再前一天轉移過來
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
@cache
def dfs(i, hold):
if i<0:
return -inf if hold else 0
if hold:
# 第i天結束時持有股票
# 第i-1天結束時就持有,當前什麼也不做
# 第i-1天結束時未持有,當前買入
# 買入之前不能有賣出
return max(dfs(i-1, True), dfs(i-2, False)-prices[i])
# 第i天結束時,未持有
return max(dfs(i-1, False), dfs(i-1, True)+prices[i])
return dfs(n-1, False)
188-買賣股票的最佳時機Ⅳ - 至多交易k次
給你一個整數陣列 prices
和一個整數 k
,其中 prices[i]
是某支給定的股票在第 i
天的價格。
設計一個演算法來計算你所能獲取的最大利潤。你最多可以完成 k
筆交易。也就是說,你最多可以買 k
次,賣 k
次。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
有次數限制,應當在遞迴的過程中記錄次數
定義 dfs(i, j, 0)
表示第i天結束時完成至多j筆交易,未持有股票的最大利潤
定義 dfs(i, j, 1)
表示第i天結束時完成至多j筆交易,持有股票的最大利潤
\(\begin{array}{l} \operatorname{dfs}(i, j, 0)=\max (d f s(i-1, j, 0), \operatorname{dfs}(i-1, j, 1)+\operatorname{prices}[i]) \\ \operatorname{dfs}(i, j, 1)=\max (d f s(i-1, j, 1), \operatorname{dfs}(i-1, j-1,0)-\operatorname{prices}[i]) \end{array}\)
第i天結束時最多完成j筆交易未持有,前一天結束時最多完成j筆交易未持有 什麼也不做, 前一天結束時最多完成j筆交易持有 當前賣出
第i天結束時最多完成j筆交易持有,前一天結束時最多完成j筆交易持有 什麼也不做,前一天結束時最多完成j-1筆交易 當前買入
買入一次就算一筆交易
遞迴邊界
dfs(·, -1, ·) = -∞
任何情況下,j都不能未負
dfs(-1, j, 0) = 0
dfs(-1, j, 1) = -∞
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
@cache
def dfs(i, j, hold):
if j<0:
return -inf
if i<0:
return -inf if hold else 0
if hold:
# 當前持有
# 買入時記作一次交易開始
return max(dfs(i-1, j, 1), dfs(i-1, j-1, 0) - prices[i])
# 當前未持有
return max(dfs(i-1, j, 0), dfs(i-1, j, 1) + prices[i])
return dfs(n-1, k, False)
遞推程式碼
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
# 遞推
# 建立三維陣列, 為什麼k+2
f = [[[0]*2 for _ in range(k+1)] for _ in range(n+1)]
# 初始化:無論允許交易多少次,第0天開始時持有股票都是不可能的,初始化為-∞
for i in range(k+1):
f[0][i][1] = -inf
for i, p in enumerate(prices):
for j in range(1, k+1):
# 未持有:什麼也不做 || 賣出
f[i+1][j][0] = max(f[i][j][0], f[i][j][1] + p)
# 持有:什麼也不做 || 買入
f[i+1][j][1] = max(f[i][j][1], f[i][j-1][0] - p)
return f[-1][-1][0]
恰好/至少交易k次,如何初始化
恰好完成k次初始化條件
f[0][1][0] = 0
第0天,恰好完成一次交易,當天買入當天賣出,利潤為0
其餘均初始化為 -inf
# 恰好
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
# 遞推
n = len(prices)
f = [[[-inf] * 2 for _ in range(k + 2)] for _ in range(n + 1)]
# 只有第0天,恰好交易1次,當天買當天賣,未持有 利潤為0
f[0][1][0] = 0 # 只需改這裡
for i, p in enumerate(prices):
for j in range(1, k + 2):
f[i + 1][j][0] = max(f[i][j][0], f[i][j][1] + p)
f[i + 1][j][1] = max(f[i][j][1], f[i][j - 1][0] - p)
return f[-1][-1][0]
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
# 遞推
n = len(prices)
# 恰好完成k次交易
f = [[[-inf]*2 for _ in range(k+1)]for _ in range(n+1)]
# 初始化
至少完成k次初始化條件
第0天
[0-k-1]均初始化為-inf
第k次初始化為0
# 至少
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
# 遞推
n = len(prices)
f = [[[-inf] * 2 for _ in range(k + 1)] for _ in range(n + 1)]
# 初始化
# 第0天開始,恰好交易0次,利潤為0
f[0][0][0] = 0
for i, p in enumerate(prices):
# 第i天結束時,恰好交易0次,未持有
f[i + 1][0][0] = max(f[i][0][0], f[i][0][1] + p)
f[i + 1][0][1] = max(f[i][0][1], f[i][0][0] - p) # 無限次
for j in range(1, k + 1):
f[i + 1][j][0] = max(f[i][j][0], f[i][j][1] + p)
f[i + 1][j][1] = max(f[i][j][1], f[i][j - 1][0] - p)
return f[-1][-1][0]
121-買賣股票的最佳時機 - 只能交易一次 - 簡單
給定一個陣列 prices
,它的第 i
個元素 prices[i]
表示一支給定股票第 i
天的價格。
你只能選擇某一天買入這隻股票,並選擇在 未來的某一個不同的日子 賣出該股票。設計一個演算法來計算你所能獲取的最大利潤。
返回你可以從這筆交易中獲取的最大利潤。如果你不能獲取任何利潤,返回 0
。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
'''
維護到當前為止的最小价格 & 最大利潤
'''
min_price = prices[0]
max_profit = 0
for p in prices:
if p<min_price:
min_price = p
if max_profit<p-min_price:
max_profit = p-min_price
return max_profit
如何用狀態機來做??等同於恰好交易一次的情況
注意初始化方式
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
f = [[[0]*2 for _ in range(1+1)]for _ in range(n+1)]
for i in range(1+1):
f[0][i][1] = -inf
for i, p in enumerate(prices):
for j in range(1, 1+1):
f[i+1][j][0] = max(f[i][j][0], f[i][j][1]+p)
f[i+1][j][1] = max(f[i][j][1], f[i][j-1][0]-p)
return f[-1][-1][0]
122-買賣股票的最佳時機Ⅱ-不限制交易次數-中等
給你一個整數陣列 prices
,其中 prices[i]
表示某支股票第 i
天的價格。
在每一天,你可以決定是否購買和/或出售股票。你在任何時候 最多 只能持有 一股 股票。你也可以先購買,然後在 同一天 出售。
返回 你能獲得的 最大 利潤 。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
'''
不限制交易次數
二維陣列即可
'''
n = len(prices)
f = [[0]*2 for _ in range(n+1)]
# 初始化
# 第0天開始時持有是不可能的
f[0][1] = -inf
for i, p in enumerate(prices):
# 未持有
f[i+1][0] = max(f[i][0], f[i][1]+p)
# 持有
f[i+1][1] = max(f[i][1], f[i][0]-p)
return f[-1][0]
123-買賣股票的最佳時機Ⅲ-最多交易兩次-困難
給定一個陣列,它的第 i
個元素是一支給定的股票在第 i
天的價格。
設計一個演算法來計算你所能獲取的最大利潤。你最多可以完成 兩筆 交易。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
'''
最多完成兩筆交易
'''
n = len(prices)
f = [[[0]*2 for _ in range(3)]for _ in range(n+1)]
# 初始化
# 第0天開始時持有是不可能的
for i in range(3):
f[0][i][1] = -inf
# 遞推
for i, p in enumerate(prices):
for j in range(1, 3):
f[i+1][j][0] = max(f[i][j][0], f[i][j][1]+p)
f[i+1][j][1] = max(f[i][j][1], f[i][j-1][0]-p)
return f[-1][-1][0]
188-買賣股票的最佳時機Ⅳ-最多交易k次-困難
給你一個整數陣列 prices
和一個整數 k
,其中 prices[i]
是某支給定的股票在第 i
天的價格。
設計一個演算法來計算你所能獲取的最大利潤。你最多可以完成 k
筆交易。也就是說,你最多可以買 k
次,賣 k
次。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
'''
最多交易k次
三維表示
'''
n = len(prices)
f = [[[0]*2 for _ in range(k+1)]for _ in range(n+1)]
# 初始化
# 第0天開始時持有是不可能的
for i in range(k+1):
f[0][i][1] = -inf
# 遞推
for i, p in enumerate(prices):
for j in range(1, k+1):
# 未持有
f[i+1][j][0] = max(f[i][j][0], f[i][j][1]+p)
f[i+1][j][1] = max(f[i][j][1], f[i][j-1][0]-p)
return f[-1][-1][0]
309-買賣股票的最佳時機-含冷凍期, 不限制交易次數-中等-再看看
給定一個整數陣列prices
,其中第 prices[i]
表示第 i
天的股票價格 。
設計一個演算法計算出最大利潤。在滿足以下約束條件下,你可以儘可能地完成更多的交易(多次買賣一支股票):
賣出股票後,你無法在第二天買入股票 (即冷凍期為 1 天)
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)
class Solution:
def maxProfit(self, prices: List[int]) -> int:
'''
不限制交易次數
二維即可
賣出股票後,無法接著買入,冷凍期為一天
但是買入之後,沒有賣出限制
'''
n = len(prices)
if n < 2:
return 0
# 初始化
f = [[0] * 2 for _ in range(n+2)] # 需要額外兩個空間用於狀態轉移
f[0][1] = -float('inf') # 第0天開始時持有是不可能的
f[1][1] = -prices[0] # 第1天持有股票的情況
# 第二天到第n天的情況
for i in range(2, n+1):
# 未持有:什麼也不做 || 賣出,賣出無限制
f[i][0] = max(f[i-1][0], f[i-1][1] + prices[i-1])
# 持有:只能從昨天未持有狀態買入
f[i][1] = max(f[i-1][1], f[i-2][0] - prices[i-1])
return f[-2][0]
714-買賣股票的最佳時機-含手續費-中等
給定一個整數陣列 prices
,其中 prices[i]
表示第 i
天的股票價格 ;整數 fee
代表了交易股票的手續費用。
你可以無限次地完成交易,但是你每筆交易都需要付手續費。如果你已經購買了一個股票,在賣出它之前你就不能再繼續購買股票了。
返回獲得利潤的最大值。
注意:這裡的一筆交易指買入持有並賣出股票的整個過程,每筆交易你只需要為支付一次手續費。
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
'''
不限制交易次數
'''
n = len(prices)
f = [[0]* 2 for _ in range(n+1)]
# 初始化
f[0][1] = -inf
# 只在買入時計算手續費
for i, p in enumerate(prices):
f[i+1][0] = max(f[i][0], f[i][1] + p)
f[i+1][1] = max(f[i][1], f[i][0] - p - fee)
return f[-1][0]