天天看點

動态規劃1. 斐波那契數列2. 爬樓梯3. 不同路徑4. 不同路徑 II5. 最小路徑和6. 最長遞增子序列7. 最長公共子序列8. 最長回文子序列9. 最長回文子串

1. 斐波那契數列

509. 斐波那契數

斐波那契數 (通常用 F(n) 表示)形成的序列稱為 斐波那契數列 。該數列由 0 和 1 開始,後面的每一項數字都是前面兩項數字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
給定 n ,請計算 F(n) 。

示例 1:

輸入:n = 2
輸出:1
解釋:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:

輸入:n = 3
輸出:2
解釋:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:

輸入:n = 4
輸出:3
解釋:F(4) = F(3) + F(2) = 2 + 1 = 3
 
提示:

0 <= n <= 30
           

題解:

class Solution:
    def fib(self, n: int) -> int:
        if n == 0:
            return 0

        if n == 1:
            return 1

        dp = [0] * (n + 1)  # 定義 dp 數組
        dp[0] = 0   # 初始化
        dp[1] = 1

        for i in range(2, n+1):
            dp[i] = dp[i - 1] + dp[i - 2]   # 遞推公式

        return dp[n]
           

2. 爬樓梯

70. 爬樓梯

假設你正在爬樓梯。需要 n 階你才能到達樓頂。

每次你可以爬 1 或 2 個台階。你有多少種不同的方法可以爬到樓頂呢?

示例 1:

輸入:n = 2
輸出:2
解釋:有兩種方法可以爬到樓頂。
1. 1 階 + 1 階
2. 2 階
示例 2:

輸入:n = 3
輸出:3
解釋:有三種方法可以爬到樓頂。
1. 1 階 + 1 階 + 1 階
2. 1 階 + 2 階
3. 2 階 + 1 階
 

提示:

1 <= n <= 45
           

爬樓梯實質上也可以看作一個斐波拉契數列:

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

        return dp[n]
           

類似題目 劍指 Offer 10- II. 青蛙跳台階問題:

class Solution:
    def numWays(self, n: int) -> int:
        if n == 0 or n == 1:
            return 1

        dp = [0] * (n + 1) 
        dp[0] = 1   # 0 個台階有 1 種 方法
        dp[1] = 1   # 1 個台階有 1 種方法
    

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

        return dp[n] % 1000000007
           

3. 不同路徑

62. 不同路徑

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

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

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

示例 1:

輸入:m = 3, n = 7
輸出:28
示例 2:

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

輸入:m = 7, n = 3
輸出:28
示例 4:

輸入:m = 3, n = 3
輸出:6
 
提示:

1 <= m, n <= 100
題目資料保證答案小于等于 2 * 109
           

題解一:

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

        def diff_path(row, col):

            # 第一列的任意單元格,隻有來自它上一個單元格過來的方法
            for i in range(n):
                dp[0][i] = 1

            # 第一行的任意單元格,隻有來自它前一個單元格過來的一種方法
            for j in range(m):
                dp[j][0] = 1

            # 随意一個單元格有來自上或者左的兩種路徑,第一行、第一列已填充,不用繼續填充
            for i in range(1, row):
                for j in range(1, col):
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

                # 傳回最後一個單元格的位置
            return dp[row - 1][col - 1]

        return diff_path(m, n)
           

題解二:(更容易了解)

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

        for row in range(m):
            for col in range(n):
                # 第一個格子隻有一種方法
                if row == 0 and col == 0:
                    dp[row][col] = 1
                elif col == 0:
                    # 第一行的任意單元格,隻有來自它前一個單元格過來的一種方法
                    dp[row][col] = dp[row-1][col]    
                elif row == 0:  
                    # 第一列的任意單元格,隻有來自它上一個單元格過來的方法
                    dp[row][col] = dp[row][col - 1]     
                else:
                    # 其他情況(中間):任意一個單元格有來自其左或上兩個方向的機器人
                    dp[row][col] = dp[row - 1][col] + dp[row][col - 1]  


        return dp[m - 1][n - 1]
           

4. 不同路徑 II

63. 不同路徑 II

示例 1:

輸入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
輸出:2
解釋:3x3 網格的正中間有一個障礙物。
從左上角到右下角一共有 2 條不同的路徑:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右


示例 2:

輸入:obstacleGrid = [[0,1],[0,0]]
輸出:1
 
提示:

m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 為 0 或 1
           

