日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

爬楼梯与路径类题目记忆化递归与动态规划双解法(Leetcode题解-Python语言)

發布時間:2023/12/4 python 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 爬楼梯与路径类题目记忆化递归与动态规划双解法(Leetcode题解-Python语言) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

70. 爬樓梯(劍指 Offer 10- II. 青蛙跳臺階問題)

遞歸(英語:Recursion),是指在函數的定義中使用函數自身的方法。有意義的遞歸通常會把問題分解成規模縮小的同類子問題,當子問題縮寫到尋常的時候,我們可以直接知道它的解。然后通過建立遞歸函數之間的聯系(轉移)即可解決原問題。

記憶化遞歸,就是用數組或者哈希表記錄下遞歸過程中的計算結果,避免重復計算。

以爬樓梯為例,我們想知道爬 n 階樓梯的方案數 f(n),由于每次只能爬 1 階或 2 階樓梯,所以其實如果知道爬 n - 1 階和 n - 2 階樓梯的方案數 f(n-1) 和 f(n-2),就能知道爬 n 階樓梯的方案數 f(n) = f(n-1) + f(n-2)。 式子中最小為 n-2 ,根據題意 n-2 >= 0(也可以嚴格大于0,區別不大,后面相應修改) ,那么 n >= 2。意味著最后一次遞歸調用為 f(2) = f(1) + f(0),邊界就是 f(1) = 1f(0) = 1

直接遞歸的代碼如下:

class Solution:def climbStairs(self, n: int) -> int:if n <= 1:return 1return self.climbStairs(n - 1) + self.climbStairs(n - 2)

是會超時的,利用記憶化遞歸可以減少許多重復運算,順利通過:

class Solution:memo = dict()def climbStairs(self, n: int) -> int:if n <= 1:return 1if n in self.memo:return self.memo[n]self.memo[n] = self.climbStairs(n-1) + self.climbStairs(n-2)return self.memo[n]

思路不難,只是用一個字典 memo 記錄出現過的臺階與對應的方案數,如果有記錄的話就不用往下遞歸了,直接返回結果即可。劍指 Offer 的題目區別只在于結果要對 1000000007 取余數。

509. 斐波那契數(劍指 Offer 10- I. 斐波那契數列)

class Solution:memo = dict()def fib(self, n: int) -> int:if n <= 1:return nif n in self.memo:return self.memo[n]self.memo[n] = self.fib(n-1) + self.fib(n-2)return self.memo[n]

求斐波那契數,除了邊界,其余代碼都是一樣的。

1137. 第 N 個泰波那契數

class Solution:memo = dict()def tribonacci(self, n: int) -> int:if n == 0:return 0if n == 1 or n == 2:return 1if n in self.memo:return self.memo[n]self.memo[n] = self.tribonacci(n-1) + self.tribonacci(n-2) + self.tribonacci(n-3)return self.memo[n]

這題求的是泰波那契數,思路基本一樣,只是遞歸公式中最小的是 n-3,所以 n >= 3,最后一次遞歸是 n =3,若知道 n = 0, 1, 2 的值即可得到 n = 3 的結果,所以遞歸邊界可知。(題目其實給了)

746. 使用最小花費爬樓梯

class Solution:memo = dict()def minCostClimbingStairs(self, cost: List[int]) -> int:if len(cost) == 1:return 0if len(cost) == 2:return min(cost)if tuple(cost) in self.memo:return self.memo[tuple(cost)]self.memo[tuple(cost)] = min(cost[0] + self.minCostClimbingStairs(cost[1:]), cost[1] + self.minCostClimbingStairs(cost[2:]))return self.memo[tuple(cost)]

這題注意開始爬樓梯時,爬一個臺階是到 cost[0],兩個臺階是到 cost[1],而不是 cost[0] 為起點。想要繼續使用字典只能用可哈希對象元組,不能用數組。

說完記憶化遞歸,我們說說動態規劃。動態規劃(英語:Dynamic programming,簡稱DP)是通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。若要解一個給定問題,我們需要解其不同部分(即子問題),再根據子問題的解以得出原問題的解。通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。

