04_Longest Increasing Subsequence 最长递增子序列

最长递增子序列

Authors: Michael Cao, Benjamin Qi, Andi Qu, Andrew Wang Contributors: Dong Liu, Siyong Huang, Aryansh Shrivastava, Kevin Sheng, Katja Frantzen


先决条件

  • Gold - Introduction to DP (动态规划简介)

教程

AA 为我们要寻找最长递增子序列(LIS)的数组。


慢速解法

时间复杂度: O(N2)O(N^2)

dp[i]dp[i] 表示以 A[i]A[i] 结尾的最长递增子序列的长度。我们可以通过简单的方法在 O(N2)O(N^2) 时间内计算 dpdp(从而得到 LIS):

from typing import List


def find_lis(a: List[int]) -> int:
    lis = 0
    dp = [1] * len(a)
    for i in range(len(a)):
        for j in range(i):
            if a[j] < a[i]:
                dp[i] = max(dp[i], dp[j] + 1)
        lis = max(lis, dp[i])
    return lis

然而,我们可以做得更好!


快速解法

时间复杂度: O(NlogN)O(N \log N)

LiL_i 是一个数组(0 索引),其中 Li[j]L_i[j] 是从 AA 的前 ii 个元素中最小的一个元素,它以长度为 j+1j+1 的递增序列结束(如果没有这样的元素则为 \infty)。

引理 1: LiL_i 形成一个严格递增的序列。

引理 2: 以 A[i+1]A[i+1] 结尾的最长递增子序列的长度等于满足 Li[j]A[i+1]L_i[j] \ge A[i+1] 的最小索引 jj

引理 3: LiL_iLi+1L_{i+1} 之间最多有 1 个元素不同。

要在 O(logN)O(\log N) 时间内找到并更新描述的 jj,我们可以使用带二分查找的列表:

from typing import List
from bisect import bisect_left


def find_lis(arr: List[int]) -> int:
    min_endings = []
    for i in arr:
        pos = bisect_left(min_endings, i)
        if pos == len(min_endings):  # we can have a new, longer increasing subsequence!
            min_endings.append(i)
        else:  # oh ok, at least we can make the ending element smaller
            min_endings[pos] = i
    return len(min_endings)

RMQ 数据结构解法

时间复杂度: O(NlogN)O(N \log N),但可能比之前的解决方案慢一个常数因子。

dp[k]dp[k] 为以元素 kak \in a 结尾的 aa 的 LIS 长度。我们的基本情况是 dp[a[0]]=1dp[a[0]] = 1。由于 LIS 具有最优子结构特性,我们按以下方式进行转换:

dp[t]=max0k<tdp[k]+1dp[t] = \max_{0 \le k < t} dp[k] + 1

如果 dpdp 是一个 RMQ 数据结构,dp[t]dp[t] 的变化只是一个点更新,而 max0k<tdp[k]\max_{0 \le k < t} dp[k] 的计算是一个区间查询。


应用 1 - 不相交的线段

首先,如果两个线段 (l1,r1)(l_1, r_1)(l2,r2)(l_2, r_2) 相交(假设 l1<l2l_1 < l_2),我们可以注意到 l1<l2r1>r2l_1 < l_2 \implies r_1 > r_2

这意味着一组不相交的线段对于所有对 (i,j)(i, j) 都满足 li<ljri<rjl_i < l_j \implies r_i < r_j

AA 是一个数组,其中 A[i]=xA[i] = x 表示位于位置 ii 的右端点的线段的左端点位于位置 xx

如果我们被要求找出不相交线段集的最大大小,答案将是 AA 的最长递增子序列!


应用 2 - 最少递增子序列数

引理(简单): 覆盖 AA 所需的最少递增子序列数至少等于 AA 最长非递增子序列的长度。

命题: 覆盖 AA 所需的最少递增子序列的数量等于 AA 最长非递增子序列的长度!

证明: 设 fif_i 表示以 AiA_i 结尾的最长非递增子序列的长度。那么对于固定的 tt,满足 fi=tf_i = tAiA_i 对于每个 tt 都是一个递增子序列。所以我们用(最长非递增子序列的长度)个递增子序列覆盖了 AA

另一种证明: 这只是 Dilworth 定理的一个特例。


示例 - PCB

Focus Problem: 在继续之前,请尽力解决这个问题!

这个问题要求我们找到最小数量的不交线段集。

from bisect import bisect_left

n = int(input())
board = []
endpoints = []
for _ in range(n):
    l, r = map(int, input().split())
    board.append((l, r))
board.sort()

endpoints.append(board[0][1])
for x in range(1, n):
    v = board[x][1]
    if v < endpoints[-1]:
        endpoints.append(v)
    else:
        index = bisect_left(
            [-x for x in endpoints], -v
        )  # invert list in order to use bisect_left
        endpoints[index] = v
print(len(endpoints))

总结

解法 时间复杂度 空间复杂度
暴力 DP O(N2)O(N^2) O(N)O(N)
二分查找优化 O(NlogN)O(N \log N) O(N)O(N)
RMQ/线段树 O(NlogN)O(N \log N) O(N)O(N)

经典问题

题目 难度 描述
Increasing Subsequence Easy 计算最长递增子序列长度
PCB Hard 不相交线段集问题

04_Longest Increasing Subsequence 最长递增子序列
https://mingsm17518.github.io/2026/04/30/算法学习/05_Gold/02_动态规划/04_Longest Increasing Subsequence 最长递增子序列/
作者
Ming
发布于
2026年4月30日
更新于
2026年9月13日
许可协议