子序列的好多问题归结起来就是类似的转移关系。
找状态转移方程的方法是,思考每个状态有哪些「选择」,只要我们能用正确的逻辑做出正确的选择,算法就能够正确运行。
动态规划之子序列问题解题模板–两种思路
参考https://mp.weixin.qq.com/s/zNai1pzXHeB2tQE6AdOXTA
1、一个一维的DP数组
int n = array.length;
int[] dp = new int[n];
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
dp[i] = 最值(dp[i], dp[j] + ...)
}
}
在子数组array[0…i]中,以array[i]结尾的目标子序列(最长递增子序列)的长度是dp[i]。
2、第二种思路模板是一个二维的 dp 数组
int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 1; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}
dp 数组含义又分为「只涉及一个字符串」和「涉及两个字符串」两种情况。
在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。
2.2 只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:
在子数组array[i…j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]。
一维–53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
dp[i]的含义是如果加上更大,则更大。
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
dp = [0] * n
for i in range(n):
if i == 0:
dp[i] = nums[i]
continue
dp[i] = max(dp[i-1] + nums[i], nums[i])
return max(dp)
信封问题
**信封问题的关键是排序,排序后和最长递增子序列其实是一样的。**先对长进行排序,再对宽进行排序,其实后续的操作就是针对宽的排序来进行。其实下面解法的复杂度是n*n,dp[i]代表的是第i个的最长递增,所以每一次都要遍历一下之前的,确保是最大的。 不遍历的话,可能存在以下情况: 1、 2、4.5、5、 4、 6、9、 7; 5对应的是4, 而4只对应了3。
a = [[1,3], [2, 9], [2,2], [1,5]]
a = sorted(a, key=lambda x:(x[0], x[1]))
print(a)
# dp[i] 代表nums[1:j]的最长序列
def envelope(nums):
nums = sorted(nums, key=lambda x:(x[0], x[1]))
dp = [1] * len(nums)
for i in range(len(nums)):
for j in range(0, i):
if i == 0:
continue
if nums[i][1] > nums[j][1]:
if dp[i] <= dp[j]:
dp[i] = dp[j] + 1
return dp[-1]
print(envelope(a))
涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。
最长递增子序列
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
# dp[i]代表的是第i个数的最长子序列
# 转移方程: dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
# if nums[i] > nums[i-1]: dp[i] = dp[i-1] + 1
# dp[i]代表的是第i个数的最长子序列
# 转移方程: dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)
dp = [0] * len(nums)
for i in range(len(nums)):
if i == 0:
dp[i] = 1
else:
dp[i] = 1
n = i - 1
while n >= 0:
if nums[i] > nums[n]:
if dp[i] <= dp[n]:
dp[i] = dp[n] + 1
n -= 1
return max(dp)
上述的时间复杂度是n*n。
但是面试肯定是要nlogn的:解决如下: 看图更容易理解:其实下图还是等级于蜘蛛纸牌的方法。如果5后面的3可以在5前面找到比5更小的,当然就不会改5了。而是改前面的。
优化:参考蜘蛛纸牌问题:按序从大到小进行堆放,如果大于所有堆顶,则新建一个堆,每次叠放优先选择最左边的堆顶。因此可以得到堆顶的牌其实都是有序的,从左到右递增,每个堆也是有序的。
nlogn—多少个堆就有多少个。此时out只记录堆顶的数即可,因为只需要堆顶和总的堆数。
二分问题其实有一点要注意
# 蜘蛛牌+二分
class Solution1:
def lengthOfLIS(self, nums):
if not nums:
return 0
n = len(nums)
out = [nums[0]]
for i in range(1, n):
done = 0
start = 0
end = len(out)
while start < end:
mid = (start + end) // 2
if nums[i] > out[mid]:
start = mid + 1
elif nums[i] <= out[mid]:
done = 1
end = mid
if done == 1:
out[end] = nums[i]
if done == 0:
out.append(nums[i])
return len(out)
1143. 最长公共子序列
讲解:https://mp.weixin.qq.com/s/SUJ35XDpTn5OKU7hud-tPw
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。使用了一个二维数组来记录。
首先明确好,跟上述的二维表示一样,dp[i][j]表示的是str1[1:i]和str2[1:j]的相同部分,可以由max(dp[i-1][j], dp[i][j-1],dp[i-1][j-1]来获得)—通过表格可以画出:
先用暴力的递归解决,再思考动态规划:
"""
# 超时,加上装饰器就可以通过。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
import functools
@functools.lru_cache(None)
def dp(i, j):
if i < 0 or j < 0:
return 0
if text1[i] == text2[j]:
return dp(i-1, j-1)+1
else:
return max(dp(i-1, j), dp(i, j-1))
return dp(len(text1)-1, len(text2)-1)
class Solution: # 字典替代上述的装饰器。
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
record = {}
def dp(i, j):
if (i,j) in record:
return record[(i,j)]
if i < 0 or j < 0:
record[(i,j)] = 0
return 0
if text1[i] == text2[j]:
record[(i,j)] = dp(i-1, j-1)+1
return record[(i,j)]
else:
record[(i,j)] = max(dp(i-1, j), dp(i, j-1))
return record[(i,j)]
return dp(len(text1)-1, len(text2)-1)
"""
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
dp = []
if not text2 or not text1:
return 0
for i in range(len(text1)):
dp.append([0]*len(text2))
for i in range(len(text1)):
for j in range(len(text2)):
if text1[i] == text2[j]:
if i == 0 or j == 0:
dp[i][j] = 1
else:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
class Solution:
def LCS(self , s1 , s2 ):
# write code here "" += 无法过,直接赋可以过......
if not s1:
return "-1"
if not s2:
return "-1"
dp = []
m = len(s1)
n = len(s2)
res = []
for i in range(m):
dp.append([""]*n)
for i in range(m):
if s1[i] == s2[0]:
dp[i][0] = s1[i]
else:
if i == 0:
pass
else:
dp[i][0] = dp[i-1][0]
for j in range(n):
if s2[j] == s1[0]:
dp[0][j] = s2[j]
else:
if j == 0:
pass
else:
dp[0][j] = dp[0][j-1]
for i in range(1, m):
for j in range(1, n):
len1 = len(dp[i-1][j])
len2 = len(dp[i][j-1])
if s1[i] == s2[j]:
len3 = len(dp[i-1][j-1]) + 1
len4 = max([len1, len2, len3])
if len4 == len1:
dp[i][j] = dp[i-1][j]
elif len4 == len2:
dp[i][j] = dp[i][j-1]
else:
dp[i][j] = dp[i-1][j-1] + s1[i]
else:
if len1 > len2:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i][j-1]
if not dp[-1][-1]:
return "-1"
return dp[-1][-1]
583. 两个字符串的删除操作
-----画表
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
输入: “sea”, “eat”
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
提示:
给定单词的长度不超过500。
给定单词中的字符只含有小写字母。
重难点在于转移公式:
两种解法,第一种利用上面的最长公共子序列来进行,通过计算最长的之后,len(word1) + len(word2) - 2 * len(sub);
if word1[i] == word2[j]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
第二种是直接计算需要删除的次数,感觉理解还可以,需要注意的是存在空字符串的情况,所以定义dp数组大小的时候,注意加上一个维度,来表示""的情况,这样才可以把basecase写好。
if word1[i] == word2[j]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1
一 最长子序列
class Solution(object):
def minDistance(self, word1, word2):
"""
:type word1: str
:type word2: str
:rtype: int
其实就是求最长,然后 len(word1) + len(word2) - 2 * len(sub)
"""
if not word1 and not word2:
return 0
if not word1:
return len(word2)
if not word2:
return len(word1)
dp = []
for i in range(len(word1)):
dp.append([0]*len(word2))
for i in range(len(word1)):
for j in range(len(word2)):
if word1[i] == word2[j]:
if i == 0 or j == 0:
dp[i][j] = 1
else:
dp[i][j] = dp[i-1][j-1] + 1
else:
if i-1>=0 and j-1>=0:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
elif i-1>=0:
dp[i][j] = dp[i-1][j]
elif j-1>=0:
dp[i][j] = dp[i][j-1]
return len(word1) + len(word2) - dp[-1][-1] * 2
二 直接删除
# 动态规划也可以直接作用在删除的次数上
class Solution(object):
###### 注意考虑为空的情况,所以dp加上一个维度!!!!!!!!!!关键!!!!!!
def minDistance(self, word1, word2):
if not word1 and not word2:
return 0
if not word1:
return len(word2)
if not word2:
return len(word1)
dp = []
for i in range(len(word1)+1):
dp.append([0]*(1+len(word2)))
for i in range(len(word1)+1):
for j in range(len(word2)+1):
if i == 0:
dp[0][j] = j
continue
if j == 0:
dp[i][j] = i
continue
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
if i-1>=0 and j-1>=0:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1
return dp[-1][-1]
72. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
注意已经分成子问题了,每一个小的串都会考虑到,所以不需要担心 删除str1某个元素,如果str2存在该元素,那么操作数不就更多,其实不然~~因为这对元素的判断其实也考虑进来了—
这道题容易乱。但是想明白,其实就是str1的替换,删除,str2删除。每增加一个元素,判断str1[i]和str2[j]是否相同,相同的话,dp[i-1][j-1]相比dp[i-1][j]+1和dp[i][j-1]+1如果也是最少的操作,那么dp[i][j] = dp[i-1][j-1],如果不是最少,那么取另两个最少的+1; 如果不相同,可以是直接删除掉,反正就是从dp[i-1][j]和dp[i][j-1]转移过来,他们本身已经是相同的str了,选择操作数少的即可。
这道题虽然看似不简单,但是也就是跟上面一样的状态的转移,要看清楚,想明白dp[i][j],以及怎么转移,另外三个被转移的状态分别代表什么含义
虽然有删除有添加,但是其实
上述方法中,最后其实就是可以归结为:word1替换,word1删除,word2删除。
首先明确状态的含义:
dp[i][j]代表的是word1[:i+1]和word2[:j+1]之间的最小编辑距离。所以,dp[i][j]其实就是从dp[i-1][j],dp[i-1][j-1],dp[i][j-1]转移过来的。
相对于dp[i][j],dp[i-1][j]代表的是经过处理之后,word1[0:i]以及word2[:j+1]是完全一样的,那么此时dp[i][j]相比于dp[i-1][j]就是多了一个i,我们可以选择直接删除,或是在word2上加上word1[i],就是一个操作可以使得两者继续相等。同理,dp[i][j-1]也是一样的道理。
注意,dp[i-1][j-1]不一样的地方,如果word1[i] == word2[j],那么如果相比于前面两个,dp[i-1][j-1]是最小的,则dp[i][j] = dp[i-1][j-1];; 如果 word1[i] != word2[j], dp[i][j] = dp[i-1][j-1] + 1。
class Solution:
"""
dp[i][j]代表word1[:i+1] 与word2[:j+1]之间的最小编辑距离
"""
def minDistance(self, word1, word2):
if not word1 and not word2:
return 0
if not word1:
return len(word2)
if not word2:
return len(word1)
m = len(word1)
n = len(word2)
dp = []
for i in range(m+1):
dp.append([0]*(n+1))
for i in range(m+1):
for j in range(n+1):
if i == 0:
dp[i][j] = j
elif j == 0:
dp[i][j] = i
else:
if word1[i-1] != word2[j-1]:
dp[i][j] = min(dp[i-1][j]+1, dp[i-1][j-1]+1, dp[i][j-1]+1)
else:
dp[i][j] = min(dp[i-1][j]+1, dp[i-1][j-1], dp[i][j-1]+1)
return dp[-1][-1]
另外的解法:Labuladong
最小编辑代价
做这个题的关键是想好状态转移,现在的状态是基于之前的状态的,之前的状态就是已完成的状态.
class Solution:
def minEditCost(self , str1 , str2 , ic , dc , rc ):
if not str1:
return len(str2) * ic
elif not str2:
return len(str1) * dc
else:
dp = []
m = len(str1)
n = len(str2)
for i in range(m):
dp.append([0]*n)
for i in range(n):
if i == 0:
if str1[0] == str2[0]:
dp[0][i] = 0
else:
dp[0][i] = min(rc, ic+dc)
else:
dp[0][i] = dp[0][i-1]+ic
for i in range(m):
if i == 0:
if str1[0] == str2[0]:
dp[0][0] = 0
else:
dp[0][0] = min(rc, ic+dc)
else:
dp[i][0] = dp[i-1][0]+dc
for i in range(1, m):
for j in range(1, n):
if str1[i] == str2[j]:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]+dc, \
dp[i][j-1]+ic)
else:
dp[i][j] = min(dp[i-1][j-1]+rc, dp[i-1][j]+dc,\
dp[i][j-1] + ic)
return dp[-1][-1]