第 4 篇 · LIS 与序列结构
本篇例题(编号沿用全册题单):
编号 题目 来源 主考 G 导弹拦截 洛谷 P1020 最长不升子序列 + Dilworth 定理 H 友好城市 洛谷 P2782 排序 + 严格 LIS I Why Did the Cow Cross the Road II P 洛谷 P3657 稀疏候选 + 降序枚举求 LIS 本篇与前三篇的唯一差别:树状数组(BIT,Fenwick 树)里存的从"前缀和"换成"前缀最大值"。和可减,max 不可减------所以这里所有写法只能"只增不减",7.4 节专讲。
第七章 LIS 的 O(nlogn)O(n\log n)O(nlogn)
7.1 朴素 DP:两个维度只能省一个
定义 :下标 i1<⋯<iki_1<\dots<i_ki1<⋯<ik 且 ai1<⋯<aika_{i_1}<\dots<a_{i_k}ai1<⋯<aik,称为上升子序列;最长者长度即 LIS 长度。"不降""不升""下降"只换不等号。
fi=1+max{fj: j<i, aj<ai},Ans=max1≤i≤nfif_i=1+\max\{f_j:\ j<i,\ a_j<a_i\},\qquad \text{Ans}=\max_{1\le i\le n}f_ifi=1+max{fj: j<i, aj<ai},Ans=1≤i≤nmaxfi
fif_ifi 是以 iii 结尾 的最长上升子序列长度。朴素转移 O(n2)O(n^2)O(n2),n=105n=10^5n=105 时 5×1095\times10^95×109 次比较,必挂。
式子里两个条件:j<ij<ij<i 是下标维 (已扫过的前缀),aj<aia_j<a_iaj<ai 是值域维 。所有 O(nlogn)O(n\log n)O(nlogn) 做法都在干同一件事:让扫描顺序自动满足 j<ij<ij<i,把值域维交给数据结构------二分法交给数组,BIT 法交给树状数组。
7.2 二分法:tails 数组
tails[k] = 长度为 kkk 的上升子序列中结尾元素的最小值,它关于 kkk 严格递增 (否则长度 k+1k+1k+1 那条的前 kkk 项会给出结尾更小的长度 kkk 子序列,矛盾),故可二分。
找第一个 ≥ai\ge a_i≥ai 的位置 ppp,令 tails[p] = a[i];ppp 超出当前长度则长度加一,最终长度即答案。严格上升用 lower_bound,不降用 upper_bound------选错这一个词答案就错。
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 500005;
int n, a[N], t[N], len;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) {
int p = lower_bound(t + 1, t + len + 1, a[i]) - t; // 求"不降"改成 upper_bound
t[p] = a[i];
if (p > len) len = p;
}
printf("%d\n", len);
}
局限很明确:只回答长度,不回答"以每个位置结尾的最优值"。
7.3 BIT 法:下标交给扫描顺序,值域交给树状数组
扫到 iii 时,BIT 里只有 j<ij<ij<i 的 fjf_jfj。把 fjf_jfj 放在下标 aja_jaj(离散化成排名 rj∈1,mr_j\in1,mrj∈1,m)处,则 max{fj: aj<ai}=query(ri−1)\max\{f_j:\ a_j<a_i\}=\text{query}(r_i-1)max{fj: aj<ai}=query(ri−1),转移只剩三步:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 500005;
int n, m, a[N], b[N], c[N];
#define lowbit(x) ((x) & -(x))
void update(int x, int v) { for (; x <= m; x += lowbit(x)) c[x] = max(c[x], v); } // 只取 max
int query(int x) { int r = 0; for (; x > 0; x -= lowbit(x)) r = max(r, c[x]); return r; }
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); b[i] = a[i]; }
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - b - 1;
int ans = 0;
for (int i = 1; i <= n; i++) {
int r = lower_bound(b + 1, b + m + 1, a[i]) - b; // 1-based 排名
int f = query(r - 1) + 1; // 严格上升:只查 < a[i]
update(r, f);
ans = max(ans, f);
}
printf("%d\n", ans);
}
update 里写的是 c[x] = max(c[x], v) 而非 c[x] = v(理由见 7.4);离散化只改下标不改答案,m≤nm\le nm≤n,BIT 按 mmm 开;时间 O(nlogn)O(n\log n)O(nlogn),空间 O(n)O(n)O(n)。它比二分法多的价值在于:给出每个 fif_ifi,且查询范围换成"任意区间"或"树上子树"时只需改边界(第 5 篇)。
7.4 前缀 max 只能"只增不减"
下标 xxx 管辖 (x−lowbit(x), x](x-\mathrm{lowbit}(x),\ x](x−lowbit(x), x](lowbit(x)=x & (−x)\mathrm{lowbit}(x)=x\ \&\ (-x)lowbit(x)=x & (−x)),c[x] 是这段区间的摘要。求和能随便改,因为加法可逆(add(x,-v) 就还原);max 没有逆运算 :update(x,v) 沿 x+=lowbit(x)x\mathrel{+}=\mathrm{lowbit}(x)x+=lowbit(x) 把 vvv 揉进 O(logn)O(\log n)O(logn) 个区间的摘要,而你并没存区间里其它元素的值------改小、删除、撤销全都做不到。
三句话:① 前缀 max 的 BIT 只支持把某个位置的值变大;② 不支持赋值、删除、撤销;③ 用之前必须验证"只增不减"。
LIS 恰好满足:fif_ifi 算出后永不再改,写入是"在 rir_iri 处取 max"。反过来,求方案数 (max 丢掉"并列最优有几个")、动态改序列 (改小或删除)、回滚这三类需求不能硬套。
7.5 什么时候换成区域最值线段树
口诀:修改只会让值变大,用 BIT;修改可能让值变小,用线段树。
| 需求 | 选谁 | 原因 |
|---|---|---|
| 单点取 max(只增)+ 前缀 max | 树状数组 | 常数小、代码两行 |
| 单点赋值(可增可减)+ 区间 max | 区域最值线段树 | 叶子存原值,回溯重算 |
| 单点赋值 + 前缀 max | 区域最值线段树 | BIT 的摘要无法重算 |
| 需要撤销 / 回滚 | 线段树或栈式回滚 | max 不可逆 |
"区域最值线段树"即第二行:modify 递归到叶子赋值 t[k]=v,回溯时 t[k]=max(t[2k],t[2k+1]);ask 同为 O(logn)O(\log n)O(logn)。多一个常数、4 倍内存,换来"任意赋值 + 任意区间";本篇三道题全是"取 max",故全程用 BIT。
7.6 四种严格性对照表(抄在草稿纸上)
rir_iri 是升序排名,ri′=m+1−rir'_i=m+1-r_iri′=m+1−ri 是反转排名。
| 要求的子序列 | 转移条件 | BIT 写法 |
|---|---|---|
| 严格上升 | aj<aia_j<a_iaj<ai | 原排名,query(r-1) |
| 不降 | aj≤aia_j\le a_iaj≤ai | 原排名,query(r) |
| 不升 | aj≥aia_j\ge a_iaj≥ai | 反转排名,query(r') |
| 严格下降 | aj>aia_j>a_iaj>ai | 反转排名,query(r'-1) |
记忆法:"严格"就减一 ;"升变降"就反转排名(把后缀 max 翻成前缀 max)。两条规则独立,可叠加。
例题 G 导弹拦截(洛谷 P1020)
难度 :进阶 主考:最长不升子序列 + Dilworth 定理
题意
依次给出若干枚导弹的高度(正整数,空格分隔,读到文件结束;n≤105n\le 10^5n≤105)。一套系统首发任意高度,之后每发不得高于前一发。第一问:一套系统最多拦多少枚?第二问:拦下全部,至少几套?样例:389 207 155 300 299 170 158 65 → 6 和 2。
触发点
- n≤105n\le10^5n≤105:朴素 O(n2)O(n^2)O(n2) 的"以 iii 结尾"DP 是 5×1095\times10^95×109,必挂。
- 转移只依赖"前面值不小于我的位置"------典型的"扫描 + 值域取最值";第二问是"最少分成几组",先想 Dilworth。
★关键结论
第一问 :gig_igi 为以 iii 结尾的最长不升子序列长度,
gi=1+max{gj: j<i, aj≥ai}g_i=1+\max\{g_j:\ j<i,\ a_j\ge a_i\}gi=1+max{gj: j<i, aj≥ai}
是 ≥\ge≥ 不是 >>>:同高度可以连续拦截,所以是不升 不是严格下降。离散化得排名 rir_iri 后 aj≥ai ⟺ rj≥ria_j\ge a_i\iff r_j\ge r_iaj≥ai⟺rj≥ri,这是后缀 max;取反转排名 ri′=m+1−rir'_i=m+1-r_iri′=m+1−ri,条件变为 rj′≤ri′r'_j\le r'_irj′≤ri′,就成了前缀 max(含相等):gi=1+query(ri′)g_i=1+\text{query}(r'_i)gi=1+query(ri′)。
第二问 :最少系统数 = 最长严格上升子序列长度。推理链:
- 一套系统拦下的序列必须不升 (同高度可接着打),故"用 kkk 套拦下全部" ⟺ \iff⟺ "把原序列划分成 kkk 个不升子序列"。
- 定义偏序 i≺j ⟺ i<ji\prec j\iff i<ji≺j⟺i<j 且 ai≤aja_i\le a_jai≤aj:链 是下标升、值不降,即不升子序列 ;反链 是下标升、值严格上升,即严格上升子序列。
- 序列上的 Dilworth 定理:最少链覆盖数 = 最长反链长度 。故第二问 === 最长严格上升子序列长度,hi=1+query(ri−1)h_i=1+\text{query}(r_i-1)hi=1+query(ri−1)。
为什么不能靠贪心模拟糊过去:贪心(每枚放进"能接住且末尾最高"的系统)确实最优,但要高效实现得维护有序 multiset 并二分,正确性还得单独论证;Dilworth 直接把它翻译成第二个 LIS,模板复用、无需再证。
步骤
- 读到 EOF,高度存进
a[1..n];排序去重得值域 1,m1,m1,m,lower_bound求每个 rir_iri。 - 第一问:清空 BIT,下标用 ri′=m+1−rir'_i=m+1-r_iri′=m+1−ri,
f = query(r'_i) + 1、update(r'_i, f)、取 max。 - 第二问:清空 BIT,下标用 rir_iri,
f = query(r_i - 1) + 1、update(r_i, f)、取 max,输出两行答案。
代码骨架
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, m, a[N], rk[N], b[N], c[N];
#define lowbit(x) ((x) & -(x))
void update(int x, int v) { for (; x <= m; x += lowbit(x)) c[x] = max(c[x], v); }
int query(int x) { int r = 0; for (; x > 0; x -= lowbit(x)) r = max(r, c[x]); return r; }
int solve(bool strictUp) { // true: 严格上升(第二问)
for (int i = 1; i <= m; i++) c[i] = 0;
int ans = 0;
for (int i = 1; i <= n; i++) {
int x = strictUp ? rk[i] : m + 1 - rk[i]; // 不升 -> 反转排名求不降
int f = query(x - (strictUp ? 1 : 0)) + 1; // 不升时含相等,故不减 1
update(x, f);
ans = max(ans, f);
}
return ans;
}
int main() {
n = 0;
int x;
while (scanf("%d", &x) == 1) a[++n] = x; // 本题输入不定长,读到 EOF
for (int i = 1; i <= n; i++) b[i] = a[i];
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - b - 1;
for (int i = 1; i <= n; i++) rk[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
printf("%d\n%d\n", solve(false), solve(true));
return 0;
}
复杂度
时间 O(nlogn)O(n\log n)O(nlogn)(排序 + 两遍 BIT),空间 O(n)O(n)O(n)。
易错点
- 第一问写成严格下降 :条件是 aj≥aia_j\ge a_iaj≥ai(含相等)。样例里没有相等元素,看不出区别;输入改成
155 155立刻暴露。 - 反转排名后仍写
query(x-1):把相等排除了,第一问偏小。含相等必须是query(x)。 - 两问共用一个
c[]只清一次 :第二问必须在清零后的 BIT 上跑(写在solve里就是防这个)。 - 输入读错 :第一行是高度本身 ,不是个数。
cin >> n会把第一个高度当成 nnn,要用while (scanf("%d", &x) == 1)。
例题 H 友好城市(洛谷 P2782)
难度 :基础 主考:排序 + 严格 LIS
题意
一条河,南北两岸各有一排城市,坐标给出(同岸坐标互不相同)。给出 nnn 对友好城市 (xi,yi)(x_i,y_i)(xi,yi):xix_ixi 北岸坐标,yiy_iyi 南岸坐标。每对之间建一座桥,桥不能相交,问最多建几座。n≤5000n\le 5000n≤5000。样例见题面。
触发点
"配对 + 不能相交"是标准的二维同时递增 约束,看到就该想到"排序 + LIS";n≤5000n\le5000n≤5000 时 O(n2)O(n^2)O(n2) 也能过,但别因此跳过建模------例题 I 就是它的 10510^5105 版本。
★关键结论
把每座桥看成点 (xi,yi)(x_i,y_i)(xi,yi)。设 xi<xjx_i<x_jxi<xj,则两桥不相交 ⟺ yi<yj\iff y_i<y_j⟺yi<yj:yi<yjy_i<y_jyi<yj 时两岸先后顺序一致,不可能相交;yi>yjy_i>y_jyi>yj 就是"左岸靠左连右岸靠右、左岸靠右连右岸靠左",必然交叉。
于是"选一组互不相交的桥" ⟺ \iff⟺ "按 xxx 排序后选一个 yyy 严格递增的子序列",答案是排序后 yyy 序列的严格 LIS 长度。
排序后为什么不再交叉:① 排序只重排枚举顺序,不改变"选哪些桥";② 排序后任取 i<ji<ji<j 必有 xi<xjx_i<x_jxi<xj(同岸坐标互不相同);③ 于是"不相交"的唯一条件落到 yyy 上,yi<yjy_i<y_jyi<yj------二维几何约束被压成一维大小关系 。本题 yyy 两两不同,严格 LIS 与不降 LIS 同长;写 query(r-1) 更稳,不依赖这一点。
步骤
- 读入 nnn 对,按 xxx 升序排序。
- 对排序后的 yyy 离散化得排名 ri∈1,mr_i\in1,mri∈1,m,依次
f = query(r_i - 1) + 1、update(r_i, f)、取 max,输出。
代码骨架
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 5005;
int n, m, c[N], b[N], rk[N];
struct Node { int x, y; } p[N];
#define lowbit(t) ((t) & -(t))
void update(int x, int v) { for (; x <= m; x += lowbit(x)) c[x] = max(c[x], v); }
int query(int x) { int r = 0; for (; x > 0; x -= lowbit(x)) r = max(r, c[x]); return r; }
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d%d", &p[i].x, &p[i].y);
sort(p + 1, p + n + 1, [](const Node &u, const Node &v) { return u.x < v.x; });
for (int i = 1; i <= n; i++) b[i] = p[i].y;
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - b - 1;
int ans = 0;
for (int i = 1; i <= n; i++) {
rk[i] = lower_bound(b + 1, b + m + 1, p[i].y) - b;
int f = query(rk[i] - 1) + 1; // 严格递增:只查 y 更小的
update(rk[i], f);
ans = max(ans, f);
}
printf("%d\n", ans);
return 0;
}
复杂度
时间 O(nlogn)O(n\log n)O(nlogn),空间 O(n)O(n)O(n)。
易错点
- 两边都排序:只排一侧就对,两边一起排会拆散配对关系,答案全错。
- 排一侧、对另一侧忘了"严格" :本题数据上
query(r)与query(r-1)可能同结果,但有重复坐标就会多算。 - 按 yyy 排序、对 xxx 求 LIS 同样正确------只要"排序与 LIS 作用在不同两维上"。
例题 I Why Did the Cow Cross the Road II P(洛谷 P3657)
难度 :提高 主考:稀疏候选 + 降序枚举 + LIS
题意
给出两个长度均为 nnn 的序列 AAA、BBB,二者都是 1..n1..n1..n 的排列(n≤105n\le10^5n≤105)。若 ∣Ai−Bj∣≤4|A_i-B_j|\le4∣Ai−Bj∣≤4,可在 AiA_iAi 与 BjB_jBj 间连一条线;每个元素至多连一条,且任意两条不交叉(对 (i,j)(i,j)(i,j) 与 (i′,j′)(i',j')(i′,j′),i<i′i<i'i<i′ 必须 j<j′j<j'j<j′)。求最多连多少条。样例见题面。
触发点
- n≤105n\le10^5n≤105,但合法配对极其稀疏 :AiA_iAi 只能与 BBB 中值落在 Ai−4, Ai+4A_i-4,\\,A_i+4Ai−4,Ai+4 的位置配对,至多 999 个候选,总数 ≤9n\le 9n≤9n。
- 点这么少,就不该走"值域 BIT 批量数点"(第 3 篇逆序对的打法),而应把候选点全列出来求最长链 :iii、jjj 同时严格递增的最长链 = 排序后的一维 LIS。
★关键结论
每条可连的线是点 (i,j)(i,j)(i,j);两线不交叉 ⟺ \iff⟺ iii 与 jjj 同时严格递增。所以答案是候选点集在"两维都严格递增"下的最长链:按 iii 递增枚举,对 jjj 求严格 LIS。
唯一的坑:同一个 iii 内的候选必须按 jjj 从大到小处理。
- 按 iii 递增枚举保证第一维严格递增,"每个 AiA_iAi 至多一条线"也由这点保证。
- 若同一 iii 内按 jjj 升序 :先更新的小 j1j_1j1 留在 BIT 里,处理 j2>j1j_2>j_1j2>j1 时
query(j_2-1)读到f(j_1),就把"同一个 AiA_iAi 用了两次"当成合法链,答案偏大。 - 按 jjj 降序 :先更新较大的 jjj,之后处理较小 jjj 时前缀查询 1,j−11,j-11,j−1 覆盖不到它,自链被天然排除。
"每个 BjB_jBj 至多一条线"自动成立:不同 iii 若用同一个 jjj,jjj 就不严格递增,LIS 不会同时收下。
候选枚举:BBB 是排列,值 vvv 在 BBB 中的位置唯一,预处理 posB[v] = j;对每个 iii 枚举 v∈Ai−4, Ai+4∩1,nv\inA_i-4,\\,A_i+4\cap1,nv∈Ai−4,Ai+4∩1,n,令 j=posBvj=\text{posB}vj=posBv 即得全部候选。BIT 下标直接用 j∈1,nj\in1,nj∈1,n,不需要离散化。
步骤
- 读入 A1..nA1..nA1..n、B1..nB1..nB1..n,填
posB[B[j]] = j;BIT 全 0。 - 对 i=1..ni=1..ni=1..n:把 v∈max(1,Ai−4), min(n,Ai+4)v\in\\max(1,A_i-4),\\ \\min(n,A_i+4)v∈max(1,Ai−4), min(n,Ai+4) 对应的
posB[v]收进临时数组 → 按 jjj 降序 排序 → 依次f = query(j-1)+1、update(j,f)、取 max。 - 输出
ans。
代码骨架
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, A[N], B[N], posB[N], c[N], cand[16];
#define lowbit(x) ((x) & -(x))
void update(int x, int v) { for (; x <= n; x += lowbit(x)) c[x] = max(c[x], v); }
int query(int x) { int r = 0; for (; x > 0; x -= lowbit(x)) r = max(r, c[x]); return r; }
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &A[i]);
for (int j = 1; j <= n; j++) { scanf("%d", &B[j]); posB[B[j]] = j; }
int ans = 0;
for (int i = 1; i <= n; i++) {
int cnt = 0;
for (int v = max(1, A[i] - 4); v <= min(n, A[i] + 4); v++)
cand[++cnt] = posB[v]; // 值 -> B 中的位置
sort(cand + 1, cand + cnt + 1, greater<int>()); // 同一 i 内 j 降序!
for (int t = 1; t <= cnt; t++) {
int f = query(cand[t] - 1) + 1; // j 严格递增
update(cand[t], f);
ans = max(ans, f);
}
}
printf("%d\n", ans);
return 0;
}
复杂度
每个 iii 至多 999 个候选,每个候选一次 O(logn)O(\log n)O(logn) 的查询加修改:时间 O(nlogn)O(n\log n)O(nlogn)(常数约 9×29\times29×2),空间 O(n)O(n)O(n)。
易错点
- 同一 iii 内按 jjj 升序处理 :答案偏大,本题最高频的错。删掉
greater<int>()随手就能对拍出差异。 - 把值当位置用 :vvv 是值 ,坐标必须经
posB[v]映射成位置 jjj;直接拿 vvv 当 jjj 同样高频。 - 边界越界 :Ai−4A_i-4Ai−4 可能小于 111、Ai+4A_i+4Ai+4 可能大于 nnn,必须 clamp 到 1,n1,n1,n,否则
posB读到未定义值。 - 用
query(j)而不是query(j-1):会允许两条线共用同一个 BjB_jBj,答案偏大。
本篇易错点
- 严格性与查询边界必须一一对应 :严格上升
query(r-1),不降query(r),不升先反转排名再query(r')。四种组合错一个,往往只错个别数据,按 7.6 的表逐条确认。 - 前缀 max 初值取 0,且要清楚它表示"长度 0"而非"非法" :fi=1+query(⋅)f_i=1+\text{query}(\cdot)fi=1+query(⋅),空查询返回 000 恰好得到 fi=1f_i=1fi=1。若另设"不可达"标记,别和这个 0 混用。
- 坐标一律 1-based :
lower_bound(...) - b得到的就是 1..m1..m1..m(b从 1 开始),别再加 1;posB存 1..n1..n1..n;update从 1 起步------x = 0时lowbit(0) = 0,循环永不前进,直接死循环。 - 修改只能是"取 max" :写成
c[x] = v(赋值)或试图"撤回"某次update,BIT 立刻不正确;需要赋值、删除、回滚时换区域最值线段树(7.5)。 - LIS 长度不是方案数 :
c[x] = max(c[x], v)把并列最优的个数全丢掉;要计数得把 max 换成"对等于最优值的状态求和",还要处理重复元素。 - BIT 的下标含义每题都可能不同 :例题 G 是离散化排名 1,m1,m1,m,循环上界是
m;例题 I 是**BBB 中的位置** 1,n1,n1,n,上界是n。都叫"下标",混用就漏值或越界。