LIS:最长上升子序列

· · 算法·理论

Update on 2026.9.13:新增了 5 道例题。

本来想加 [ABC360G] Suitable Edit for LIS 的,但是这题似乎没有只用 LIS 就能解决的方案,题解区里唯一只用 LIS 的题解还被 hack 了。算鸟不加了,给感兴趣的读者食用。

LIS,即最长上升子序列。相信你们都会 O(n^2) 了,这里不讲了。这里讲 O(n\log n)。

LIS 例题

注意下面的做法是严格上升。

我们来考虑一个状态 f_i,表示长度为 i 的最长上升子序列的最后一项最小为多少。

显然越小答案越优,所以这是没问题的。我们考虑现在的最长上升子序列长为 m。现在考虑 A_k。那么:

使用 lower_bound 可更简洁地找到这个 j。

int n, cnt = 0;
int f[100010];

int main() {
    scanf("%d", &n);
    for (int i = 1, x ; i <= n ; i++) {
        scanf("%d", &x);
        if (x > f[cnt]) f[++cnt] = x;
        else f[lower_bound(f + 1, f + cnt + 1, x) - f] = x;
    }
    printf("%d\n", cnt);
    return 0;
}

:::warning[注意]{open} 当 A_i 不是正数时,f_i 初值要设为负无穷。 :::

现在,我们把它运用在题目当中。

递增

贪心。显然挑出一个 LIS,把剩下的修改就一定可以满足。证明:无需证明。

所以答案就是 n 减最长上升子序列的长度。

金币收集

由于从左往右走,所以考虑按地点排序。直接考虑有点困难,但注意到小 A 站原地越久越容易丢失越多的金币,所以考虑设 p_i 为小 A 到 i 号金币处前,最多在其他地方不动多久。那么如果原地不动时间超过 p_i,就领不到了。

显然 p_i = t_i - x_i。那么发现一个很重要的结论:答案就是以 0 为首项的 p 的最长不降子序列!

该结论不必多说,只是不好发现。

注意:

:::success[Code]

#include <bits/stdc++.h>
using namespace std;

int n;
pair <int, int> c[100010];
int f[100010], cnt = 0;

int main() {
    scanf("%d", &n);
    for (int i = 1 ; i <= n ; i++)
        scanf("%d%d", &c[i].first, &c[i].second);
    sort(c + 1, c + n + 1);
    for (int i = 1 ; i <= n ; i++) {
        int x = c[i].second - c[i].first;
        if (x < 0) continue;
        if (f[cnt] <= x)
            f[++cnt] = x;
        else f[upper_bound(f + 1, f + cnt + 1, x) - f] = x;
    }
    printf("%d\n", cnt);
    return 0;
}

:::

[ABC134E] Sequence Decomposing

显然这题要求最少的上升子序列,使所有数都被覆盖。

结论:答案为最长不升子序列长度。

这也就是并非大名鼎鼎的 Dilworth 定理:将一个序列剖成若干个单调上升子序列的最少个数等于该序列最长不升子序列的长度。

:::info[有趣的事情]{open}

证明考虑贪心。设 f_i 表示第 i 个子序列的末项,对于一个为 x 的数,我们让满足 f_i < x 的最小的 i 来接这个 x。如果不存在这样的 i 就再创一个子序列来接这个 x。

显然这样构造一定是最优的,可以直接二分求解。其实这个过程就是在求最长不升子序列(当然没意识到也无所谓),于是做完了。

小 trick:求降序数组的大于 x 的位置,可以用 upper_bound(f + 1, f + cnt + 1, x, greater <int> ()) 实现。注意初始化 f_i 为正无穷。

:::success[Code]

#include <bits/stdc++.h>
using namespace std;

int n;
int f[100010], cnt = 0;

int main() {
    scanf("%d", &n);
    memset(f, 127, sizeof(f));
    for (int i = 1, x ; i <= n ; i++) {
        scanf("%d", &x);
        if (f[cnt] >= x)
            f[++cnt] = x;
        else f[upper_bound(f + 1, f + cnt + 1, x, greater <int> ()) - f] = x;
    }
    printf("%d\n", cnt);
    return 0;
}

:::

导弹拦截

结论:第一问为最长不升子序列;第二问为最少不升子序列数量,也就是最长上升子序列。

这和上面的那题不一样的吗?双倍经验。

[ABC393F] Prefix LIS Query

回忆一下 LIS 的求法。可以发现,当前 i 个数均被插入时,所有数字都小于等于 X 的 LIS 长度为最小满足 f_i \le X 的 i。