題解:

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        row = len(obstacleGrid)
        col = len(obstacleGrid[0])

        # 隻有一個單元格,即一行一列時
        if row == 1 and col == 1:
            if obstacleGrid[0][0] == 1:
                return 0
            else:
                return 1

        dp = [[0] * col for i in range(row)]

        for i in range(row):
            for j in range(col):
                # 遇到阻礙,就跳過目前循環
                if obstacleGrid[i][j] == 1:
                    continue

                if i == 0 and j == 0:
                    dp[i][j] = 1
                elif i == 0:
                    dp[i][j] = dp[i][j - 1]
                elif j == 0:
                    dp[i][j] = dp[i - 1][j]
                else:
                    dp[i][j] = dp[i][j - 1] + dp[i - 1][j]

        return dp[row - 1][col - 1]
           

5. 最小路徑和

64. 最小路徑和

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

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

示例 1:

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

輸入:grid = [[1,2,3],[4,5,6]]
輸出:12
 
提示:

m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 100
           

題解:

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

        # 隻有一個單元格時
        if m == 1 and n == 1:
            return grid[0][0]

        dp = [[0] * n for i in range(m)]

        for i in range(m):
            for j in range(n):
                
                if i == 0:
                    # 第一行,和 = 目前單元格數字 + 前一個單元格數字
                    dp[i][j] = dp[i][j - 1] + grid[i][j]
                elif j == 0:
                    # 第一列,和 = 目前單元格數字 + 上一個單元格數字
                    dp[i][j] = dp[i - 1][j] + grid[i][j]
                else:
                    # 中間單元格,和 = 目前單元格數字 + 上一個和前一個單元格中最小的數字
                    dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]

        return dp[m-1][n-1]
           
注意:

dp[i][j - 1]、dp[i - 1][j]

代表的是狀态轉移,它包括前面走過的路徑之和,而

grid[i][j]

代表的僅僅隻是目前單元格一個數字,是以是:

dp[i][j] = dp[i][j - 1] + grid[i][j]

,而不是

dp[i][j] = grid[i][j - 1] + grid[i][j]

6. 最長遞增子序列

300. 最長遞增子序列

給你一個整數數組 nums ,找到其中最長嚴格遞增子序列的長度。

子序列 是由數組派生而來的序列,删除(或不删除)數組中的元素而不改變其餘元素的順序。例如,[3,6,2,7] 是數組 [0,3,1,6,2,2,7] 的子序列。

示例 1:

輸入:nums = [10,9,2,5,3,7,101,18]
輸出:4
解釋:最長遞增子序列是 [2,3,7,101],是以長度為 4 。
示例 2:

輸入:nums = [0,1,0,3,2,3]
輸出:4
示例 3:

輸入:nums = [7,7,7,7,7,7,7]
輸出:1
 
提示:

1 <= nums.length <= 2500
-104 <= nums[i] <= 104
           
動态規劃1. 斐波那契數列2. 爬樓梯3. 不同路徑4. 不同路徑 II5. 最小路徑和6. 最長遞增子序列7. 最長公共子序列8. 最長回文子序列9. 最長回文子串

題解:

dp[i]

表示目前元素的遞增子集個數

  • 元素 1: 因為沒有比它小的元素,是以遞增子集數為 1
  • 元素 7:遞增子集有:

    1、 17

    1 + dp[0]

  • 元素 2:遞增子集有:因為

    7

    2

    大,是以隻有

    1、 12

    ,即

    1 + dp[0]

  • 元素 5:遞增子集有:

    1 、 15、 125

    ,即

    1 + dp[2]

  • 如此類推就能推出

    dp[i]

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 1:
            return 1

        dp = [1] * n

        res = 1
        for i in range(n):
            for j in range(i):
                # 目前元素與它之前所有的元素進行比較,若之前元素比目前元素大,則跳過不考慮,隻需要考慮比它小的即可
                if nums[i] > nums[j]:
                    dp[i] = max(dp[j] + 1, dp[i])

            res = max(res, dp[i])

        return res
           
參考:【算法太難了】【23】最長遞增子序列-動态規劃

7. 最長公共子序列

1143. 最長公共子序列

給定兩個字元串 text1 和 text2,傳回這兩個字元串的最長 公共子序列 的長度。如果不存在 公共子序列 ,傳回 0 。

一個字元串的 子序列 是指這樣一個新的字元串:它是由原字元串在不改變字元的相對順序的情況下删除某些字元(也可以不删除任何字元)後組成的新字元串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
兩個字元串的 公共子序列 是這兩個字元串所共同擁有的子序列。

示例 1:

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

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

輸入:text1 = "abc", text2 = "def"
輸出:0
解釋:兩個字元串沒有公共子序列,傳回 0 。
           

題解:

對于兩個字元串求子序列的問題,都是用兩個指針

i

j

