子串、子数组与子序列类型问题的动态规划求解(Leetcode题解-Python语言)
一般來說,子串和子數(shù)組都是連續(xù)的,而子序列是可以不連續(xù)的,遇到子序列問題基本上都是用動(dòng)態(tài)規(guī)劃求解。
53. 最大子數(shù)組和(劍指 Offer 42. 連續(xù)子數(shù)組的最大和)
class Solution:def maxSubArray(self, nums: List[int]) -> int:n = len(nums)dp = [-10001] * (n+1)dp[0] = nums[0]for i in range(1, n):dp[i] = max(dp[i-1] + nums[i], nums[i])return max(dp)把 dp 數(shù)組定義為:元素 dp[i] 表示以 nums[i] 為結(jié)尾的數(shù)組的連續(xù)子數(shù)組最大和。則初始條件為 dp[0] = nums[0] (只有一個(gè)元素)。如果知道了 dp[i-1],則 dp[i] 只有兩種取值:dp[i-1] + nums[i] 和 nums[i],取兩者中的較大值即可。
152. 乘積最大子數(shù)組
class Solution:def maxProduct(self, nums: List[int]) -> int:n = len(nums)if n == 1:return nums[0]max_dp = [0] * nmin_dp = [0] * nmax_dp[0] = min_dp[0] = nums[0]for i in range(1, n):max_dp[i] = max(max_dp[i-1] * nums[i], min_dp[i-1] * nums[i], nums[i])min_dp[i] = min(max_dp[i-1] * nums[i], min_dp[i-1] * nums[i], nums[i])return max(max(max_dp), max(min_dp))求乘積的話能不能照搬上面的思路呢?是不可以的,因?yàn)槌朔e可能為負(fù)數(shù),每次只取較大值的話是不會(huì)選擇負(fù)數(shù)的,但是最大乘積可能由負(fù)負(fù)得正而來。解決方法是使用兩個(gè) dp 數(shù)組,一個(gè)記錄最大值一個(gè)記錄最小值(負(fù)的最大),這樣當(dāng)?shù)谝淮纬霈F(xiàn)負(fù)數(shù)時(shí),結(jié)果會(huì)被 min_dp 記錄下來,而第二次出現(xiàn)負(fù)數(shù)的時(shí)候,結(jié)果又會(huì)進(jìn)入 max_dp。
674. 最長連續(xù)遞增序列
class Solution:def findLengthOfLCIS(self, nums: List[int]) -> int:n = len(nums)dp = [1] * nfor i in range(1, n):if nums[i-1] < nums[i]:dp[i] = dp[i-1] + 1return max(dp)這題的關(guān)鍵詞是連續(xù),所以如果在位置 i 的數(shù)字大于前一個(gè)數(shù)字,就記錄這個(gè)位置的序列長度(dp[i])為前一個(gè)位置序列長度加一( dp[i-1] + 1)
300. 最長遞增子序列
class Solution:def lengthOfLIS(self, nums: List[int]) -> int:n = len(nums)dp = [1] * nfor i in range(n):for j in range(i):if nums[i] > nums[j]:dp[i] = max(dp[i], dp[j] + 1)return max(dp)將 dp 數(shù)組定義為:元素 dp[i] 表示以 nums[i] 為結(jié)尾的數(shù)組的最長遞增子序列長度。初始化 dp 數(shù)組的每個(gè)元素都是1(最小都是它本身,長度為1)。當(dāng)遍歷到 dp[i] 時(shí),它前面的每個(gè) dp[j] (j < i) 我們都是知道的,需要從中找到能構(gòu)成遞增關(guān)系的(nums[i] > nums[j]),最大長度 max(dp[i], dp[j] + 1)。
1143. 最長公共子序列
class Solution:def longestCommonSubsequence(self, text1: str, text2: str) -> int:m, n = len(text1), len(text2)dp = [[0 for _ in range(n+1)] for _ in range(m+1)]ans = 0for i in range(1, m+1):for j in range(1, n+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]dp[i][j]:長度為 [0, i - 1] 的字符串 text1 與長度為 [0, j - 1] 的字符串 text2 的最長公共子序列
如果text1[i - 1] 與 text2[j - 1]相同,那么找到了一個(gè)公共元素,所以 dp[i][j] = dp[i - 1][j - 1] + 1; 如果 text1[i - 1] 與 text2[j - 1] 不相同,那就看看 text1[0, i - 2] 與 text2[0, j - 1] 的最長公共子序列 和 text1[0, i - 1] 與 text2[0, j - 2] 的最長公共子序列,取最大的。
如果 dp[i][j] 表示的是長度為 [0, i] 的字符串 text1 與長度為 [0, j] 的字符串 text2 的最長公共子序列,在初始化上就麻煩不少,代碼如下:
class Solution:def longestCommonSubsequence(self, text1: str, text2: str) -> int:m, n = len(text1), len(text2)dp = [[0 for _ in range(n)] for _ in range(m)]ans = 0if text1[0] == text2[0]:dp[0][0] = 1for i in range(1, m):if text1[i] == text2[0] or dp[i-1][0] == 1:dp[i][0] = 1for j in range(1, n):if text1[0] == text2[j] or dp[0][j-1] == 1:dp[0][j] = 1for i in range(1, m):for j in range(1, n):if text1[i] == text2[j]: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]673. 最長遞增子序列的個(gè)數(shù)
class Solution:def findNumberOfLIS(self, nums: List[int]) -> int:n = len(nums)max_len = ans = 0dp = [1] * ncnt = [1] * nfor i in range(n):for j in range(i):# 如果當(dāng)前元素可以加入遞增序列,使得dp[j]可以+1if nums[i] > nums[j]:# 遇到更長的遞增子序列,則更新if dp[j] + 1 > dp[i]:dp[i] = dp[j] + 1cnt[i] = cnt[j] # 重置計(jì)數(shù)# 相同長度的遞增子序列elif dp[j] + 1 == dp[i]:cnt[i] += cnt[j]if dp[i] > max_len:max_len = dp[i]ans = cnt[i] # 重置計(jì)數(shù)elif dp[i] == max_len:ans += cnt[i]return ans在上一題的基礎(chǔ)上,要對最長遞增子序列的個(gè)數(shù)進(jìn)行計(jì)數(shù),我們定義一個(gè) cnt 數(shù)組,cnt[i] 表示以 nums[i] 結(jié)尾的最長遞增子序列的個(gè)數(shù)。設(shè) nums 的最長遞增子序列的長度為 maxLen,那么答案 ans 為所有滿足 dp[i] = maxLen 的 i 所對應(yīng)的 cnt[i] 之和。關(guān)鍵是對當(dāng)前元素可以加入遞增序列(nums[i] > nums[j])后的情況進(jìn)行分類討論,以找出當(dāng)前最長的遞增子序列以及對它進(jìn)行計(jì)數(shù)。
354. 俄羅斯套娃信封問題
class Solution:def maxEnvelopes(self, envelopes: List[List[int]]) -> int:envelopes.sort(key=lambda k: (k[0], -k[1]))n = len(envelopes)dp = [1] * nfor i in range(n):for j in range(i):if envelopes[i][1] > envelopes[j][1]:dp[i] = max(dp[i], dp[j] + 1)return max(dp)關(guān)鍵思路是對寬度進(jìn)行升序排序而對高度進(jìn)行降序排序,降序的目的是為了保證寬度相同時(shí)只有一個(gè)信封會(huì)被選入到最長遞增子序列當(dāng)中(如果高度也是升序,則會(huì)進(jìn)入多個(gè),但是它們的寬度相同,不符合題意)。最后求關(guān)于高度的最長遞增子序列的長度即為答案。
198. 打家劫舍
class Solution:def rob(self, nums: List[int]) -> int:n = len(nums)if n <= 2:return max(nums)dp = [0] * ndp[0] = nums[0]dp[1] = max(nums[0], nums[1])for i in range(2, n):dp[i] = max(dp[i-1], dp[i-2] + nums[i])return dp[-1]這題可以看作是不相鄰子序列的最大和,dp[i] 表示到第 i 號房為止能偷到的最多錢,狀態(tài)轉(zhuǎn)移方程為偷了第 i - 2 家后再偷第 i 家或者偷了第 i - 1 家(不能偷第 i 家了)兩者的最大值。
740. 刪除并獲得點(diǎn)數(shù)
class Solution:def deleteAndEarn(self, nums: List[int]) -> int:# 以數(shù)字作為下標(biāo),對應(yīng)點(diǎn)數(shù)作為值maxVal = max(nums)total = [0] * (maxVal + 1)for val in nums:total[val] += val# 打家劫舍問題n = len(total)if n == 1:return total[0]dp = [0] * ndp[0] = total[0]dp[1] = max(total[0], total[1])for i in range(2, n):dp[i] = max(dp[i-2] + total[i], dp[i-1])return dp[n-1]將出現(xiàn)的數(shù)字作為下標(biāo),數(shù)字出現(xiàn)次數(shù) * 數(shù)字本身 = 數(shù)字對應(yīng)的點(diǎn)數(shù),點(diǎn)數(shù)作為值,構(gòu)建一個(gè) total 數(shù)組,即變成了對于 total 數(shù)組的打家劫舍問題。
213. 打家劫舍 II
class Solution:def rob(self, nums: List[int]) -> int:if not nums:return 0n = len(nums)if n == 1:return nums[0]if n == 2:return max(nums)dp_1 = [0] * ndp_2 = [0] * ndp_1[0] = nums[0]dp_1[1] = max(nums[0], nums[1])dp_2[1] = nums[1]dp_2[2] = max(nums[1], nums[2])for i in range(2, n-1):dp_1[i] = max(dp_1[i-2] + nums[i], dp_1[i-1])for i in range(3, n):dp_2[i] = max(dp_2[i-2] + nums[i], dp_2[i-1])return max(dp_1[-2], dp_2[-1])房屋首尾相連了,意味著偷了第一家就不能偷最后一家,反之亦然。因此,我們可以分類討論,把偷第一家和偷最后一家分別考慮,用兩個(gè) dp 分別算出兩個(gè)方案的結(jié)果,取較大值即可。對于第一家和最后一家都不偷的情況,其實(shí)已經(jīng)被包含在上面兩種情況里面了,因?yàn)樯厦嬉仓皇强紤]偷第一家或最后一家,但不一定偷。
337. 打家劫舍 III
class Solution:def rob(self, root: TreeNode) -> int:def postTravel(root):if not root: return 0, 0 # 偷,不偷left = postTravel(root.left)right = postTravel(root.right)# 偷當(dāng)前節(jié)點(diǎn), 則左右子樹都不能偷val_1 = root.val + left[1] + right[1]# 不偷當(dāng)前節(jié)點(diǎn), 則取左右子樹中最大的值val_2 = max(left) + max(right)return val_1, val_2return max(postTravel(root))這題實(shí)際上不算子序列問題,而是樹形 dp 問題,但還是打家劫舍系列的所以放一起了。使用的是后序遍歷,因?yàn)橐全@得左右子樹的結(jié)果,然后對于當(dāng)前節(jié)點(diǎn),有偷或者不偷兩種方案,都需要返回,取其中的較大值即可,對于左右子樹也是一樣的。
總結(jié)
以上是生活随笔為你收集整理的子串、子数组与子序列类型问题的动态规划求解(Leetcode题解-Python语言)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电信天翼分享4条手机保养小技巧 电量最好
- 下一篇: 在数组中找重复数、只出现一次的数或丢失数