LeetCode之動態規劃專題

5.最長迴文子串

  • 暴力法,用雙重for循環列舉所有子串(On2),判斷每個子串是否是迴文(On)
  • 中心擴展法,以每個字母爲中心,向外擴展。
  • 動態規劃。dp[i][j]表示從i到j這個子串是不是迴文(bool)。

dp[i][j] = s[i]==s[j] && dp[i+1][j-1]

class Solution:
    def longestPalindrome(self, s: str) -> str:
        dp=[]
        for _ in range(len(s)):
            dp.append([0]*len(s))
        res=''
        for i in range(len(s)-1,-1,-1):
            for j in range(i, len(s)):
                dp[i][j]=(s[j]==s[i])and(j-i<2 or dp[i+1][j-1]) # j-i<2代表相鄰或重疊
                if dp[i][j] and j-i+1>len(res):
                    res=s[i:j+1]
        return res

62. 不同路徑

同劍指offer中最大禮物價值。二維數組dp[i][j]記錄從左上角到達(i,j)有多少路徑。

  • 動態規劃,從左至右,從上到下依次加。
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp=[]
        for _ in range(m):
            dp.append([0]*n)
        dp[0][0]=1
        for i in range(m):
            for j in range(n):
                if i==0 or j==0:
                    dp[i][j]=1
                    continue
                dp[i][j]=dp[i-1][j]+dp[i][j-1]
        return dp[m-1][n-1]

還可以用一位數組優化。

63. 不同路徑II

在62題的基礎上,增加了障礙物,遇到障礙物,則dp[i][j]爲0。預先處理第一行和第一列的數據。

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m,n=len(obstacleGrid), len(obstacleGrid[0])
        dp=[]
        for _ in range(m):
            dp.append([0]*n)
        for i in range(m):
            for j in range(n):
                if i==0:
                    if j==0:
                        if obstacleGrid[i][j]:
                            return 0
                        dp[0][0]=1
                        continue
                    else:
                        if obstacleGrid[i][j]:
                            dp[i][j]=0
                        else:
                            dp[i][j]=dp[i][j-1]
                        continue
                if j==0:
                    if obstacleGrid[i][j]:
                        dp[i][j]=0
                    else: 
                        dp[i][j]=dp[i-1][j]
                    continue
                if obstacleGrid[i][j]:
                    dp[i][j]=0
                else:
                    dp[i][j]=dp[i][j-1]+dp[i-1][j]
        return dp[m-1][n-1]      

64.最小路徑

同最大禮物問題

72. 編輯距離

對“dp[i-1][j-1] 表示替換操作,dp[i-1][j] 表示刪除操作,dp[i][j-1] 表示插入操作。”的補充理解:
以 word1 爲 “horse”,word2 爲 “ros”,且 dp[5][3] 爲例,即要將 word1的前 5 個字符轉換爲 word2的前 3 個字符,也就是將 horse 轉換爲 ros,因此有:
(1) dp[i-1][j-1],即先將 word1 的前 4 個字符 hors 轉換爲 word2 的前 2 個字符 ro,然後將第五個字符 word1[4](因爲下標基數以 0 開始) 由 e 替換爲 s(即替換爲 word2 的第三個字符,word2[2])
(2) dp[i][j-1],即先將 word1 的前 5 個字符 horse 轉換爲 word2 的前 2 個字符 ro,然後在末尾補充一個 s,即插入操作
(3) dp[i-1][j],即先將 word1 的前 4 個字符 hors 轉換爲 word2 的前 3 個字符 ros,然後刪除 word1 的第 5 個字符

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m,n=len(word1),len(word2)
        if m*n == 0:
            return m+n
        dp=[[0]*(n+1) for _ in range(m+1)]
        for i in range(m+1):
            dp[i][0]=i
        for j in range(n+1):
            dp[0][j]=j
        for i in range(m):
            for j in range(n):
                if word1[i]== word2[j]:
                    dp[i+1][j+1]=dp[i][j]
                else:
                    dp[i+1][j+1]=min(
                       1+dp[i+1][j], # 添加
                       1+dp[i][j+1], # 刪除
                       1+dp[i][j] # 替換
                    )
        return dp[-1][-1]  

264. 醜數

  • 三指針法,前面醜數的2,3,5倍中,且超過最後一個醜數,選擇最小的作爲新醜數。
