LIS和LCS都是动态规划典型题目,常规做法是O(n^2)的,但是可以优化成O(nlgn)。
例题: 300. 最长递增子序列
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
def lis(nums):
n = len(nums)
# dp = [float('inf')] * (n+1) # dp[i](从1开始) 为长度为i的上升子序列的可以的最小尾巴
dp = []
for num in nums:
if not dp or num > dp[-1]:
dp.append(num)
else:
pos = bisect_left(dp,num)
dp[pos] = num
return len(dp)
return lis(nums)
链接: 1143. 最长公共子序列
lcs_n2 是O(n2)的,lcs_nlgn是优化的。
优化思路,把s1的数全部转化为下标,逆序拼接,在s2中映射对应下标,然后计算LIS。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
def lis_nlgn(nums):
n = len(nums)
# dp = [float('inf')] * (n+1) # dp[i] 为长度为i的上升子序列的可以的最小尾巴
dp = []
for num in nums:
if not dp or num > dp[-1]:
dp.append(num)
else:
pos = bisect_left(dp,num)
dp[pos] = num
return len(dp)
def lcs_nlgn(s1,s2):
s1_poses = collections.defaultdict(list)
for i in range(len(s1)-1,-1,-1): # 需要逆序
s1_poses[s1[i]].append(i)
cleaned_s2_pos = list(itertools.chain.from_iterable([s1_poses[x] for x in s2 if x in s1_poses]))
same_lis = lis_nlgn(cleaned_s2_pos)
return same_lis
def lcs_n2(s1,s2):
m,n = len(s1),len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1,m+1):
for j in range(1,n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j],dp[i][j-1])
return dp[m][n]
return lcs_nlgn(text1,text2)
链接: 1300. 转变数组后最接近目标值的数组和
和模板2的区别在于,s1这个字符串本身是去重的,那么转化下标时,每个数的下标不需要list,效率快不少。
class Solution:
def minOperations(self, target: List[int], arr: List[int]) -> int:
'''
题意一看是找LCS(最长公共子序列)。然后给arr中补上target中剩下的数就行。
但是LCS标准DP是O(nm),过不了。
需要转化成LIS(最长上升子序列)来二分优化。
本题最长公共子序列的意思是target中的数,在arr中尽量找到的多,且相对位置保持不变。
位置就是下标,位置有序转换过去就是下标递增。
由于target本身是去重的,因此可以翻转下标,用下标唯一来代表这个数。
也就是说把target中每个数的下标映射到arr中,寻找子序列,下标是递增的,问题转化为LIS
arr中的不在target中的字符是没用的,可以先干掉。
'''
def lis_nlgn(nums):
# 最长上升序列的二分优化版本
n = len(nums)
# dp = [float('inf')] * (n+1) # dp[i] 为长度为i的上升子序列的可以的最小尾巴
dp = []
for num in nums:
if not dp or num > dp[-1]:
dp.append(num)
else:
pos = bisect_left(dp,num)
dp[pos] = num
return len(dp)
def lcs_nlgn_when_s1_unique(s1,s2):
# 当s1本身是去重的,用这个,下标不用拼list
s1_poses = {v:pos for pos,v in enumerate(target)}
cleaned = [s1_poses[x] for x in arr if x in s1_poses]
return lis_nlgn(cleaned)
def lcs_nlgn(s1,s2):
# 转lis利用二进制优化。
s1_poses = collections.defaultdict(list)
for i in range(len(s1)-1,-1,-1): # 需要逆序
s1_poses[s1[i]].append(i)
cleaned_s2_pos = list(itertools.chain.from_iterable([s1_poses[x] for x in s2 if x in s1_poses]))
return lis_nlgn(cleaned_s2_pos)
return len(target)-lcs_nlgn_when_s1_unique(target,arr)
链接: 926. 将字符串翻转到单调递增
这题最佳做法是DP,O(n)。但LIS也能做,从提交时间上来看差的不多。
参见: [LeetCode解题报告] 926. 将字符串翻转到单调递增
和LIS的区别,bisect_left改成bisect_right,dp append条件从>变成>=。
class Solution:
def minFlipsMonoIncr(self, s: str) -> int:
def lis_non_decrease(nums):
n = len(nums)
# dp = [float('inf')] * (n+1) # dp[i] 为长度为i的上升子序列的可以的最小尾巴
dp = []
for num in nums:
if not dp or num >= dp[-1]:
dp.append(num)
else:
pos = bisect_right(dp,num)
dp[pos] = num
return len(dp)
return len(s) - lis_non_decrease([int(c) for c in s])