leetcode刷題-- 5. 動態規划


動態規划思路

參考
狀態轉移方程:
明確「狀態」-> 定義dp數組/函數的含義 -> 明確「選擇」-> 明確 base case

試題

53最大子序和

題目描述

53
給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。

示例:

輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,為 6。

題解思路

思路一
我可以這么想pd[i]表示指針一直掃到i時目前存在的連續區間的最大和。那就不需要記錄每個以nums[i]結尾的連續區間最大和,即上面代碼注釋那里。我們直接取pd[i]不就時我們想要的結果了嗎。

指針指到i后的狀態:

  • 我們將nums[i]作為連續區間末尾
  • 我們不將nums[i]作為連續區間末尾

由此想到,改變的量不只是i還有是否將nums[i]作為末尾,所以還有一個變量或者我理解成選擇,於是pd[i]變成pd[i][0], pd[i][1],其中0代表不將nums[i]作為結尾,1代表將nums[i]作為結尾。

dp[i][0]含義: 到nums[i]為止,不以nums[i]為結尾,前面連續區間和的最大值。dp[i][1]類似。

狀態轉移方程

dp[i][0] = max(dp[i-1][0], dp[i-1][1])  # 不以nums[i]結尾,那最大的就是前面i-1兩種情況中一個

dp[i][1] = max(dp[i-1][1]+nums[i], nums[i]) #以i結尾

base case

dp[0][0] = -inf   # 這里不選0結尾,那最大和為什么不是0,是為了防止nums[0]<0,那么dp[1][0] = 0而不是nums[0]出錯
dp[0][1] = nums[0]

題解

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        dp = [[0]*2]*len(nums)
        dp[0][0] = float("-inf")
        dp[0][1] = nums[0]
        for i in range(1, len(nums)):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1])
            dp[i][1] = max(dp[i-1][1]+nums[i], nums[i])
        return max(dp[len(nums)-1][0], dp[len(nums)-1][1])

這里其實不需要dp[i][0],因為這道題里,最大情況只會存在於pd[i][1]里(因為假設最大情況以nums[i]結尾,那最大的就是Pd[i][1])。而pd[i][1]與pd[i][0]毫無關系,所以不需要計算pd[i][0],狀態轉移方程:pd[i] = max(pd[i-1]+nums[1], nums[i]),這里循環的同時,將nums掃過的部分看作pd[i],更新。

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        for i in range(1,len(nums)):
            nums[i] = max(nums[i-1]+nums[i], nums[i])
        return max(nums)

64最小路徑和

題目描述

64
給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小。

說明:每次只能向下或者向右移動一步。

示例:

輸入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
輸出: 7
解釋: 因為路徑 1→3→1→1→1 的總和最小。

題目思路

明確狀態dp的含義,dp[i][j]:從左上角走到(i,j)處,路勁數字和最小為dp[i][j]

兩種選擇,右或下,就造成dp[i][j]由下面兩種結果得到

狀態轉移方程:dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + nums[i][j]

base case: 這里的初始化比較復雜點,因為當i或j=0時,在狀態轉移方程里出現了 i-1=-1,j-1=-1這肯定是不可能的。所以要初始化dp[i][0]以及dp[0][j]

這里的初始化也好初始化,因為dp[i][0]只能是從dp[0][0]一路往下走,dp[0][j]同樣從dp[0][0]一路往右走。 帶pd數組的迭代法,自下而上,一般遞歸是自上而下。

題解

# 戰勝99.74 %的方法
class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        dp = [[0 for i in range(n)] for j in range(m)]

        dp[0][0] = grid[0][0]
        for i in range(1,m):
            dp[i][0] = dp[i-1][0] + grid[i][0]

        for i in range(1,n):
            dp[0][i] = dp[0][i-1] + grid[0][i]

        for i in range(1,m):
            for j in range(1,n):
                dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
        return dp[m-1][n-1]

這道題遇到的問題

dp = [[0 for i in range(n)] for j in range(m)] 這句話我一開始寫的是 dp = [[0]n]m,這樣是絕對不行的。因為這種 , 一旦改變一個值所有都變了。比如

a = [[0]*5]*5
a[0][0] = 1
print(a)

輸出:
[[1, 0, 0, 0, 0], 
[1, 0, 0, 0, 0], 
[1, 0, 0, 0, 0], 
[1, 0, 0, 0, 0], 
[1, 0, 0, 0, 0]]

所以在pytohn里初始化一個多維數組,最好用[[0] for i in range(n)]這種。

優化