我們應該留意到,動態規劃的核心思路也是通過記憶化避免重復運算,實際上,動態規劃中的 dp 數組(狀態數組)對應的就是記憶化遞歸中的 memo 字典。關于動態規劃的簡單入門,我推薦這篇知乎文章。

總結來說就是三點:定義 dp 數組元素的含義(狀態是什么)、找出 dp 數組元素間的關系式(遞推公式或狀態轉移方程)、找出初始條件。用上面的例題進行說明:

70. 爬樓梯(劍指 Offer 10- II. 青蛙跳臺階問題)

dp 數組元素 dp[i] 的含義為:爬 i 階樓梯的方案數。
數組元素間的關系,由最開始的分析可知,dp[i] = dp[i-1] + dp[i-2]。
初始條件為 dp[1] = 1, dp[2] = 2,注意我這里不討論 dp[0] 的初始化,是因為題目說了 n 不可能為 0!所以我的遍歷也是從 3 開始的,代碼如下:

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

509. 斐波那契數(劍指 Offer 10- I. 斐波那契數列)

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

dp[i] 的含義:i 對應的斐波那契數

遞推公式:dp[i] = dp[i-1] + dp[i-2]

初始條件: dp[0] = 0, dp[1] = 1

觀察到實際上每次都只改變了三個位置,所以優化寫法如下:

class Solution:def fib(self, n: int) -> int:if n == 0 or n == 1:return nf1 = 0f2 = 1f3 = 0for i in range(1, n):f3 = f1 + f2f1, f2 = f2, f3return f3

1137. 第 N 個泰波那契數

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

dp[i] 的含義:i 對應的泰波那契數

遞推公式:dp[i] = dp[i-3] + dp[i-2] + dp[i-1]

初始條件: dp[0] = 0, dp[1] = 1, dp[2] = 1

優化寫法:

class Solution:def tribonacci(self, n: int) -> int:if n <= 1:return nif n == 2:return 1f1 = 0f2 = 1f3 = 1f4 = 2for i in range(3, n+1):f4 = f1 + f2 + f3f1, f2, f3 = f2, f3, f4return f4

746. 使用最小花費爬樓梯

class Solution:def minCostClimbingStairs(self, cost: List[int]) -> int:n = len(cost)dp = [0] * (n + 1)dp[0] = cost[0]dp[1] = cost[1]for i in range(2, n):dp[i] = cost[i] + min(dp[i-1], dp[i-2])dp[n] = min(dp[n-1], dp[n-2])return dp[n]

dp[i] 的含義:到達第 i 級臺階時最小的總花費(第一步有花費,最后一步沒花費)

遞推公式:dp[i] = cost[i] + min(dp[i-1], dp[i-2])

初始條件: dp[0] = cost[0], dp[1] = cost[1]

由于最后一步沒花費,所以最后一個元素需要單獨求。優化寫法:

class Solution:def minCostClimbingStairs(self, cost: List[int]) -> int:n = len(cost)f1 = cost[0]f2 = cost[1]f3 = 0for i in range(2, n):f3 = cost[i] + min(f1, f2)f1, f2 = f2, f3f3 = min(f1, f2)return f3

62. 不同路徑(劍指 Offer II 098. 路徑的數目)

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

dp[i][j] 的含義:表示從 (0, 0) 出發,到 (i, j) 有 dp[i][j] 條不同的路徑

遞推公式:dp[i][j] = dp[i-1][j] + dp[i][j-1] (只能從上或左到達當前位置)

初始條件: dp[i][0]一定都是1,因為從 (0, 0) 的位置到 (i, 0) 的路徑只有一條,dp[0][j]也同理,方便起見,就將整個 dp 二維數組都初始化為 1

for 循環是 m 和 n,因為要返回的就是 dp[m-1][n-1],符合定義。

63. 不同路徑 II