class Solution:
    def nthUglyNumber(self, n: int) -> int:
        if n <=0:
            return None
        index = 1
        n2 = n3 = n5 = 0
        res = [1]
        while index < n:
            start = min(2*res[n2], 3*res[n3], 5*res[n5])
            res.append(start)
            while 2 * res[n2] <= start:
                n2 += 1
            while 3 * res[n3] <= start:
                n3 += 1
            while 5 * res[n5] <= start:
                n5 += 1
            index += 1
        return res[-1]

279. 完全平方數

dp[i]爲將i拆分爲完全平方數字所需要的最小數目。

class Solution:
    def numSquares(self, n: int) -> int:
        if n <=0:
            return 0
        dp = [0]*(n+1)
        for i in range(1, n+1):
            j = 1
            dp[i]=i
            while (i - j**2) >= 0:
                dp[i] = min(dp[i], dp[i-j**2]+1)
                j += 1
        return dp[-1]

300. 最長上升子序列

dp[i] 爲從0-i範圍內,以i爲終點的最大上升子序列。因爲最大上升子序列的終點未必是在n-1的位置上,所以返回的是max(dp)。

dp[i] = max(dp[i-k]+1) if nums[i] > nums[i-k] else 1, k in {1, i-1}

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        dp = [1] * (len(nums)+1)
        dp[0] = 0
        for i in range(1, len(nums)+1):
            temp = 1
            for j in range(1, i):
                if nums[i-1] > nums[i-1-j]:
                    temp=max(temp, dp[i-j]+1)
            dp[i] = temp
        return max(dp)

304. 二維區域和檢索

  • dp[i][j] = 前面和上面的值相加,加上當期位置的數字,再減去重複的部分dp[i-1][j-1]
class NumMatrix:

    def __init__(self, matrix: List[List[int]]):
        self.matrix = matrix
        self.m = len(matrix)
        self.flag=True
        if not matrix:
            self.flag=False
            return
        self.n = len(matrix[0])
        self.dp = [[0]*(self.n+1) for _ in range(self.m+1)]
        for i in range(1,self.m+1):
            for j in range(1, self.n+1):
                self.dp[i][j] = self.matrix[i-1][j-1]+self.dp[i-1][j] + self.dp[i][j-1] - self.dp[i-1][j-1]

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        if not self.flag:
            return 0
        return self.dp[row2+1][col2+1] - self.dp[row1][col2+1] - self.dp[row2+1][col1] + self.dp[row1][col1]

96. 不同的二叉搜索樹

  • G(n): 是長度爲n的序列的不同二叉搜索樹個數;
  • F(i,n):以i爲根的二叉搜索樹個數。總共有n個節點
  • G(n)=iF(i,n),i1,...,nG(n) = \sum_i F(i,n), i \in {1, ..., n}
  • 而F(i,n)怎麼定義? 當以i爲根,i左邊的全部是左子樹部分,i右邊的全部是右子樹部分。F(i,n) = G(i-1)*G(n-i). (左子樹的個數 * 右子樹的個數)
class Solution:
    def numTrees(self, n: int) -> int:      
        G = [0]*(n+1)
        G[0], G[1] = 1, 1
        for i in range(2, n+1):
            for j in range(1, i+1):
                G[i] += G[j-1] * G[i-j]
        return G[n]

95 不用的二叉搜索樹II

這題是基於第96題。96題只需要求出有多少不同組合的二叉樹數目。但這道題需要我們生成這些二叉樹,就是需要記錄下來。
其實這道題,我感覺不算是動態規劃的題目了,因爲代碼的形式更加向全排列或者說回溯法。

  • 1到n都能是根節點,所以放到for循環中,當i爲根節點,i左邊是左子樹,i右邊是右子樹。
  • 對於左子樹的根節點,可以在1至i-1中選擇,這又是一個for循環,右子樹同理。因此,for循環內部是for循環,整個代碼形式是for之中用遞歸。分別對左邊和右邊遞歸,得到做子樹的根和右子樹的根。可以看成是由底至頂,先搭建最底下的。
class Solution:
    def generateTrees(self, n: int) -> List[TreeNode]:
        def core(start, end):
            res = []
            if start > end:
                return [None,]
            for i in range(start, end +1):
                left = core(start, i-1)
                right = core(i+1, end)
                for l in left:
                    for r in right:
                        node=TreeNode(i)
                        node.left=l
                        node.right=r
                        res.append(node)
            return res
        return core(1, n) if n else []

91. 解碼方法

典型的動態規劃問題,A-Z對應了1-26,輸入數字字符串,求出解碼方法的最大數目。遞歸方程是 f[n] = f[n-1] + g* f[n-2], 如果s[n-1:n+1]在10-26之間,g爲1.

