LIS(Longest Increasing Subsequence)问题是一个经典的动态规划问题
LeetCode 300. 最长递增子序列
给出一个长为n的序列s,求其中最长递增子序列的长度
例如,n=6,s=172548,则长递增子序列为1258,长度为4
思路:
这样定义dp[i],是子序列问题中,常用的dp数组定义方法
dp[0]...dp[i-1]
,怎么求dp[i]
:对于字符s[i]
,可以将其拼接到其他递增序列上,求其中的最大值dp[i] = max(dp[i] , dp[j]+1) ,尝试所有的s[j],但前提是:字符s[j]
含义:以si结尾的最长递增序列长度=max[以sj结尾的最长递增序列长度]+1
实现:
ps. 如果需要输出这个最长的子序列,可以用列表father[i]记录以s[i]结尾的最长递增序列中,s[i]的上一个元素的下标位置
只求解长度的代码:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
"""求最长递增子序列"""
maxLen = 1
# dp[i]代表以nums[i]结尾的最长递增子序列长度
dp = [1 for _ in range(len(nums))] # base case
for i in range(1, len(nums)):
for j in range(0, i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1) # 状态转移
maxLen = max(maxLen, dp[i]) # 更新最长长度
return maxLen
能够打印具体最长递增子序列的代码:
n = int(input())
s = list(input())
# dp[i]表示以s[i]结尾的最长递增序列的长度,初值为1
dp = [1 for _ in range(n)]
# father[i]记录以s[i]结尾的最长递增序列中,s[i]的上一个数字的下标,初值为s[i]本身
father = [idx for idx in range(n)]
maxLen=0
for i in range(n):
# 对于所有比s[i]小的s[j],考虑是否要把s[i]拼接到s[j]后面
for j in range(i):
if s[j]<s[i]:
if dp[j]+1>dp[i]:
# 有更好的选择
dp[i]=dp[j]+1
father[i]=j
else:
#保持原来的选择,dp[i]=dp[i]
pass
#若不关心最长序列的内容,上面的代码可直接写作dp[i] = max(dp[j]+1,dp[i])
# 更新最大长度
if dp[i]>maxLen:
maxLen = dp[i]
print('maxLen=',maxLen)
ans_seq=[]
while True:
ans_seq.append(s[i])
if i==father[i]:
#上一个元素是它本身,则结束
break
i=father[i]
#反向读入,正向输出
ans_seq=ans_seq[::-1]
print('seq= ',ans_seq)
=================== RESTART: C:\Users\13272\Desktop\最长递增子序列.py ==================
6
172548
maxLen= 4
seq= ['1', '2', '5', '8']
=================== RESTART: C:\Users\13272\Desktop\最长递增子序列.py ==================
# 此程序也适用于字符串
6
efabcd
maxLen= 4
seq= ['a', 'b', 'c', 'd']
上面方法的性能问题在于每次需要O(n)复杂度扫描字符s[i]之前小于它的哪些s[k]字符,联想到有序数组用二分查找提高查找效率的特点,我们可以改变dp的含义
如果不关心序列的内容,只需要求最长递增子序列的长度,
那么,另外一种思路是:
注意,这里可能会把s序列中后面的元素放到seq序列的中部,所以最终seq的内容不一定是那个最长递增子序列
实现:
ps. 若还需要输出这个最长递增子序列,可以另外用seqLen[i]记录:将s[i]添加到seq的末尾/替换seq中部的某个元素时,以s[i]结尾的递增子序列的长度;
最后i从[最长子序列长度]到1遍历,从后往前找出seqLen中第一个seqLen[idx]==i的下标值idx,并入队ans,最终逆序输出ans即可
只求解长度的代码:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
"""求最长递增子序列"""
def left_bound(ch):
"""在dp中二分查找第一个大于等于ch的元素(一定存在)"""
l, r = 0, len(dp) - 1 # 闭区间
while l <= r:
mid = (l + r) // 2
if dp[mid] == ch:
r = mid - 1
elif dp[mid] < ch:
l = mid + 1
elif dp[mid] > ch:
r = mid - 1
return l
dp = [] # dp[i]表示目前长度为i的最长递增子序列的末尾字符
for num in nums:
if len(dp) == 0 or dp[-1] < num:
# 拼接到末尾
dp.append(num)
else:
idx = left_bound(num)
dp[idx] = num
return len(dp)
能够打印具体最长递增子序列的代码:
def lower_bound(arr, target,low, high):
"""传入非递减序列arr,返回arr中第一个>=target的值的下标
其中,搜索范围为[low, high(包含)],若找不到返回-1"""
pos = -1
while low<high:
mid = (low+high)//2
if arr[mid] < target:
low = mid+1
else:#>=
high = mid
#pos = high
if arr[low]>=target:
pos = low
return pos
n = int(input())
s = list(map(int,input().split()))
seq = [s[0]]# 初始化
length=1
#若不输出最长递增子序列的内容,可不使用seqLen和ans
#将s[i]添加到seq的末尾/替换seq中部的某个元素时,以s[i]结尾的递增子序列的长度
#每个s[i]都对应一个seqLen值
seqLen=[1]#一开始s[0]对应的序列长度为1
ans=[]#记录最长序列的内容
for i in range(1,n):
# s[0]已经初始化到seq中,从s[1]开始
# 遍历每个s[i],查看s[i]和seq[-1]的关系
if s[i]>seq[-1]:
length+=1
seq.append(s[i])
seqLen.append(length)#当前的上升序列的长度是length
else:# s[i]<=seq[-1]:
#二分查找第一个大于或等于s[i]的元素,并替换
#这里不是“大于”而是“大于等于”,因为要防止seq中出现相同的元素
last_idx=lower_bound(seq,s[i],0, len(seq)-1)
seq[last_idx]=s[i]#last_idx可能是seq中的任何位置
seqLen.append(last_idx+1)#当前的上升序列的长度就是pos+1
# 最大长度就是seq序列的长度
print(length)
length_countDown=length
for i in range(len(seqLen)-1,-1,-1):
if length_countDown<=0:
break
if seqLen[i]==length_countDown:
ans.append(s[i])
length_countDown-=1
# 逆序输出这个最长递增序列
ans=ans[::-1]
print(ans)
=============== RESTART: C:\Users\13272\Desktop\二分查找 - 副本 - 副本.py ==============
5
5 2 3 3 4
3
[2, 3, 4]
LeetCode 354. 俄罗斯套娃信封问题
给出一堆信封的长、宽二元组,一个信封若长、宽都大于另一信封,就可被装入,求最多可以“套娃”装入多少信封
预处理并转化问题:最终需要求长、宽都单调递增的最长子序列,是二维LIS问题,可以先将长度递增排列,再求出宽度的最长递增子序列即可(转化为一维LIS问题)
注意细节:多个长度相同的信封,只能选一个,因此排序时首先按照长升序,长度相同应该按宽度降序排列,保证长度相同的信封中只有一个会被选上(至于选哪个,留给一维LIS解决,它会找出能构成最长递增子序列的答案)
class Solution:
def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
"""二维最长递增子序列LIS问题"""
# 首先按照长升序,长度相同应该按宽度降序排列
arr = sorted(envelopes, key=lambda x: (x[0], -x[1]))
h = [weightHeight[1] for weightHeight in arr] # 取出宽度
def lengthOfLIS(nums) -> int:
"""求一维LIS"""
...
# 套用一维LIS解法即可
ans = lengthOfLIS(h)
return ans
对于三维的LIS,即箱子的“套娃”问题,就不能再按照这种思路(先按照前两个维度排序,第三维求LIS),这类问题叫“偏序问题”,需要借助树状数组解决