跳到主要内容

BISHI133. 最长不下降子序列

题目描述​

给定由 n 个正整数组成的数组 A,请你计算该数组的最长不下降子序列 (Longest Non-decreasing Subsequence,简称 LNDS)长度。

对于一条子序列,要求保留原数组中的相对顺序,并满足子序列中的元素 依次不小于前一个元素。子序列为从原数组中删除任意个元素后得到的新数组。

输入格式​

第一行输入一个整数 n,代表数组的元素数量。 第二行输入 n 个整数 A[i],代表数组元素。

输出格式​

在一行上输出一个整数,表示最长不下降子序列的长度。

示例​

示例 1​

输入:

6
1 2 4 2 3 4

输出:

5

说明: 可以选择子序列 1, 2, 2, 3, 4,满足单调不降,长度为 5。

示例 2​

输入:

5
5 4 3 2 1

输出:

1

说明: 任意两个数都不满足不降关系,最长只能取单个元素,长度为 1。


解题思路​

第一步:理解问题本质​

「子序列」不要求连续,只要求保持相对顺序。要在所有子序列里找一条 最长的、元素不递减的。这是一个经典的动态规划问题。

第二步:O(n²) 动态规划​

设 dp[i] 表示「以 A[i] 结尾的最长不下降子序列长度」。 转移时枚举它前面所有不超过它的元素:

for i in range(n):
dp[i] = 1
for j in range(i):
if A[j] <= A[i]:
dp[i] = max(dp[i], dp[j] + 1)

答案是 max(dp)。正确但 O(n²),n 较大时会超时。

第三步:贪心 + 二分 O(n log n)​

维护一个辅助数组 tails,其中 tails[k] 表示「长度为 k+1 的 不下降子序列的最小可能结尾」。它天然是非递减的,可以二分。

遍历每个元素 x:

  • 用 bisect_right(tails, x) 找到第一个严格大于 x 的位置 pos;
  • 若 pos == len(tails),说明 x 能接在当前最长序列后面,追加;
  • 否则用 x 替换 tails[pos](让结尾更小,未来更容易接上)。

遍历结束后 len(tails) 就是答案。

为什么用 bisect_right 而不是 bisect_left? 因为是不下降(允许相等)。对于严格上升子序列要用 bisect_left, 两者差一个等号,这是本题最容易写错的地方。


完整代码实现​

算法可视化 · 逐帧回放这道题的代码18 帧 · 录制输入
import sys
from bisect import bisect_right


def solve() -> None:
"""O(n log n) 求最长不下降子序列长度"""
data = sys.stdin.read().split()
if not data:
return
n = int(data[0])
arr = list(map(int, data[1:1 + n]))

tails = [] # tails[k] = 长度 k+1 的不下降子序列的最小结尾
for x in arr:
# 不下降:找第一个「大于 x」的位置替换(相等可以接在后面)
pos = bisect_right(tails, x)
if pos == len(tails):
tails.append(x)
else:
tails[pos] = x
sys.stdout.write(str(len(tails)) + "\n")


if __name__ == "__main__":
solve()

示例推演​

以 1 2 4 2 3 4 为例,tails 的变化:

处理 xbisect_right 结果tails
1空 → 追加[1]
21 → 追加[1, 2]
42 → 追加[1, 2, 4]
22 → 替换(4 换成 2)[1, 2, 2]
33 → 追加[1, 2, 2, 3]
44 → 追加[1, 2, 2, 3, 4]

最终 len(tails) = 5,与答案一致。


复杂度分析​

解法时间复杂度空间复杂度说明
O(n²) DPO(n²)O(n)直观,n 大时超时
贪心 + 二分O(n log n)O(n)推荐

易错点总结​

1. 二分用错(严格上升 vs 不下降)​

不下降用 bisect_right,严格上升用 bisect_left。 写反了,「相等元素」就会被当成下降,答案偏小。

2. tails 不是答案序列​

tails 里存的只是「各长度的最小结尾」,它本身不一定是一条合法的 子序列(元素可能来自不同位置)。题目只问长度,所以没问题; 但不要把它当成答案直接输出。

3. 初始长度​

tails 从空数组开始,遇到第一个元素直接追加,天然得到长度 1。


扩展思考​

  • 把 bisect_right 换成 bisect_left 就变成「最长严格上升子序列」。
  • 要求输出具体的子序列时,需要在替换时记录每个位置的前驱。
  • 「最长下降子序列」只需把数组取负,再套同样的模板。

相关题目​

自测​

加载评论中...