由dp[i][j] = dp[i-1][j] + dp[i][j-1]可知,當我們計算第i行的值的時候是不需要1~i-2行的值,那么只需要一個一維數組保存上一行的信息即可。
dp[i] = dp[i] + dp[i-1],每次從左到右更新dp,這里的dp[i-1]就是i左邊那一格,等號后面的dp[i]還沒更新,是上一行第i列的值。

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        dp = [0 for i in range(n)] 

        dp[0] = grid[0][0]
        
        #初始化第0行的值
        for i in range(1,n):
            dp[i] = dp[i-1] + grid[0][i]

        for i in range(1,m):
            dp[0] = dp[0] + grid[i][0]
            for j in range(1,n):
                dp[j] = min(dp[j], dp[j-1]) + grid[i][j]
        return dp[n-1]

72. 編輯距離(hard)

題目描述

72
給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使用的最少操作數 。

你可以對一個單詞進行如下三種操作:

  1. 插入一個字符
  2. 刪除一個字符
  3. 替換一個字符

示例 1:

輸入: word1 = "horse", word2 = "ros"
輸出: 3
解釋: 
horse -> rorse (將 'h' 替換為 'r')
rorse -> rose (刪除 'r')
rose -> ros (刪除 'e')

示例 2:

輸入: word1 = "intention", word2 = "execution"
輸出: 5
解釋: 
intention -> inention (刪除 't')
inention -> enention (將 'i' 替換為 'e')
enention -> exention (將 'n' 替換為 'x')
exention -> exection (將 'n' 替換為 'c')
exection -> execution (插入 'u')

題目思路

狀態定義
每個階段狀態pd[i][j],定義為word1長度為i,word2長度為j,將word1變成word2需要的最小操作數。
注意,這里我們只專注於操作數, pd[i][j]可以理解成word1[:i]變成word2[:j]需要的操作數。

選擇
我們遍歷兩個單詞每個字符,如果word1[i]==wordr2[j],那么什么都不需要做。pd[i][j] = pd[i-1][j-1],i,j兩個字符就被排除了,只需要算剩下的字符。

如果不相等,有三種選擇:

  1. 插入,在word1 i處插入word2[j]。即 dp[i][j] = dp[i][j-1] + 1
  2. 刪除,刪除word1[i],即 dp[i][j] = 1 + dp[i-1][j]
  3. 替換, dp[i][j] = dp[i-1][j-1] + 1

綜上,只需要取dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1

Base case
當i或j為0時,有一個單詞長度為0,自然只能不斷刪除或者插入。需要兩個for循環來初始化pd[i][0]和pd[0][j]的值。

題解

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n, m = len(word1), len(word2)
        dp = [[0 for i in range(m+1)] for j in range(n+1)] #注意單詞長度從0~len(word1)所以要+1

        for i in range(1, n+1):
            dp[i][0] = dp[i-1][0] + 1
        for i in range(1, m+1):
            dp[0][i] = dp[0][i-1] + 1
        
        for i in range(1,n+1):
            for j in range(1,m+1):
                if word1[i-1] == word2[j-1]: # 這里也要注意下標要-1
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1
        
        return dp[n][m]

優化

同上題一樣,可以畫圖觀察dp[i][j]需要那些量,發現dp[i][j]只與dp[i-1][j] , dp[i-1][j-1] , dp[i][j-1]相關。與之前不同的是,這里多了個dp[i-1][j-1]需要額外的變量保存,不然我們更新了dp[i-1]再來計算dp[i]就得不到上一行的i-1處的值,然而這又是我們需要的,所以額外用一個變量保存即可。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n, m = len(word1), len(word2)
        dp = [j for j in range(m+1)] #注意單詞長度從0~len(word1)所以要+1
      
        for i in range(1,n+1):
            temp = dp[0]
            dp[0] = i
            for j in range(1,m+1):
                pre = temp
                temp = dp[j]

                if word1[i-1] == word2[j-1]: # 這里也要注意下標要-1
                    dp[j] = pre
                else:
                    dp[j] = min(dp[j-1], dp[j], pre) + 1

        return dp[m]

64. 最小路徑和

64
給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小。

說明:每次只能向下或者向右移動一步。

示例:

輸入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
輸出: 7
解釋: 因為路徑 1→3→1→1→1 的總和最小。

dp[i][j]:表示到(i,j)的最小路徑和

那么怎么才能到達(i,j)處呢,有兩種要么從(i-1,j)要么從(i,j-1)走來。所以:dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j]

優化