class Solution:def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:m = len(obstacleGrid)n = len(obstacleGrid[0])dp = [[0 for _ in range(n)] for _ in range(m)]for i in range(m):if obstacleGrid[i][0] == 1:breakelse:dp[i][0] = 1for j in range(n):if obstacleGrid[0][j] == 1:breakelse:dp[0][j] = 1#print(dp)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]return dp[m-1][n-1]

dp[i][j] 的含義:表示從 (0, 0) 出發,到 (i, j) 有 dp[i][j] 條不同的路徑

遞推公式:dp[i][j] = dp[i-1][j] + dp[i][j-1] (前提是當前位置不是障礙)

初始條件: 先將整個 dp 二維數組都初始化為 0,然后 dp[i][0] (第一列)在遇到障礙之前都為 1,因為從 (0, 0) 的位置到 (i, 0) 的路徑只有一條,dp[0][j](第一行)同理。

64. 最小路徑和(劍指 Offer II 099. 最小路徑之和)

class Solution:def minPathSum(self, grid: List[List[int]]) -> int:m = len(grid)n = len(grid[0])dp = [[0 for _ in range(n)] for _ 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 j in range(1, n):dp[0][j] = dp[0][j-1] + grid[0][j]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[i][j] 的含義:表示從 (0, 0) 出發,到 (i, j) 的最小路徑之和

遞推公式:dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]

初始條件:dp[0][0] 就是 grid[0][0],然后第一列與第一行的最小路徑之和都是唯一的,就是單純地累加

931. 下降路徑最小和

class Solution:def minFallingPathSum(self, matrix: List[List[int]]) -> int:n = len(matrix)dp = [[0 for _ in range(n)] for _ in range(n)]for j in range(n):dp[0][j] = matrix[0][j]for i in range(1, n):for j in range(n):if j == 0:dp[i][j] = min(dp[i-1][j], dp[i-1][j+1]) + matrix[i][j]elif j == n-1:dp[i][j] = min(dp[i-1][j], dp[i-1][j-1]) + matrix[i][j]else:dp[i][j] = min(dp[i-1][j], dp[i-1][j+1], dp[i-1][j-1]) + matrix[i][j]return min(dp[-1])

dp[i][j] 的含義:表示從 (0, 0) 出發,到達 (i, j) 的最小路徑之和

遞推公式
如果在最左邊的話,路徑和就等于正上方和右上方兩者中小的路徑和加上當前的路徑花費,
dp[i][j] = min(dp[i-1][j], dp[i-1][j+1]) + matrix[i][j]
如果在最右邊的話,路徑和就等于左上方和正上方兩者中小的路徑和加上當前的路徑花費,
dp[i][j] = min(dp[i-1][j], dp[i-1][j-1]) + matrix[i][j]
如果在中間,則路徑和就等于左上方、正上方和右上方三者中小的路徑和加上當前的路徑花費,dp[i][j] = min(dp[i-1][j], dp[i-1][j+1], dp[i-1][j-1]) + matrix[i][j]

初始條件:dp 數組的第一層等于 matrix 第一層,實際上最左邊和最右邊也可以作為初始化

120. 三角形最小路徑和(劍指 Offer II 100. 三角形中最小路徑之和)

class Solution:def minimumTotal(self, triangle: List[List[int]]) -> int:n = len(triangle)dp = []for i in range(1, n+1):dp.append([0 for _ in range(i)])dp[0][0] = triangle[0][0]for i in range(1, n):for j in range(i+1):if j == 0:dp[i][j] = dp[i-1][j] + triangle[i][j]elif j == i:dp[i][j] = dp[i-1][j-1] + triangle[i][j] else:dp[i][j] = min(dp[i-1][j], dp[i-1][j-1]) + triangle[i][j]return min(dp[-1])

dp[i][j] 的含義:表示從 (0, 0) 出發,到達 (i, j) 的最小路徑之和

遞推公式
如果在最左邊的話,路徑和就等于正上方的路徑和加上當前的路徑花費,
dp[i][j] = dp[i-1][j] + triangle[i][j]
如果在最右邊的話,路徑和就等于左上方的路徑和加上當前的路徑花費,
dp[i][j] = dp[i-1][j-1] + triangle[i][j]
如果在中間,則路徑和就等于正上方和左上方兩者中小的路徑和加上當前的路徑花費,
dp[i][j] = min(dp[i-1][j], dp[i-1][j-1]) + triangle[i][j]

初始條件:dp 數組的第一層等于 triangle 第一層,實際上最左邊和最右邊也可以作為初始化

這題注意 j 的循環次數,在第 i 層就循環 i 次

1289. 下降路徑最小和 II

困難題,我最開始的想法是,在上上題的基礎上,讓當前位置最小路徑和等于不是當前列的路徑和中最小的值再加上當前位置的路徑花費:

class Solution:def minFallingPathSum(self, grid: List[List[int]]) -> int:m, n = len(grid), len(grid[0])dp = [[0] * n for _ in range(m)]dp[0][:] = grid[0][:]for i in range(1, m):for j in range(n):temp = float('inf')for k in range(n):if k != j and dp[i-1][k] < temp:temp = dp[i-1][k]dp[i][j] = temp + grid[i][j]return min(dp[-1])

但是顯然,這個方法的時間復雜度是 O(m * n * n),容易超時。換一個思路,如果知道了第一層的最小路徑,那么在第二層除了跟它同一列的位置,都是加上這個最小路徑為最優;那同一列的位置加誰呢?第一層中第二小的路徑唄。這樣實際上就完成了從第一層到第二層的遞推,dp 數組自然可以構建出來了。

class Solution:def minFallingPathSum(self, grid: List[List[int]]) -> int:m, n = len(grid), len(grid[0])dp = [[0] * n for _ in range(m)]dp[0][:] = grid[0][:]for i in range(1, m):# 找到最小的值minflag = dp[i-1].index(min(dp[i-1]))# 除了同一列的位置,都加上這個最小值for j in range(0, n):if j != minflag:dp[i][j] = grid[i][j] + dp[i-1][minflag]# 找到第二小的值if minflag == 0:minflag2 = min(dp[i-1][1:])elif minflag == n - 1:minflag2 = min(dp[i-1][:-1])else:minflag2 = min(min(dp[i-1][:minflag]), min(dp[i-1][minflag+1:]))# 給同一列的位置加上這個第二小的值dp[i][minflag] = grid[i][minflag] + minflag2return min(dp[-1])

343. 整數拆分

class Solution:def integerBreak(self, n: int) -> int:dp = [0 for _ in range(n+1)]dp[2] = 1for i in range(3, n+1):for j in range(1, i):# 假設對正整數 i 拆分出的第一個正整數是 j(1 <= j < i),則有以下兩種方案:# 1) 將 i 拆分成 j 和 i?j 的和,且 i?j 不再拆分成多個正整數,此時的乘積是 j * (i-j)# 2) 將 i 拆分成 j 和 i?j 的和,且 i?j 繼續拆分成多個正整數,此時的乘積是 j * dp[i-j]dp[i] = max(dp[i], j * (i - j), j * dp[i - j])return dp[n]

dp[i] 的含義:表示分拆數字 i,可以得到的最大乘積為 dp[i]

遞推公式:dp[i] = max(dp[i], j * (i - j), j * dp[i - j])

初始條件:dp[2] = 1(dp[0] dp[1] 不應該初始化,因為沒有意義)

96. 不同的二叉搜索樹

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

dp[i] 的含義:1到 i 為節點組成的二叉搜索樹的個數為 dp[i]

遞推公式:dp[i] += dp[j-1] * dp[i-j]

初始條件:dp[0] = 1

當 n = 1 時,dp[1] = 1;當 n = 2 時,dp[2] = 2;當 n = 3 時,左右子樹可能的數量分別為(2,0)、(1,1)、(0,2),這對應了 dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2];后面的以此類推。

總結

以上是生活随笔為你收集整理的爬楼梯与路径类题目记忆化递归与动态规划双解法(Leetcode题解-Python语言)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。