class Solution:
    def numDecodings(self, s: str) -> int:
        size = len(s)
        #特判
        if size == 0:
            return 0
        dp = [0]*(size+1)
        dp[0] = 1 
        for i in range(1,size+1):
            t = int(s[i-1])
            if t>=1 and t<=9:
                dp[i] += dp[i-1] #最後一個數字解密成一個字母
            if i >=2:#下面這種情況至少要有兩個字符
                t = int(s[i-2])*10 + int(s[i-1])
                if t>=10 and t<=26:
                    dp[i] += dp[i-2]#最後兩個數字解密成一個一個字母
        return dp[-1]

120. 三角形最小路徑和

下面的代碼沒有采用空間優化。dp是和輸入大小一樣的空間。實際上,dp可以只用一個一維的大小爲n的數組。

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        dp=[]
        height=len(triangle)
        for h in range(height):
            dp.append([0]*(h+1))
        dp[0][0]=triangle[0][0]
        for i in range(1,height):
            for j in range(i+1):
                if j==0: # 處理最左邊,只有一個選擇
                    dp[i][j]=dp[i-1][0]+triangle[i][j]
                    continue
                if j== i: # 處理最右邊, 也只有一個選擇
                    dp[i][j]=dp[i-1][-1]+triangle[i][j]
                    continue
                dp[i][j]=min(dp[i-1][j-1]+triangle[i][j],
                dp[i-1][j]+triangle[i][j])
            
        return min(dp[-1])
                

139. 單詞拆分

我採用的是回溯法,即暴力法,並且用mask剪枝。
思路是:dic記錄下有什麼單詞需要匹配。start指針指向單詞開頭位置,end指向單詞尾部位置。滑動end直到匹配到某個字符,如果匹配不到,start這個位置會被mask記錄爲失敗匹配,實現剪枝。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dic={}
        mask=[1]*(len(s)+1)
        for l in wordDict:
            dic[l]=0
        return self.unfold(s,dic,0,mask)


    def unfold(self,s,c,start,mask):
        if mask[start] == 0:
            return False
        if start==len(s):
            return True
        for end in range(start, len(s)):
            word=s[start:end+1]
            if word in c:
                c[word]+=1
                if self.unfold(s,c,end+1,mask):
                    return True
                c[word]-=1
        mask[start]=0
        return False

152. 乘法最大子數組

求一個數組內,最大的連續子數組的乘積
因爲涉及到負數,其實這道題還是不好處理。所以需要兩個數組作爲dp,一個記錄爲i爲結尾的連續數組乘積的最小值,另一個記錄以i爲結尾的乘積最大值。

其實這兩個數組可以合併爲dp[i][k],k爲0和1。dp[i][1]的含義是:爲i爲結尾的連續數組的乘積的最小值。
還可以優化空間,只用兩個變量就行了。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if len(nums)<=1:
            return nums[0]
        res=nums[0]
        minv=[0]*len(nums)
        minv[0]=res # 初始化
        maxv=[0]*len(nums)
        maxv[0]=res # 初始化第1個位置
        for i in range(1,len(nums)):
            temp1=nums[i]*maxv[i-1]
            temp2=nums[i]*minv[i-1]
            minv[i]=min(temp2,temp1,nums[i])
            maxv[i]=max(temp1,temp2,nums[i])
            res=max(res,maxv[i])
        return res

377.組合總數IV

這道題和零錢兌換有點像,只不過零錢兌換要求的是,能兌換出target的最少硬幣數目。這道題則要求能兌換出零錢的最大組合數目。
核心就是更改遞歸方程:
f(n)=jf(nNum[j])f(n) = \sum_j f(n-Num[j])

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target+1)
        nums.sort()
        dp[0] = 1 # 當i == nums[j]時, 有一個直接用現有硬幣兌換的組合
        for i in range(1, target+1 ):
            temp = 0
            for j in range(len(nums)):
                if i - nums[j] < 0:  # 邊界條件
                    continue
                temp += dp[i - nums[j]]
            dp[i] = temp
        return dp[-1]
                

416.分割等和子集

先貼一段暴力回溯法,當然超時了
在這裏插入圖片描述

動態規劃的思路是:dp[i][j]代表前i個數字中有沒有和爲j的組合。
遞歸方程爲:dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]
當不選擇nums[i]時,爲dp[i-1][j] 當選擇nums[i],說明從第0到第i-1的數字中有和爲j-nums[i]

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章