我們發現這個二維dp數組的值取決於,左邊的值和上面的值。dp[1][j]第一行的值也就取決於dp[0][j]dp[1][j-1],我們只需要維護一個一維數組即可。

從第0行開始,初始化一個dp[j],那么dp[j] = dp[j-1] + grid[j],因為第0行只能往右邊走。

第1行:dp[j] = min(dp[j], dp[j-1]) + grid[j],因為一開始dp[j]還沒有更新所以他保留的是上一行的值,相當於dp[i-1][j]。這里的dp[j-1]相當於dp[i][j-1]。因此,狀態轉移方程為:dp[j] = min(dp[j], dp[j-1]) + grid[j]

BaseCase: 我們需要初始化第一行。

題解

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        dp = [0 for i in range(n)] 

        dp[0] = grid[0][0]
        
        #初始化第0行的值
        for i in range(1,n):
            dp[i] = dp[i-1] + grid[0][i]

        for i in range(1,m):
            dp[0] = dp[0] + grid[i][0]
            for j in range(1,n):
                dp[j] = min(dp[j], dp[j-1]) + grid[i][j]
        return dp[n-1]

62. 不同路徑

一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。

問總共有多少條不同的路徑?

示例 1:

輸入: m = 3, n = 2
輸出: 3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例 2:

輸入: m = 7, n = 3
輸出: 28

思路同上題

題解

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0] for i in range(n)]
        
        for i in range(n):
            dp[i] = 1

        for i in range(1,m):
            for j in range(1,n):
                dp[j] = dp[j-1] + dp[j]
        
        return dp[n-1]

343. 整數拆分

343
給定一個正整數 n,將其拆分為至少兩個正整數的和,並使這些整數的乘積最大化。 返回你可以獲得的最大乘積。

示例 1:

輸入: 2
輸出: 1
解釋: 2 = 1 + 1, 1 × 1 = 1。
示例 2:

輸入: 10
輸出: 36
解釋: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

因為題目要求是,至少拆為2個,並使他們乘積最大。

狀態: 乘積。我們用dp[i]表示整數i拆分后的最大乘積。

選擇:怎么得到dp[i]的?假設i拆成,j和i-j兩個數。那么直觀上dp[i] = dp[j] * dp[i-j]。這里有一個錯誤,dp[j]不一定比j大,因為題目必須讓我們拆成至少兩個數再乘,如果dp[j]比j小,那還不如j就不拆了,dp[i] = j * dp[i-j]。例如 2拆成 1 * 1 = 1 < 2。所以dp[i]改進后:

狀態轉移方程dp[i] = max(dp[j], j) * max(dp[i-j], i-j)

class Solution:
    def integerBreak(self, n: int) -> int:
        num = n+1
        dp = [0 for i in range(num)]

        for i in range(2, num):
            temp = 1
            for j in range(1, i):
                temp = max(temp,max(dp[j], j) * max(dp[i-j],i-j))
            dp[i] = temp
            
        return dp[n]

這里我犯了一個錯誤,沒有temp,直接dp[i] = max(temp,max(dp[j], j) * max(dp[i-j],i-j)),所以沒有保存下最大值。

第 k 個數

leetcode

有些數的素因子只有 3,5,7,請設計一個算法找出第 k 個數。注意,不是必須有這些素因子,而是必須不包含其他的素因子。例如,前幾個數按順序應該是 1,3,5,7,9,15,21。

示例 1:

輸入: k = 5

輸出: 9

這里的數的因子只能是3,5,7的組合。

我們發現,從1開始下一個數就是min(1*3, 1*5, 1*7),在下一個就是min(3*3, 1*5, 1*7),為什么只考慮min(3*3, 1*5, 1*7),不考慮3*5, 3*7呢?

因為我們的數列是從小到大的,很明顯后面的數3乘以5和7肯定大於前面的數1乘以5和7。所以不用考慮3*5, 3*7

因此發現,用三個指針分別從左往右掃,三個指針分別代表要將指向的數乘以3或5或7,每次我們選擇當前最小值即可。

狀態轉移方程

pd[i] = min(dp[p1]3, dp[p2]5, dp[p3]*7),這里p1只負責將它指向的數乘以3,同理p2,p3分別代表乘以5和7。這三個指針都從左往右掃描數組。

題解

class Solution:
    def getKthMagicNumber(self, k: int) -> int:
        dp = [1]
        p1,p2,p3 = 0,0,0

        for i in range(k-1):
            Min = min(dp[p1]*3, dp[p2]*5, dp[p3]*7)
            dp.append(Min)
            if dp[p1]*3==Min: p1+=1
            if dp[p2]*5==Min: p2+=1
            if dp[p3]*7==Min: p3+=1    

        return dp[k-1]

