25年南大智科预推免考察了该知识点 要求用py
LIS 最长上升子序列
LIS = Longest Increasing Subsequence,最长上升子序列
子序列:可以跳过元素,不用连续,但元素相对顺序不能变。
例子:[1,3,2,4] LIS:1,3,4 或者 1,2,4,长度是 3
两个版本
- 严格上升:后一个 > 前一个(LIS 常用)
- 非递减:后一个 ≥ 前一个
暴力 O (n²) DP 写法(考试常考)
dp[i]:以第 i 个元素结尾的 LIS 长度
nums = [1,3,2,4]
n = len(nums)
dp = [1]*n
for i in range(1,n):
for j in range(i): # j 在 i前面,j∈[0,i‑1]
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j]+1)
print(max(dp))
dp 数组过程:
- dp0=1
- i=1,j=0:nums0 < nums1 → dp1=2
- i=2,j=0:满足;j=1:3 不小于 2 → dp 2=2
- i=3,j=0,1,2 都满足 → dp 3=3 max (dp)=3
优化:二分+贪心
核心思想:维护一个数组 tails,tails[k] 表示长度为 k+1 的上升子序列,末尾最小的元素 。 同样求严格最长上升子序列 (nums[j] < nums[i])
python
import bisect
def length_of_lis(nums):
tails = []
for x in nums:
# 在tails里找第一个 >= x 的位置
idx = bisect.bisect_left(tails, x)
if idx == len(tails):
tails.append(x)
else:
tails[idx] = x
return len(tails)
print(length_of_lis([1,3,2,4])) #输出3
bisect_left(a, x):在有序升序数组a里,返回可以插入 x,还能保持数组升序的最靠左的下标 。等价找第一个 ≥ x 的元素的下标 ;如果数组里所有元素都 < x,返回数组长度。idx 代表:把 x 放到 tails 的 idx 位置,代表以 x 结尾的 LIS 长度就是 idx
+1。
python
if idx == len(tails):
tails.append(x)
idx等于 tails 数组长度,说明当前 x 比 tails 里面所有元素都大。
tails 数组的含义:tails[k] = 长度为k+1的上升子序列的最小末尾值。
x 比全部末尾都大,说明可以把 x 接在当前最长子序列后面,生成更长的子序列。 →直接把 x 追加到 tails 末尾。
例子:tails=[1,3],x=4,idx=2 == len(tails)=2,执行append(4),tails 变成[1,3,4]。
python
else: tails[idx] = x
含义:数组中找到了某个位置idx,该处元素≥x。 我们要更新 tails idx 为更小的 x。
目的:同样长度的上升子序列,把末尾变得更小,方便后面来的数字更容易接上去。 ⚠注意:这一步不增加 LIS 总长度,只是优化后续的条件。
进阶:山峰序列
除了标准的 LIS 问题,还有一种变体要求寻找序列中"先严格递增后严格递减"的最长子序列(即山峰型序列)。这类问题可以通过分别计算正向和反向的 LIS 来求解。
python
def solve():
n = int(input())
if n == 0:
return 0
a = list(map(int, input().split()))
# dp_inc[i]: 以位置 i 结尾的最长严格递增子序列长度
dp_inc = [1] * n
for i in range(1, n):
for j in range(i):
if a[j] < a[i]:
dp_inc[i] = max(dp_inc[i], dp_inc[j] + 1)
# dp_dec[i]: 从位置 i 开始的最长严格递减子序列长度
dp_dec = [1] * n
for i in range(n-2, -1, -1):
for j in range(i+1, n):
if a[i] > a[j]:
dp_dec[i] = max(dp_dec[i], dp_dec[j] + 1)
# 找到最长的山形子序列
max_length = 0
for i in range(n):
length = dp_inc[i] + dp_dec[i] - 1
max_length = max(max_length, length)
return max_length
# 读取输入并输出结果
print(solve())
优化 O (n log n)
维护一个数组,贪心 + 二分,适合 n 很大的情况。
python
from bisect import bisect_left
def get_lis_lengths(a):
"""
ans[i] 表示以 a[i] 结尾的最长严格递增子序列长度。
"""
tails = []
ans = []
for x in a:
pos = bisect_left(tails, x)
if pos == len(tails):
tails.append(x)
else:
tails[pos] = x
ans.append(pos + 1)
return ans
def solve():
n = int(input())
a = list(map(int, input().split()))
# 以每个位置结尾的最长严格递增子序列
dp_inc = get_lis_lengths(a)
# 反转后求 LIS,对应原数组中从每个位置开始的严格递减子序列
dp_dec = get_lis_lengths(a[::-1])[::-1]
answer = 0
for i in range(n):
answer = max(answer, dp_inc[i] + dp_dec[i] - 1)
print(answer)
solve()
易混区分
-
子序列 (subsequence):不要求连续,LIS 就是它
-
子串 (subarray/substring):必须连续!最长上升子串和 LIS 不一样。
pos = bisect_left(tails, x)
在有序列表 tails 中查找 x 应该插入的最左位置
在有序数组 a 中,返回第一个大于等于 x的下标。
使用 bisect_left 可以保证处理的是严格递增子序列:
-
遇到相同元素时,替换原位置。
-
不会因为相同元素而增加 LIS 长度。
a[::-1]
Python 切片,表示把列表反转。
完整切片格式是:
a[start:stop:step]
这里步长是 -1,所以从后向前取:
[1, 2, 3][::-1]
结果是:
[3, 2, 1]
后面的:
get_lis_lengths(a[::-1])[::-1]
意思是:
- 反转原数组。
- 计算反转数组中每个位置的 LIS 长度。
- 再把计算结果反转回来。
- 得到原数组中从每个位置开始的最长严格递减子序列长度。