分别在兩個字元串上移動,大機率是動态規劃思路

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        # dp 函數的定義:dp(s1, i, s2, j) 計算 s1[i..]和s2[j..]的最長公共子序列長度
        dp = [[-1 for _ in text2] for _ in text1]

        def ways(s1, i, s2, j):
            # bad case,此時 s1 或 s2 相當于空串了
            if len(s1) == i or len(s2) == j:
                return 0
            
            # 備忘錄,避免重複計算
            if dp[i][j] != -1:
                return dp[i][j]
			
            # 比較兩個字元串是否相等,若相等一定這個字元串一定在 lcs 中
            if s1[i] == s2[j]:
                dp[i][j] = 1 + ways(s1, i+1, s2, j+1)
            else:
                # 不相等,則 s1[i] 、s2[j] 至少有一個不在 lcs 中
                dp[i][j] = max(
                    ways(s1, i+1, s2, j),	# s1[i] 不在 lcs 中
                    ways(s1, i, s2, j+1),	# s2[j] 不在 lcs 中
                    ways(s1, i+1, s2, j+1)	# 都不在 lcs 中(這一步可省略,因為前面兩步已經包含了)
                )

            return dp[i][j]

        return ways(text1, 0, text2, 0)
           

8. 最長回文子序列

516. 最長回文子序列

給你一個字元串 s ,找出其中最長的回文子序列,并傳回該序列的長度。

子序列定義為:不改變剩餘字元順序的情況下,删除某些字元或者不删除任何字元形成的一個序列。

 
示例 1:

輸入:s = "bbbab"
輸出:4
解釋:一個可能的最長回文子序列為 "bbbb" 。
示例 2:

輸入:s = "cbbd"
輸出:2
解釋:一個可能的最長回文子序列為 "bb" 。
 

提示:

1 <= s.length <= 1000
s 僅由小寫英文字母組成
           

題解:

dp[i][j

表示字元串 s 下标範圍

[i, j]

内最長回文子序列的長度,假設字元串 s 的長度為 n,則隻有當

0<=i<j<n

時,才會有

dp[i][j] > 0

,否則有

dp[i][j]=0

class Solution: 
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        dp = [[0] * n for _ in range(n)]

        # 倒序遞歸
        for i in range(n-1, -1, -1):
            # bad case,若隻有一個字元,最長回文子序列長度是 1,dp[i][j] = 1 (i == j)
            dp[i][i] = 1	
            for j in range(i+1, n):
                # 它倆一定在最長回文子序列中
                if s[i] == s[j]:
                    dp[i][j] = dp[i+1][j-1] + 2
                else:
                    # s[i+1..j] 和 s[i..j-1] 誰的回文子序列更長
                    dp[i][j] = max(dp[i+1][j], dp[i][j-1])
        
        return dp[0][n-1]
           
參考:子序列問題通用思路|最長回文子序列

9. 最長回文子串

5. 最長回文子串

給你一個字元串 s,找到 s 中最長的回文子串。

示例 1:

輸入:s = "babad"
輸出:"bab"
解釋:"aba" 同樣是符合題意的答案。
示例 2:

輸入:s = "cbbd"
輸出:"bb"
 
提示:

1 <= s.length <= 1000
s 僅由數字和英文字母組成
           

題解:

class Solution:
    def longestPalindrome(self, s: str) -> str:
        long_sub = ""

        for i in range(len(s)):
            odd = self.is_palindrom(s, i, i)    # 回文串元素個數為奇數
            even = self.is_palindrom(s, i, i + 1)   # 回文串元素個數為偶數
            long_sub = odd if len(odd) > len(long_sub) else long_sub
            long_sub = even if len(even) > len(long_sub) else long_sub

        return long_sub

    def is_palindrom(self, s, l, r):
        """不超出邊界且擴散的兩邊的元素相等,傳回回文子串"""
        while l >= 0 and r < len(s) and s[l] == s[r]:
            l -= 1
            r += 1

        return s[l + 1: r]
           
s = 'babad'  n = len(s) = 5
i = 0  
	奇數:l = 0, r = 0 ==> s[l] == s[r] ==> l = -1, r = 1  ===> s[0: 1] ==> long_sub = "b"
    偶數: l = 0, r = 1 ==> s[l] != s[r] ===> s[0: 1] ==> long_sub = "b"
        
i = 1
	奇數:l = 1, r = 1 ==> s[l] == s[r] ==> l = 0, r = 2  ===> l = -1, r = 3  ===> s[0: 3] ==> long_sub = "bab"
    偶數: l = 1, r = 2 ==> s[l] != s[r] ===> s[2: 2] ==> long_sub = "bab"
        
....
           

繼續閱讀