所以如果 R_i 能单调不降,该有多好啊!这样我们就可以通过将 A_i 按顺序插入来求出所有查询啦!

发现查询之间互相独立,考虑把它们离线下来按 R_i 排序,做完了。

:::success[Code]

#include <bits/stdc++.h>
using namespace std;

int n, q, cnt = 0;
int a[200010], c[200010], ans[200010];
vector <pair <int, int>> qu[200010];

int main() {
    scanf("%d%d", &n, &q);
    for (int i = 1 ; i <= n ; i++) scanf("%d", &a[i]);
    for (int r, x, i = 1 ; i <= q ; i++)
        scanf("%d%d", &r, &x), qu[r].push_back({x, i});
    for (int i = 1 ; i <= n ; i++) {
        if (a[i] > c[cnt]) c[++cnt] = a[i];
        else c[lower_bound(c + 1, c + cnt + 1, a[i]) - c] = a[i];
        for (auto y : qu[i])
            ans[y.second] = upper_bound(c + 1, c + cnt + 1, y.first) - c - 1;
    }
    for (int i = 1 ; i <= q ; i++)
        printf("%d\n", ans[i]);
    return 0;
}

:::

[ABC354F] Useless for LIS

我们希望求出以下值:

此时只要有 f_i+g_i-1=K,那么 i 就是一个正确的答案。

:::success[Code] ``` #include <bits/stdc++.h> using namespace std; int T, n, cnt = 0; int a[200010], c[200010]; int f[200010], g[200010]; int main() { scanf("%d", &T); for ( ; T-- ; ) { scanf("%d", &n); cnt = 0; for (int i = 0 ; i <= n ; i++) c[i] = -2e9; // 注意是 -inf 不是 0,因为用了下面的 trick a[i] 会小于 0 for (int i = 1 ; i <= n ; i++) { scanf("%d", &a[i]); f[i] = lower_bound(c + 1, c + cnt + 1, a[i]) - c; if (a[i] > c[cnt]) c[++cnt] = a[i]; else c[f[i]] = a[i]; } for (int i = 0 ; i <= n ; i++) c[i] = -2e9; cnt = 0; for (int i = n ; i ; i--) { a[i] = -a[i]; // trick: 倒着求原数的相反数,LIS 长不变 g[i] = lower_bound(c + 1, c + cnt + 1, a[i]) - c; if (a[i] > c[cnt]) c[++cnt] = a[i]; else c[g[i]] = a[i]; } vector <int> ans; for (int i = 1 ; i <= n ; i++) if (f[i] + g[i] - 1 == cnt) ans.push_back(i); printf("%u\n", ans.size()); for (auto i : ans) printf("%d ", i); puts(""); } return 0; } ``` ::: - - - ### [[ABC468F] Chmax](https://www.luogu.com.cn/problem/AT_abc468_f) 注意审题,我赛时就是看错题了。~~我不会告诉你就算看对题了我也不会做的。~~ 一个错误的想法就是求两个 LIS,因为强制做操作,导致前缀最大值会添乱。想想 LIS 为什么不能贪心做就知道了。 发现 $\max(x,y)=\max_{j=1}^{i}P_j$,所以如果现在遇到了前缀最大值,可以直接对 $x,y$ 较大值进行更新。所以可以将**前缀最大值**单独考虑。这里将前缀最大值对 $x$ 更新,所以 $x$ 一直为前缀最大值。 现在对 $x$ 更新的数字定下来了,我们发现,更新 $y$ 的数字就是**剩下数字的 LIS**。因为不在 LIS 的数字可以直接去更新 $x$ 然后浪费,也就是不操作。 :::info[感性理解正确性]{open} 前缀最大值无论更新 $x$ 还是 $y$ 都会对答案产生贡献,只需要使其他数最优即可。其他数最优就是取它们的 LIS。 ::: :::success[Code] ``` #include <bits/stdc++.h> using namespace std; int n, cnt = 0, ans = 0; int a[500010], c[500010]; int main() { scanf("%d", &n); int x = 0; for (int i = 1 ; i <= n ; i++) { scanf("%d", &a[i]); if (a[i] > x) x = a[i], ++ans, a[i] = 0; } for (int i = 1 ; i <= n ; i++) if (a[i]) { if (a[i] > c[cnt]) c[++cnt] = a[i]; else c[lower_bound(c + 1, c + cnt + 1, a[i]) - c] = a[i]; } printf("%d\n", cnt + ans); return 0; } ``` ::: - - - 希望本文章对你们有所帮助,同时欢迎各位分享一些有趣的 LIS 题目。