力扣-動態規劃全解

幻影星全能的木豆發表於2024-07-22

目錄
  • 動態規劃
    • 斐波那契數列-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

  1. 確定 dp 陣列的含義:dp[i], 下標 i, 第 i 個斐波那契數的值為 dp[i]
  2. 確定遞推公式:dp[i] = dp[i-1] + dp[i-2]
  3. dp 陣列如何初始化:dp[0] = 1, dp[1] = 1
  4. 確定遍歷順序:從前向後遍歷
  5. 列印 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

image-20240722114606879

程式碼

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

image-20240722114634907

到達不花費,向上跳才花費。

程式碼

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

image-20240722114652105

程式碼

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

image-20240722114718419

程式碼

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

image-20240722114733596

回溯程式碼 & 四個方向 & 路徑數

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*

image-20240722114751895

程式碼

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]

思考

  1. 對於所求 dp[i],及要拆分的正整數 i
  2. 將 i 拆分為 j 和 i-j,此時有兩種方案
    1. i-j 不再拆分
    2. i-j 再次進行拆分,拆分之後的乘積依賴於dp[i-j]

不同的二叉搜尋樹-MID

image-20240722114814500

程式碼

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]

思考

  1. 對 i 進行拆分,根節點佔用一個值,實際拆分的是 i-1
  2. 拆分為 j 和 i-1-j,左側 j 個依賴於dp[j],右側 i-1-j 個,依賴於 dp[i-1-j]
  3. 總的種數為左右兩側相乘
  4. dp 陣列輸出化時,dp[0] = 1,0 個節點組成的二叉樹不存在,但左側子樹 0 個節點時,算作一種,且需要與右側種數相乘,故為 1

揹包問題-理論基礎

01 揹包:n 種物品每種物品只有一個。

完全揹包:n 種物品每種物品無限個。

多重揹包:n 種物品每種物品個數各不相同。

01 揹包問題

image-20240722114918463

回溯演算法暴力搜尋

每個物品兩種狀態,取 & 不取。

二維 dp 陣列解法

dp 陣列的含義:[0, i]物品任取放入容量為 j 的揹包

遞推公式,對於 \(dp[i, j]\) 及物品 i:存在兩種狀態

  1. 不放物品 i:依賴於\(dp[i-1, j]\)
  2. 放物品 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 來考慮的

  1. 不放物品 i:就是 dp[j]
  2. 放物品 i:\(dp[j-weight[i]] + value[i]\)

dp 陣列的初始化:

  • dp[0] = 0
  • 非 0 下標時,也要初始化為 0,因為遞推公式要對兩個價值取最大值,這裡應該初始化為一個小的值。

遍歷順序:先遍歷物品,再遍歷揹包,且揹包倒序遍歷,防止物品重複放入。

面試題目

  1. 01 揹包問題,用二維dp陣列實現,兩個 for 迴圈能不能顛倒
  2. 再用 1 維 dp 陣列實現,兩個 for 迴圈可不可以顛倒順序,容量 for 迴圈為什麼從後向前遍歷,二維 dp 陣列中為什麼不用從後向前遍歷
    1. 1 維 dp 陣列是重複利用的

分割等和子集-EASY

image-20240722114935107

一維 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

image-20240722114950856

思考

  1. 回溯暴力應該可以做
  2. 揹包問題
    1. 兩兩石頭塊進行粉碎,求最終的最小值,也可以轉化為分成兩堆,其和儘量接近,這樣相減得到的值最小。

一維 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 *

image-20240722115015675

思路

  • 對於每一個元素,要麼加要麼減,類似於要麼選,要麼不選,所以回溯暴力演算法應該可以解。
  • 加和減也可以看做兩個集合,(分割等和子集是兩個集合相等,最後一塊石頭的重量是兩個集合儘可能接近),這道題要求兩個集合的總和為指定的 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]]
  • image-20240722115035916
  • 類似的組合問題 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*

image-20240722115054956

子集需要滿足:最多 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-最大子陣列和-中等

image-20240722115108440

動態規劃

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-環形子陣列的最大和-中等

image-20240722115121213

image-20240722115133336

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-爬樓梯-簡單

image-20240716125146271

看示例,我們當前應該是處於第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-打家劫舍-中等-記住

image-20240716143043039

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-單詞拆分-中等

image-20240716145854162

動態規劃

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-零錢兌換-中等

image-20240716153237892

動態規劃

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-最長遞增子序列-中等

image-20240716155546512

動態規劃

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-三角形最小路徑和

image-20240716160749206

動態規劃,填充主對角線左側三角區域

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-最小路徑和-中等

image-20240716161750047

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-不同路徑Ⅱ-中等

image-20240716163712206

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-最長迴文子串-中等

image-20240717105045874

❌錯誤的中心擴散法

這樣選定中心點,同時向兩邊擴散,只能處理迴文子串為奇數的情況,無法處理偶數的情況,如上述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-交錯字串-中等

image-20240717121118012

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-編輯距離-中等

image-20240719101741798

二維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-正確程式碼

image-20240719101955585

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-買賣股票的最佳時機Ⅲ-困難

image-20240719111354187

暴力-超時程式碼

image-20240719111115358

假設第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

股票問題通用解法

image-20240719114453624

122-買賣股票的最佳時機Ⅱ-不限交易次數

給你一個整數陣列 prices ,其中 prices[i] 表示某支股票第 i 天的價格。

在每一天,你可以決定是否購買和/或出售股票。你在任何時候最多隻能持有一股股票。你也可以先購買,然後在 同一天 出售。

返回 你能獲得的 最大 利潤

image-20240719114746398

為了後面改為遞迴,從最後一天向前思考。

啟發思路:最後一天發生了什麼?

從第0天開始到第5天結束時的利潤 = 從第0天開始到第4天結束時的利潤 + 第5天的利潤

1.)如果第5天什麼也不做,利潤為0

2.)如果第5天買入股票,利潤就是-4

3.)如果第5天賣出股票,利潤就是+4

在第i-1天結束時,如果買入股票,那在第i天開始時的狀態就是持有股票

在第i-1天結束時,如果賣出股票,那在第i天開始時的狀態就是未持有股票

image-20240719115553569

如果什麼也不做,那麼狀態不變

這種表示狀態之間轉換關係的圖叫狀態機

從這張圖中可以看出,狀態轉移有這4中情況

定義 dfs(i, 0) 表示到第i天結束時,未持有股票的最大利潤

定義 dfs(i, 1) 表示到第i天結束時,持有股票的最大利潤

由於第i-1天的結束就是第i天的開始

dfs(i-1, •)也表示到第i天開始時的最大利潤

image-20240719115853787

\(\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

image-20240719122450252

# 擊敗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 天)

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

image-20240719123543185

思考,除了是否持有股票的狀態,現在新增了冷凍期的狀態

買入股票的時候,前一天不能有賣出的操作,那麼從再前一天轉移過來

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 次。

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

image-20240719131517922

有次數限制,應當在遞迴的過程中記錄次數

定義 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]

相關文章