32. 最長有效括號

32

題目描述

給定一個只包含 '(' 和 ')' 的字符串,找出最長的包含有效括號的子串的長度。

示例 1:

輸入: "(()"
輸出: 2
解釋: 最長有效括號子串為 "()"
示例 2:

輸入: ")()())"
輸出: 4
解釋: 最長有效括號子串為 "()()"

解題思路

狀態,好定義。dp[i]表示以第i個字符結尾的最長有效括號。結尾一般是')'結尾。

選擇,即怎么得到dp[i]。因為我們只考慮s[i]==')'的情況,所以:

當s[i]=')'時,若s[i-1]='(',則dp[i] = dp[i-2]+2
              若s[i-1]=')',若s[i-1-dp[i-1]]='(',則dp[i] = d[i-1] + 2 + dp[i-2-dp[i-1]] #將s[i-1-dp[i-1]]之前的字符也考慮在內,所以加上dp[i-2-dp[i-1]],稍微想想就明白

Base Case,都為0。這道題要注意判斷條件。

題解

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        # dp初始化為0
        # 當s[i]=')'時,若s[i-1]='(',則dp[i] = dp[i-2]+2
        #               若s[i-1]=')',若s[i-1-dp[i-1]]='(',則dp[i] = d[i-1] + 2 + dp[i-2-dp[i-1]]
        # 邊界條件=號判斷再想想

        dp = [0 for i in range(len(s))]
        for i in range(1,len(s)):
            if s[i]==')':
                if s[i-1]=='(':
                    # 這里不用判斷i-2>=0的原因是,因為只有i=1時,i-2才會小於0也就是-1,
                    # 然而dp[-1]=0的,所以不用管。之后i-2都大於或等於0
                    dp[i] = dp[i-2] + 2 
                # i-1-dp[i-1]就是指向了與 s[i]相對應的那個有效順括號當然要判斷它是否大於等於0以及他是否等於'(',
                # 如果不能同時滿足這兩個條件,那么dp[i]=0,也就值不變
                elif i-1-dp[i-1]>=0 and  s[i-1-dp[i-1]]=='(': 
                    # 這里dp[i-2-dp[i-1]]也就是之前的值了,也要判斷下是否大於或等於0,否則不加上他
                    dp[i] = dp[i-1] + 2 + dp[i-2-dp[i-1]] if i-2-dp[i-1]>=0 else dp[i-1] + 2 
        
        if dp:
            return max(dp)
        else:
            return 0

1143. 最長公共子序列

1143

題目

給定兩個字符串 text1 和 text2,返回這兩個字符串的最長公共子序列。

一個字符串的 子序列 是指這樣一個新的字符串:它是由原字符串在不改變字符的相對順序的情況下刪除某些字符(也可以不刪除任何字符)后組成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。兩個字符串的「公共子序列」是這兩個字符串所共同擁有的子序列。

若這兩個字符串沒有公共子序列,則返回 0。

示例 1:

輸入:text1 = "abcde", text2 = "ace" 
輸出:3  
解釋:最長公共子序列是 "ace",它的長度為 3。

狀態:dp[i][j],text1[:i]與text2[:j]最長公共子序列

選擇:text1[i] == text2[j]或者不等

轉移方程: 相等時dp[i][j] = dp[i-1][j-1]+1,不等時dp[i][j] = max(dp[i-1][j], dp[i][j-1])

同樣可以優化成一個以維dp數組解決,同上面的編輯距離優化問題:

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        # dp[i][j] :text1[:i] 與 text2[:j]最長公共子序列
        # 如果text1[i] == text2[j] , dp[i][j] = dp[i-1]dp[j-1] + 1
        # 其他 dp[i][j] = max(dp[i-1][j], dp[i][j-1])

        dp = [0 for i in range(len(text2)+1)]

        for i in range(1,len(text1)+1):
            temp = dp[0]
            for j in range(1,len(dp)):
                pre = temp
                temp = dp[j]
                if text1[i-1]==text2[j-1]:
                    dp[j] = pre + 1
                else:
                    dp[j] = max(dp[j], dp[j-1])   
        return dp[len(text2)]

注意這里保存dp[i-1][j-1]的操作,再第一層循環初始化temp = dp[0], 第二層pre = temp; temp = dp[j]這樣就可以下次循環仍能取到上次的dp[j-1]的值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM