算法 - 动态规划
動態規劃是一種自底向上的算法,通常用于解決最大、最小等最值問題。
能使用動態規劃解決的問題,一定具備:
- 重疊子問題:和暴力搜索不同,需要記錄子問題的解,避免重復求解(剪枝)
- 最優子結構:子問題達到最值,整體才能達到最值,即以小見大
- 狀態轉移方程:在每個“狀態”做出的“選擇”會到達什么“狀態”
然后就以合適的順序填表,窮舉所有情況并求最值即可
整體流程:明確 base case -> 明確「狀態」-> 明確「選擇」 -> 定義 dp 數組/函數的含義
文章目錄
- 1.湊零錢
- 2.最長遞增子序列*
- 3.最長回文子序列*
- 最長回文子串
- 最長公共子序列
- 最長公共子串
- 4.打家劫舍
- 5.打家劫舍II
- 6.打家劫舍III *
- 7.含冷凍期的股票買賣
- 股票買賣的最佳時機II *
- 股票買賣的最佳時機III
- 8.編輯距離
- 帶權編輯距離
- 8.01背包問題
- 9.分割等和子集(背包問題模板)
- 10.湊硬幣II(完全背包問題)
- 11.目標和(01背包)
- 12 n個骰子的點數
1.湊零錢
給你一個整數數組 coins ,表示不同面額的硬幣;以及一個整數 amount ,表示總金額。
計算并返回可以湊成總金額所需的 最少的硬幣個數 。如果沒有任何一種硬幣組合能組成總金額,返回 -1 。
2.最長遞增子序列*
- 明確狀態:dp[n] = 以下標n結束的(而非從下標0到下標n的,這種弱綁定),最長序列長度
- 不是所有的題目都采用“強綁定”,只能說隨機應變
3.最長回文子序列*
- dp[ i ][ j ] = 從下標 i 到下標 j 的最長回文子序列的長度(不包括頭尾,即 s[ i ] != s[ j ] 時 dp[ i ][ j ] 可以不為 0)
- 每次填一個左斜的列
最長回文子串
- 填表的時候,每個位置需要查看其左下方向的值,所以填表順序可以是,先將對角線上的兩列初始化,再按列填表
- dp[ i ][ j ] = s[ i : j + 1] 是否是回文子串
最長公共子序列
class Solution(object):def longestCommonSubsequence(self, text1, text2):""":type text1: str:type text2: str:rtype: int"""# dp[i][j] = t1的前i個字母,和t2的前j個字母的最長相同子序列長度l1, l2 = len(text1), len(text2)dp = [[0 for _ in range(l2 + 1)] for _ in range(l1 + 1)]# 填表,按行填for i in range(1, l1 + 1):for j in range(1, l2 + 1):if text1[i - 1] == text2[j - 1]:dp[i][j] = dp[i - 1][j - 1] + 1else:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])return dp[-1][-1]最長公共子串
class Solution:def LCS(self , str1 , str2 ):# write code herem, n = len(str1), len(str2)if m == 1:return str1 elif n == 1:return str2 dp = [[0 for _ in range(n)] for _ in range(m)]res, end1idx = 0, 0 # 初始化for i in range(m):if str1[i] == str2[0]:dp[i][0] = 1res = 1end1idx = ifor j in range(n):if str2[j] == str1[0]:dp[0][j] = 1# 填表for i in range(1, m):for j in range(1, n):if str1[i] == str2[j]:dp[i][j] = dp[i - 1][j - 1] + 1end1idx = i if res < dp[i][j] else end1idxres = max(res, dp[i][j])return str1[end1idx - res + 1: end1idx + 1]4.打家劫舍
一般的動態規劃(推薦)
class Solution(object):def rob(self, nums):""":type nums: List[int]:rtype: int"""# 寫個動態規劃# 狀態:偷到i號房子時的最大值# 選擇:是否偷當前的房子# dp[i]=max(dp[i - 2] + nums[i], dp[i - 1])length = len(nums)if length == 1:return nums[0]dp = [0 for _ in range(length)]dp[0] = nums[0]dp[1] = nums[0] if nums[0] > nums[1] else nums[1]for i in range(2, length):steal = dp[i - 2] + nums[i]not_steal = dp[i - 1]dp[i] = steal if steal > not_steal else not_stealreturn dp[length - 1]還可以使用自頂向下遞歸+備忘錄。這種方法引入了遞歸,比較適合解決樹結構的問題——打家劫舍III
class Solution(object):def rob(self, nums):""":type nums: List[int]:rtype: int"""memo = [-1 for _ in range(len(nums))]return self.search(0, nums, memo) # 或memo[0]def search(self, idx, nums, memo):length = len(nums)# 遞歸出口if idx > length - 1:return 0# 查表避免計算elif memo[idx] != -1:return memo[idx]# 后根,先遞歸后返回,以獲取當前序列之后的信息else:steal = nums[idx] + self.search(idx + 2, nums, memo)not_steal = self.search(idx + 1, nums, memo)memo[idx] = max(steal, not_steal)return memo[idx]5.打家劫舍II
將輸入變成環形,和 I 相比討論兩種情況,取最大值。
class Solution(object):def rob(self, nums):""":type nums: List[int]:rtype: int"""length = len(nums)if length == 1:return nums[0]elif length <= 3:return max(nums)# 分兩種情況:[1:]和[:-1]nums1 = nums[1: ]nums2 = nums[: -1]dp1 = [0 for _ in range(length - 1)]dp1[0] = nums1[0]dp1[1] = max(nums1[0], nums1[1])for i in range(2, length - 1):dp1[i] = max(dp1[i - 1], dp1[i - 2] + nums1[i])dp2 = [0 for _ in range(length - 1)]dp2[0] = nums2[0]dp2[1] = max(nums2[0], nums2[1])for i in range(2, length - 1):dp2[i] = max(dp2[i - 1], dp2[i - 2] + nums2[i])return max(dp1[-1], dp2[-1])6.打家劫舍III *
- 一道典中典的題:動態規劃+樹結構 = memo+遞歸
- 第二次做的時候思維卡在了,想先獲取某節點的父節點,和父節點的父節點(類似于普通dp的想法)來確定當前點的值,使用遞歸的好處是可以“知道”后續的信息,在當前加以判斷
對于樹結構的問題,明確每個節點該干什么
class Solution(object):def rob(self, root):""":type root: TreeNode:rtype: int"""# dp+樹=memo+遞歸memo = dict()def search(node):# 出口if node is None:return 0# 查表if memo.has_key(node):return memo[node]# 搶l, r = 0, 0if node.left is not None:l = search(node.left.left) + search(node.left.right)if node.right is not None:r = search(node.right.left) + search(node.right.right)yes = node.val + l + r# 不搶no = search(node.left) + search(node.right)# 取最大值寫入memomemo[node] = max(yes, no)return memo[node]return search(root)7.含冷凍期的股票買賣
給定一個整數數組,其中第 i 個元素代表了第 i 天的股票價格 。?
設計一個算法計算出最大利潤。在滿足以下約束條件下,你可以盡可能地完成更多的交易(多次買賣一支股票):
- 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
- 賣出股票后,你無法在第二天買入股票 (即冷凍期為 1 天)。
股票買賣的最佳時機II *
- 可以多次交易,但每次只能持有一個股票
- 狀態DP,加入當前是否已持有股票的狀態
股票買賣的最佳時機III
- 和上面類似,但限制最多只能交易兩次
- 需要額外加入兩個狀態
8.編輯距離
問題復雜,題解簡介。關鍵是找到DP數組的定義。
選擇思路——引用leetcode下面的高贊評論:
-
問題1:如果 word1[0…i-1] 到 word2[0…j-1] 的變換需要消耗 k 步,那 word1[0…i] 到 word2[0…j] 的變換需要幾步呢?
-
答:先使用 k 步,把 word1[0…i-1] 變換到 word2[0…j-1],消耗 k 步。再把 word1[i] 改成 word2[j],就行了。如果 word1[i] == word2[j],什么也不用做,一共消耗 k 步,否則需要修改,一共消耗 k + 1 步。
-
問題2:如果 word1[0…i-1] 到 word2[0…j] 的變換需要消耗 k 步,那 word1[0…i] 到 word2[0…j] 的變換需要消耗幾步呢?
-
答:先經過 k 步,把 word1[0…i-1] 變換到 word2[0…j],消耗掉 k 步,再把 word1[i] 刪除,這樣,word1[0…i] 就完全變成了 word2[0…j] 了。一共 k + 1 步。
-
問題3:如果 word1[0…i] 到 word2[0…j-1] 的變換需要消耗 k 步,那 word1[0…i] 到 word2[0…j] 的變換需要消耗幾步呢?
-
答:先經過 k 步,把 word1[0…i] 變換成 word2[0…j-1],消耗掉 k 步,接下來,再插入一個字符 word2[j], word1[0…i] 就完全變成了 word2[0…j] 了。
帶權編輯距離
給定兩個字符串str1和str2,再給定三個整數ic,dc和rc,分別代表插入、刪除和替換一個字符的代價,請輸出將str1編輯成str2的最小代價
class Solution:def minEditCost(self , str1 , str2 , ic , dc , rc ):# write code here# dp[i][j] 代表從s1[0...i] 修改為s2[0...j]的代價# dp[i][j] = dp[i-1][j-1] if equals else ...m, n = len(str1), len(str2)dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]for i in range(m + 1):dp[i][0] = i * dc for j in range(n + 1):dp[0][j] = j * ic for i in range(1, m + 1):for j in range(1, n + 1):if str1[i - 1] == str2[j - 1]:dp[i][j] = dp[i - 1][j - 1] else:dp[i][j] = min(dp[i - 1][j] + dc, dp[i][j - 1] + ic, dp[i - 1][j - 1] + rc)return dp[-1][-1]8.01背包問題
- 算法設計課上的例題。物品只能選擇裝入/不裝入,所以是01背包。
- 01背包的問題形式:湊夠目標和target ——(能否)湊夠target,湊target有幾種方式
9.分割等和子集(背包問題模板)
抽象成可裝載重量為 sum / 2 的背包,每個物品的重量為 nums[i],在 sum/2 的前提下,盡量往里裝最多的數字,如果恰好能為sum / 2則滿足題意
class Solution(object):def canPartition(self, nums):""":type nums: List[int]:rtype: bool"""# 特判奇偶,奇數不能劃分summary = sum(nums)if summary % 2 == 1:return Falsetarget = summary // 2n = len(nums)dp = [[0 for _ in range(target + 1)] for _ in range(n)]# 初始化首行if nums[0] == target:return Truefor i in range(target + 1):dp[0][i] = nums[0] if i >= nums[0] else 0# 填表:經典的背包模板for i in range(1, n):for j in range(target + 1):# 不能裝下物品iif j < nums[i]:dp[i][j] = dp[i - 1][j]# 可以裝下物品ielse:dp[i][j] = max(dp[i - 1][j], (dp[i - 1][j - nums[i]] if nums[i] <= j else 0) + nums[i])return dp[-1][-1] == target可以進一步優化空間:因為每行在填寫時只使用上一行dp,所以dp只保留一行即可
10.湊硬幣II(完全背包問題)
- 處理背包問題一定要注意dp數組的定義,不要少定義下標
- 完全背包問題的每種物品數量無限
- 這道題同樣可以對dp數組進行空間優化
11.目標和(01背包)
給你一個整數數組 nums 和一個整數 target 。 向數組中的每個整數前添加 ‘+’ 或 ‘-’ ,然后串聯起所有整數,可以構造一個
表達式 : 例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串聯起來得到表達式
“+2-1” 。 返回可以通過上述方法構造的、運算結果等于 target 的不同 表達式 的數目。
12 n個骰子的點數
- 把n個骰子扔在地上,所有骰子朝上一面的點數之和為s。輸入n,打印出s的所有可能的值出現的概率
總結
- 上一篇: 以创新音频技术破局 华为引领TWS迈向全
- 下一篇: 算法 - 前缀和