学习笔记:cdq 分治
CGcgxyXY
·
·
算法·理论
介绍
cdq 分治是陈丹琦总结出的算法,也因此得名。
2008 国集论文。
cdq 分治主要用于解决带有偏序条件的问题,例如操作序列的时间偏序,一维 dp 的下标偏序,或者多维偏序统计等等。
cdq 分治的流程如下:
首先保证序列按任意一个偏序维排序,这里记为 a_i。
那么序列满足 a_i 单调。
特别地,如果偏序条件包含 a_i 相等,那么需要按照其他维的偏序排序。
如果出现所有维都相等的情况,可以合并这些元素或者再添加一维。
记当前处理的区间为 [l,r]。
如果 l=r ,计算单点贡献之后直接返回。
否则以区间中点 mid=\lfloor \frac{l+r}{2} \rfloor 为分界点,把 [l,r] 划分为左区间 [l,mid] 和右区间 [mid+1,r]。
对于跨区间的贡献单独计算,不跨区间的贡献递归计算。
由于排序,第 j 个元素对第 i 个元素有贡献当且仅当 j<i(j=i 特殊处理,作为单点贡献)。
左区间下标范围是 [l,mid],右区间下标范围是 [mid+1,r]。
由于 mid<mid+1,显然 \forall j \in [l,mid],i \in [mid+1,r],j 对 i 都有贡献。
因此 cdq 分治过程的更新显然是正确的。
将分治树画出来:(借用一下 OI Wiki 的图)
可以发现,\forall j \le i,j 对 i 的贡献在 [j,j] 与 [i,i] 在树上节点对应的 LCA 节点所代表的区间 [l,r] 计算。
因此,cdq 分治可以不重不漏的计算所有 (j,i) 对的贡献。
一般地,递归与计算的先后顺序不会影响答案。
特别地,对于 DP 等类需要用更新后的 j 更新 i 的问题,需要在递归树上中序遍历以保证更新时 j 的值是正确的。
cdq 分治的作用可以看作消掉了一维偏序条件,可以看作将修改查询混杂变成先若干修改后若干查询,也可以看作在线转离线。
cdq 分治的时间复杂度是递归计算的,一般需要用到主定理。
cdq 分治优势有许多:常数小,空间占用小,好写等等,可以通过例题自行体会。
此外,cdq 分治还可以处理和区间最值 / 区间最值位置相关的信息,可见这一篇文章。
例题
统计类
P3810 【模板】三维偏序 / 陌上花开
Description:
有 n 个元素,第 i 个元素有 a_i,b_i,c_i 三个属性,设 f(i) 表示满足 a_j \leq a_i 且 b_j \leq b_i 且 c_j \leq c_i 且 j \ne i 的 j 的数量。
对于所有 d \in [0, n) ,求 f(i) = d 的数量。
Solution:
对每一个 i,计算 \sum_{j \in [1,n]}[a_j \le a_i][b_j \le b_i][c_j \le c_i] 的值,然后使用桶统计即可。
考虑使用 cdq 分治。
由于可能出现完全相同的元素,需要特别处理。
:::info[Sol 1]
将相同的元素合并成一个,处理带权的值。
由于 a_i 仍然有可能相同,需要以三维分别为三个关键字进行排序,以确保所有有贡献的 j 都能被统计到。
| 最后统计答案的时候需要考虑权值。 |
| :::info[Sol 2] |
| 可以将元素拆成加入和查询,这样排序时如果 a 相同那么加入操作优先。 |
最后需要去掉 i 对自身的贡献。
| 优点是方便,缺点是这样 n 相当于翻倍了,常数大了一些。 |
| 分治过程中,由于只用处理跨区间贡献,问题变成: |
| 给定若干个元素 (b_j,c_j) 和若干个询问,每次询问所有 j 中满足 b_j \le b_i \land c_j \le c_i 的个数。 |
显然变成一个二维偏序(或者说二维数点)。
解法很多,这里选择排序后使用权值树状数组维护。
时间复杂度 T(n)=2T(\frac{n}{2})+O(n \log V),主定理得到是 O(n \log n \log V)。
空间复杂度 O(n+k+V)。
可以发现,处理偏序条件的方法有许多,例如排序、数据结构、cdq 分治。
每一种方法都可以处理掉一维偏序,同样可以推广到更高维。
具体可以见四维偏序的例题。
另外介绍两种优化:
归并优化:对于需要将区间元素排序,并且排序的关键值不变的题目,可以使用归并排序以减小常数。
暴力优化:分析复杂度,当区间大小不超过一定值(通常需要复杂度分析确定)时可以采用暴力。
Code
:::info[Sol 1]
#include<bits/stdc++.h>
using namespace std;
#define rep(a,b,c) for(int a=(b);a<=(c);a++)
const int N = 1e5 + 5, V = 2e5 + 5;
int n, k;
struct elem {
int a, b, c, w, ans;
bool operator == (const elem & x) const{return a == x.a && b == x.b && c == x.c;}
bool operator < (const elem & x) const{return a == x.a ? b == x.b ? c < x.c : b < x.b : a < x.a;}
} a[N],b[N];
bool cmp(const elem & x, const elem & y){return x.b == y.b ? x.c < y.c : x.b < y.b;}
int tot;
int t[V];
void add(int p, int x){
while (p <= k) t[p] += x, p += p &-p;
}
int sum(int p){
int res = 0;
while (p) res += t[p], p &= p - 1;
return res;
}
void cdq(int l, int r){
if (l >= r) return;
int mid = (l + r) >> 1;
if (r - l <= 15){
rep(i, l, r) rep(j, l, i - 1) if (a[j].b <= a[i].b && a[j].c <= a[i].c) a[i].ans += a[j].w;
sort(a + l, a + r + 1, cmp);
return;
}
cdq(l, mid);
cdq(mid + 1, r);
int j = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].b <= a[i].b) add(a[j].c, a[j].w), ++j;
a[i].ans += sum(a[i].c);
}
rep(i, l, j - 1) add(a[i].c,-a[i].w);
int p = j = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].b <= a[i].b) b[p++] = a[j++];
b[p++] = a[i];
}
while (j <= mid) b[p++] = a[j++];
rep(i, l, r) a[i] = b[i];
}
int cnt[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> k;
rep(i, 1, n){
int x, y, z;
cin >> x >> y >> z;
a[i] = {x, y, z, 1, 0};
}
sort(a + 1, a + n + 1);
rep(i, 1, n){
if (tot && a[i] == a[i - 1])++b[tot].w;
else b[++tot] = a[i];
}
rep(i, 1, tot) a[i] = b[i];
cdq(1, tot);
rep(i, 1, tot) cnt[a[i].ans + a[i].w - 1] += a[i].w;
rep(i, 0, n - 1) cout << cnt[i] << '\n';
return 0;
}
:::
:::info[Sol 2]
#include<bits/stdc++.h>
using namespace std;
#define rep(a,b,c) for(int a=(b);a<=(c);a++)
const int N = 2e5 + 5, V = 2e5 + 5;
int n, k;
struct elem {
int a, b, c, id;
bool operator < (const elem & x) const{return a == x.a ? id < x.id : a < x.a;}
} a[N],b[N];
int ans[N];
int t[V];
void add(int p, int x){
while (p <= k) t[p] += x, p += p &-p;
}
int sum(int p){
int res = 0;
while (p) res += t[p], p &= p - 1;
return res;
}
void cdq(int l, int r){
if (l >= r) return;
int mid = (l + r) >> 1;
cdq(l, mid);
cdq(mid + 1, r);
int j = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].b <= a[i].b){
if (!a[j].id) add(a[j].c, 1);
++j;
}
if (a[i].id) ans[a[i].id] += sum(a[i].c);
}
rep(i, l, j - 1) if (!a[i].id) add(a[i].c,-1);
int p = j = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].b <= a[i].b) b[p++] = a[j++];
b[p++] = a[i];
}
while (j <= mid) b[p++] = a[j++];
rep(i, l, r) a[i] = b[i];
}
int cnt[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> k;
rep(i, 1, n){
int x, y, z;
cin >> x >> y >> z;
a[i] = {x, y, z, 0};
a[n + i] = {x, y, z, i};
} sort(a+1,a+n*2+1);
cdq(1, n * 2);
rep(i, 1, n)++cnt[ans[i] - 1];
rep(i, 0, n - 1) cout << cnt[i] << '\n';
return 0;
}
:::
P4390 [BalkanOI 2007] Mokia 摩基亚
Description
维护一个平面,每个点有权值,初始均为 0。
要求支持两种操作:
类型 1:点 (x,y) 的权值添加 w。
类型 2:查询矩形 (x1,y1,x2,y2) 内所有点的总权值。
Solution
从偏序的角度来看:
一个操作 1 的 (x,y,w) 对一个操作 2 的 (x1,y1,x2,y2) 有 w 的贡献当且仅当 x \in [x1,x2] \land y \in [y1,y2] \land t<t'。
其中 t 为操作 1 的时间,t' 为操作二的时间。
记 $S(x,y,t)=\sum_{t'<t \land x' \le x \land y' \le y}w'$,其中 $(x',y',w',t')$ 表示一次操作 1。
那么 $\sum_{x \in [x1,x2] \land y \in [y1,y2] \land t<t'}w=S(x2,y2,t')-S(x1-1,y2,t')-S(x2,y1-1,t')+S(x1-1,y1-1,t')$。
然后便可以直接偏序统计。
从在线转离线的角度来看:
同样考虑计算操作 1 对操作 2 的贡献。
在一次分治中,只处理左区间的操作 1 和右区间的操作 2。
容易发现这样每次分治只需要处理二维数点,排序后使用数据结构维护即可。
正确性易证。
代码中使用了树状数组。
不难分析,时间复杂度 $O(n \log n \log w)$,空间复杂度 $O(n+w)$。
其中 $n$ 为总操作数。
##### Code
:::info[Code]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
const int N = 16e4 + 5, M = 4e4 + 5, V = 2e6 + 5;
int w;
struct op {
int x, y, z, typ, id;
}a[N + M], b[N + M];
int cnt, cntq;
int ans[M];
int t[V];
void add(int p, int x){while (p <= w) t[p] += x, p += p &-p;}
int sum(int p){int res = 0; while (p) res += t[p], p &= p - 1; return res;}
void solve(int l, int r){
if (l >= r) return;
int mid = (l + r) >> 1;
solve(l, mid);
solve(mid + 1, r);
int j = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].x <= a[i].x){
if (!a[j].typ) add(a[j].y, a[j].z);
++j;
}
if (a[i].id) ans[a[i].id] += sum(a[i].y) * a[i].typ;
}
rep(i, l, j - 1) if (!a[i].typ) add(a[i].y,-a[i].z);
int p = j = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].x <= a[i].x) b[p++] = a[j++];
b[p++] = a[i];
}
while (j <= mid) b[p++] = a[j++];
rep(i, l, r) a[i] = b[i];
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int op;
while (cin >> op){
if (!op) cin >> w;
else if (op == 3) break;
else if (op == 1){
int x, y, k;
cin >> x >> y >> k;
a[++cnt] = {x, y, k, 0, 0};
}
else if (op == 2){
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
++cntq;
a[++cnt] = {x2, y2, 0, 1, cntq};
a[++cnt] = {x2, y1 - 1, 0,-1, cntq};
a[++cnt] = {x1 - 1, y2, 0,-1, cntq};
a[++cnt] = {x1 - 1, y1 - 1, 0, 1, cntq};
}
}
solve(1, cnt);
rep(i, 1, cntq) cout << ans[i] << '\n';
return 0;
}
```
:::
#### [P14957 【模板】离线静态四维数点](https://www.luogu.com.cn/problem/P14957)
##### Description
平面上有 $n$ 个矩形,第 $i$ 个矩形的左下角是 $x_{i,1},y_{i,1}$,右上角是 $x_{i,2},y_{i,2}$。
有 $m$ 次询问,第 $j$ 次询问给定一个左下角是 $X_{j,1},Y_{j,1}$,右上角是 $X_{j,2},Y_{j,2}$ 的矩形,求有多少个平面上的矩形完全包含了询问给定的矩形。
左下角是 $x_{i,1},y_{i,1}$,右上角是 $x_{i,2},y_{i,2}$ 的矩形包含左下角是 $X_{j,1},Y_{j,1}$,右上角是 $X_{j,2},Y_{j,2}$ 的矩形的充分必要条件是 $x_{i,1}\le X_{j,1}$ 且 $y_{i,1}\le Y_{j,1}$ 且 $X_{j,2}\le x_{i,2}$ 且 $Y_{j,2}\le y_{i,2}$。
##### Solution
根据题意,只需要对于每一次询问,计算 $\sum_{i \in [1,n]}[x_{i,1}\le X_{j,1}][y_{i,1}\le Y_{j,1}][X_{j,2}\le x_{i,2}][Y_{j,2}\le y_{i,2}]$ 的值。
四维偏序,排序先消去一维。
然后可以使用 cdq 分治和数据结构消去剩下的维度。
这里选择两次 cdq 分治加树状数组。
cdq 分治的嵌套需要注意当前处理元素属于原来的左区间还是右区间。
本题左区间只处理原来的矩形,右区间只处理询问的矩形,根据矩形类型即可判断。
cdq 分治的嵌套仍然可以使用归并和暴力的优化。
经过分析,时间复杂度为 $O((n+m) \log^3 (n+m))$,空间复杂度为 $O(n+m)$。
##### Code
:::info[归并优化]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
const int N = 4e5 + 5, M = 8e5 + 5;
int n, q;
int ans[N];
int val[M], tot;
struct op {
int x1, y1, x2, y2, id;
bool operator < (const op & x) const{return x1 == x.x1 ? id < x.id : x1 < x.x1;}
} a[M],b[M],c[M];
int ct, cnt;
bool cmp(const op & x, const op & y){return x.y1 == y.y1 ? x.id < y.id : x.y1 < y.y1;}
int t[M];
inline void add(int p, int x){
while (p) t[p] += x, p &= p - 1;
}
inline int sum(int p){
int res = 0;
while (p <= tot) res += t[p], p += p &-p;
return res;
}
void cdq2(int l, int r){
if (l >= r) return;
int mid = (l + r) >> 1;
cdq2(l, mid);
cdq2(mid + 1, r);
int j = l;
rep(i, mid + 1, r){
while (j<= mid && b[j].x2>= b[i].x2){
if (!b[j].id) add(b[j].y2, 1);
++j;
}
if (b[i].id) ans[b[i].id] += sum(b[i].y2);
}
rep(i, l, j - 1) if (!b[i].id) add(b[i].y2,-1);
int p = j = l;
rep(i, mid + 1, r){
while (j<= mid && b[j].x2>= b[i].x2) c[p++] = b[j++];
c[p++] = b[i];
}
while (j <= mid) c[p++] = b[j++];
rep(i, l, r) b[i] = c[i];
}
void cdq1(int l, int r){
if (l >= r) return;
int mid = (l + r) >> 1;
cdq1(l, mid);
cdq1(mid + 1, r);
cnt = 0;
int j = l, p = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].y1 <= a[i].y1){
if (!a[j].id) b[++cnt] = a[j];
c[p++] = a[j++];
}
if (a[i].id) b[++cnt] = a[i];
c[p++] = a[i];
}
while (j <= mid) c[p++] = a[j++];
rep(i, l, r) a[i] = c[i];
cdq2(1, cnt);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> q;
rep(i, 1, n){
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
a[++ct] = {x1, y1, x2, y2, 0};
}
rep(i, 1, q){
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
a[++ct] = {x1, y1, x2, y2, i};
}
rep(i, 1, ct) val[i] = a[i].y2;
sort(val + 1, val + ct + 1);
tot = unique(val + 1, val + ct + 1) - (val + 1);
rep(i, 1, ct) a[i].y2 = lower_bound(val + 1, val + tot + 1, a[i].y2) - val;
sort(a + 1, a + ct + 1);
cdq1(1, ct);
rep(i, 1, q) cout << ans[i] << '\n';
return 0;
}
```
:::
:::info[归并优化 + 暴力优化]
使用了 `fread` 快读和 `fwrite` 快写。
```cpp
#include<bits/stdc++.h>
using namespace std;
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
struct IO {
#define SZ 1<<21
char buf[SZ],*p1,*p2;
char pbuf[SZ],*pp;
IO() : p1(buf), p2(buf), pp(pbuf){}
~IO(){fwrite(pbuf, 1, pp - pbuf, stdout);}
inline char gc(){
if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, SZ, stdin);
return p1 == p2 ? EOF :*p1++;
}
void read(int & x){
x = 0;
char ch = gc();
while (ch < 48) ch = gc();
do {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = gc();
}
while (ch > 47);
}
void push(const char & c){
if (pp - pbuf == SZ) fwrite(pbuf, 1, SZ, stdout), pp = pbuf;
*pp++ = c;
}
int stk[10], top;
void write(int x){
if (!x){push('0'); push('\n'); return;}
while (x) stk[++top] = x % 10, x /= 10;
while (top) push(stk[top--] + 48);
push('\n');
}
} io;
const int N = 4e5 + 5, M = 8e5 + 5;
int n, q;
int ans[N];
int val[M], tot;
struct op {
int x1, y1, x2, y2, id;
bool operator < (const op & x) const{return x1 == x.x1 ? id < x.id : x1 < x.x1;}
} a[M],b[M],c[M];
int ct, cnt;
bool cmp(const op & x, const op & y){return x.y1 == y.y1 ? x.id < y.id : x.y1 < y.y1;}
bool Cmp(const op & x, const op & y){return x.x2 == y.x2 ? x.id<y.id : x.x2> y.x2;}
int t[M];
inline void add(int p, int x){
while (p) t[p] += x, p &= p - 1;
}
inline int sum(int p){
int res = 0;
while (p <= tot) res += t[p], p += p &-p;
return res;
}
inline void cdq2(int l, int r){
if (l >= r) return;
if (r - l + 1 <= 16){
rep(i, l, r) if (b[i].id) rep(j, l, i - 1) if (!b[j].id) ans[b[i].id] += b[j].y1 <= b[i].y1 & b[i].x2 <= b[j].x2 & b[i].y2 <= b[j].y2;
sort(b + l, b + r + 1, Cmp);
return;
}
int mid = (l + r) >> 1;
cdq2(l, mid);
cdq2(mid + 1, r);
int j = l;
rep(i, mid + 1, r){
while (j<= mid && b[j].x2>= b[i].x2){
if (!b[j].id) add(b[j].y2, 1);
++j;
}
if (b[i].id) ans[b[i].id] += sum(b[i].y2);
}
rep(i, l, j - 1) if (!b[i].id) add(b[i].y2,-1);
int p = j = l;
rep(i, mid + 1, r){
while (j<= mid && b[j].x2>= b[i].x2) c[p++] = b[j++];
c[p++] = b[i];
}
while (j <= mid) c[p++] = b[j++];
rep(i, l, r) b[i] = c[i];
}
inline void cdq1(int l, int r){
if (l >= r) return;
if (r - l + 1 <= 64){
rep(i, l, r) if (a[i].id) rep(j, l, i - 1) if (!a[j].id) ans[a[i].id] += a[j].y1 <= a[i].y1 & a[i].x2 <= a[j].x2 & a[i].y2 <= a[j].y2;
sort(a + l, a + r + 1, cmp);
return;
}
int mid = (l + r) >> 1;
cdq1(l, mid);
cdq1(mid + 1, r);
cnt = 0;
int j = l, p = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].y1 <= a[i].y1){
if (!a[j].id) b[++cnt] = a[j];
c[p++] = a[j++];
}
if (a[i].id) b[++cnt] = a[i];
c[p++] = a[i];
}
while (j <= mid) c[p++] = a[j++];
rep(i, l, r) a[i] = c[i];
cdq2(1, cnt);
}
int main(){
io.read(n), io.read(q);
rep(i, 1, n){
int x1, y1, x2, y2;
io.read(x1), io.read(y1), io.read(x2), io.read(y2);
a[++ct] = {x1, y1, x2, y2, 0};
}
rep(i, 1, q){
int x1, y1, x2, y2;
io.read(x1), io.read(y1), io.read(x2), io.read(y2);
a[++ct] = {x1, y1, x2, y2, i};
}
rep(i, 1, ct) val[i] = a[i].y2;
sort(val + 1, val + ct + 1);
tot = unique(val + 1, val + ct + 1) - (val + 1);
rep(i, 1, ct) a[i].y2 = lower_bound(val + 1, val + tot + 1, a[i].y2) - val;
sort(a + 1, a + ct + 1);
cdq1(1, ct);
rep(i, 1, q) io.write(ans[i]);
return 0;
}
```
:::
### DP 类
#### [P3364 Cool loves touli](https://www.luogu.com.cn/problem/P3364)
#### Description
给定 $n$ 个元素,每个元素有四个维度 $l,s,w,a$。
问最多能选择多少个元素,使得这些元素按照 $l$ 升序排序后,使得相邻的两个元素 $i$ 和 $i+1$ 满足 $a_i \le s_{i+1} \land a_{i+1} \ge w_i$。
保证 $l$ 互不相同。
#### Solution
先按 $l$ 升序排序。
考虑 DP。
记 $f_i$ 为以 $i$ 结尾的子序列的最大长度。
有转移 $f_i=\max_{j <i \land a_j \le s_i \land a_i \ge w_j}f_j+1$,如果不存在合法的 $j$,那么 $f_i=1$。
转移方程有偏序约束,可以使用 cdq 分治。
排序消掉一维,cdq 分治解决一维,再用数据结构解决最后一维。
由于条件是简单的小于等于,最内层可以使用树状数组维护单点修改前缀最大值。
(如果是区间限制,那么需要使用其他的思路,例如扫描线。)
由于用 $f_j$ 更新 $f_i$ 需要保证 $f_j$ 已经计算完毕,需要在分治树上中序遍历以保证正确性。
中序遍历也可以使用归并优化,但是需要加上分裂区间的操作,比较麻烦,一般也没人卡这个。
除了更新顺序不同,cdq 分治优化偏序 DP 和偏序类统计区别不大,因此不做赘述。
唯一需要注意的是元素顺序的变化与还原。
简单分析,得到时间复杂度 $O(n \log^2 n)$,空间复杂度 $O(n)$。
##### Code
:::info[Code]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
const int N = 1e5 + 5, V = 3e5 + 5;
int n;
struct elem {
int l, s, w, a, f;
bool operator < (const elem & x) const{return l < x.l;}
} a[N];
bool cmps(const elem & x, const elem & y){return x.s < y.s;}
bool cmpa(const elem & x, const elem & y){return x.a < y.a;}
int val[V], tot;
#define ins(x) (val[++tot]=x)
#define dc(x) (x=lower_bound(val+1,val+tot+1,x)-val)
int t[V];
void upd(int p, int x){
while (p <= tot) t[p] = max(t[p], x), p += p &-p;
}
void clr(int p){
while (p <= tot) t[p] = 0, p += p &-p;
}
int qry(int p){
int res = 0;
while (p) res = max(res, t[p]), p &= p - 1;
return res;
}
void solve(int l, int r){
if (l >= r) return;
sort(a + l, a + r + 1);
int mid = (l + r) >> 1;
solve(l, mid);
sort(a + l, a + mid + 1, cmpa);
sort(a + mid + 1, a + r + 1, cmps);
int j = l;
rep(i, mid + 1, r){
while (j <= mid && a[j].a <= a[i].s) upd(a[j].w, a[j].f), ++j;
a[i].f = max(a[i].f, qry(a[i].a) + 1);
}
rep(i, l, j - 1) clr(a[i].w);
solve(mid + 1, r);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
rep(i, 1, n){
int l, s, w, x;
cin >> l >> s >> w >> x;
a[i] = {l, s, w, x, 1};
ins(s), ins(w), ins(x);
} sort(val+1,val+tot+1);
tot = unique(val + 1, val + tot + 1) - (val + 1);
rep(i, 1, n) dc(a[i].s), dc(a[i].w), dc(a[i].a);
solve(1, n);
int ans = 0;
rep(i, 1, n) ans = max(ans, a[i].f);
cout << ans << '\n';
return 0;
}
```
:::
:::info[归并优化]
辅助数组可以只存下标,代码中保存了对应元素。
(经测试,由于存下标的额外内存访问,存下标跑的比存元素还要慢一些,但是空间常数小。)
```cpp
#include<bits/stdc++.h>
using namespace std;
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
const int N = 1e5 + 5, V = 3e5 + 5;
int n;
struct elem {
int l, s, w, a, id;
} a[N],b[N],c[N],d[N];
int f[N];
bool cmpl(const elem & x, const elem & y){return x.l < y.l;}
bool cmpa(const elem & x, const elem & y){return x.a < y.a;}
bool cmps(const elem & x, const elem & y){return x.s < y.s;}
void split(int l, int r){
int mid = (l + r) >> 1;
int i = l, j = mid + 1;
rep(x, l, r) d[b[x].l <= a[mid].l ? i++ : j++] = b[x];
rep(x, l, r) b[x] = d[x];
i = l, j = mid + 1;
rep(x, l, r) d[c[x].l <= a[mid].l ? i++ : j++] = c[x];
rep(x, l, r) c[x] = d[x];
}
void merge(int l, int r){
int mid = (l + r) >> 1;
int p = l, j = l;
rep(i, mid + 1, r){
while (j <= mid && b[j].a <= b[i].a) d[p++] = b[j++];
d[p++] = b[i];
}
while (j <= mid) d[p++] = b[j++];
rep(i, l, r) b[i] = d[i];
p = l, j = l;
rep(i, mid + 1, r){
while (j <= mid && c[j].s <= c[i].s) d[p++] = c[j++];
d[p++] = c[i];
}
while (j <= mid) d[p++] = c[j++];
rep(i, l, r) c[i] = d[i];
}
int val[V], tot;
#define ins(x) (val[++tot]=x)
#define dc(x) (x=lower_bound(val+1,val+tot+1,x)-val)
int t[V];
void upd(int p, int x){
while (p <= tot) t[p] = max(t[p], x), p += p &-p;
}
void clr(int p){
while (p <= tot) t[p] = 0, p += p &-p;
}
int qry(int p){
int res = 0;
while (p) res = max(res, t[p]), p &= p - 1;
return res;
}
void solve(int l, int r){
if (l >= r) return;
split(l, r);
int mid = (l + r) >> 1;
solve(l, mid);
int j = l;
rep(i, mid + 1, r){
while (j <= mid && b[j].a <= c[i].s) upd(b[j].w, f[b[j].id]), ++j;
f[c[i].id] = max(f[c[i].id], qry(c[i].a) + 1);
}
rep(i, l, j - 1) clr(b[i].w);
solve(mid + 1, r);
merge(l, r);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
rep(i, 1, n){
int l, s, w, x;
cin >> l >> s >> w >> x;
a[i] = {l, s, w, x, i};
ins(s), ins(w), ins(x);
f[i] = 1;
}
sort(val + 1, val + tot + 1);
tot = unique(val + 1, val + tot + 1) - (val + 1);
rep(i, 1, n) dc(a[i].s), dc(a[i].w), dc(a[i].a);
rep(i, 1, n) b[i] = c[i] = a[i];
sort(a + 1, a + n + 1, cmpl);
sort(b + 1, b + n + 1, cmpa);
sort(c + 1, c + n + 1, cmps);
solve(1, n);
int ans = 0;
rep(i, 1, n) ans = max(ans, f[i]);
cout << ans << '\n';
return 0;
}
```
:::
#### [P5979 [PA 2014] Druzyny](https://www.luogu.com.cn/problem/P5979)
本题涉及区间最值的处理,建议先阅读[这一篇文章](https://www.luogu.com.cn/article/xmfi4bx1)。
同时这一题也是上述文章中的例题。
##### Description
给定长度为 $n$ 的序列 $c,d$。
你需要将其分为若干段,每段满足 $\max_{i=l}^r c_i \le r-l+1 \le \min_{i=l}^r d_i$。
求出最大段数和使得段数最大的方案数,方案数对 $10^9+7$ 取模。
##### Solution
由题意,可以 DP。
定义 $f_i$ 为处理了前 $i$ 个,并且 $i$ 为一个右端点的答案,$g_i$ 为 $f_i$ 的方案数。
那么有转移 $f_i=\max_{1 \le j \le i \land \max_{k=j}^i c_k \le i-j+1 \le \min_{k=j}^i d_k} f_{j-1}+1$,$g_i=\sum_{1 \le j \le i \land \max_{k=j}^i c_k \le i-j+1 \le \min_{k=j}^i d_k }[f_i=f_{j-1}+1]g_{j-1}$。
$g_i$ 的转移可以和 $f_i$ 放在一起。
使用二元组 $(f_i,g_i)$ 表示一个位置的值,那么转移就是 $(f_i,g_i) \leftarrow (\max(f_i,f_j+1),[f_i \ge f_j]g_i+[f_i \le f_j]g_j)$。
对于 $\max_{k=j}^i c_k \le i-j+1 \le \min_{k=j}^i d_k$ 的限制,可以把两个最值从中点拆开。
用 $mx_i$ 表示 $c_i$ 的前 / 后缀最大值,$mn_i$ 表示 $d_i$ 的前 / 后缀最小值。
那么转移条件变成 $1 \le j \le i \land i-j+1 \in[\max(mx_i,mx_j),\min(mn_i,mn_j)]$。
变形得到 $i \in [j+mx_j-1,j+mn_j-1] \land j \in [i-mn_i+1,i-mx_i+1]$。
此时变成一个三维偏序约束的 DP。
cdq 分治已经解决了 $1 \le j \le i$(注意在 $l=r$ 时更新)。
剩下来的两维可以排序后使用线段树维护。
用线段树维护每一个位置 $j$ 的 $(f_j,g_j)$ 对转移的贡献。
对于每一个 $j$,在 $i=j+mx_j-1$ 时更新线段树上的 $j$ 为 $(f_{j-1},g_{j-1})$,在 $i=j+mn_j-1$ 时更新线段树上的 $j$ 为 $(-\infty,0)$。
升序处理 $i$,每次先加入 $j$,再查询 $[i-mn_i+1,i-mx_i+1]$ 内所有 $(f_j,g_j)$ 的贡献,然后删除 $j$。
线段树的节点合并与转移的更新类似,$(f_x,g_x)$ 与 $(f_y,g_y)$ 合并得到 $(\max(f_x,f_y),[f_x \ge f_y]g_x+[f_x \le f_y]g_y)$。
时间复杂度 $O(n\log^2n)$,空间复杂度 $O(n)$。
##### Code
:::info[Code]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
#define per(a,b,c) for(int a=(b);a>=(c);--a)
const int N = 1e6 + 5, P = 1e9 + 7, INF = 0x3f3f3f3f;
int n;
int c[N], d[N];
struct dp {
int f, g;
} f[N];
dp merge(dp x, dp y){
return {max(x.f, y.f), ((x.f >= y.f) * x.g + (x.f <= y.f) * y.g) % P};
}
int mx[N], mn[N];
dp t[N << 2];
#define ls(x) (x<<1)
#define rs(x) (x<<1|1)
void upd(int p, int pl, int pr, int x, dp v){
if (pl == pr) return void(t[p] = v);
int mid = (pl + pr) >> 1;
x <= mid ? upd(ls(p), pl, mid, x, v) : upd(rs(p), mid + 1, pr, x, v);
t[p] = merge(t[ls(p)], t[rs(p)]);
}
dp query(int p, int pl, int pr, int l, int r){
if (l > r) return {-INF, 0};
if (l <= pl && pr <= r) return t[p];
int mid = (pl + pr) >> 1;
if (r <= mid) return query(ls(p), pl, mid, l, r);
if (l > mid) return query(rs(p), mid + 1, pr, l, r);
return merge(query(ls(p), pl, mid, l, r), query(rs(p), mid + 1, pr, l, r));
}
struct vec {
int head[N], nxt[N], val[N], cnt;
void add(int x, int y){
nxt[++cnt] = head[x];
head[x] = cnt;
val[cnt] = y;
}
} add,del;
void solve(int l, int r){
if (l == r){
if (c[l] == 1) f[l] = merge(f[l], {f[l - 1].f + 1, f[l - 1].g});
return;
}
int mid = (l + r) >> 1;
solve(l, mid);
mx[mid] = c[mid], mx[mid + 1] = c[mid + 1];
per(i, mid - 1, l) mx[i] = max(mx[i + 1], c[i]);
rep(i, mid + 2, r) mx[i] = max(mx[i - 1], c[i]);
mn[mid] = d[mid], mn[mid + 1] = d[mid + 1];
per(i, mid - 1, l) mn[i] = min(mn[i + 1], d[i]);
rep(i, mid + 2, r) mn[i] = min(mn[i - 1], d[i]);
rep(i, mid + 1, r) add.head[i] = del.head[i] = 0;
add.cnt = del.cnt = 0;
rep(i, l, mid){
int L = max(mid + 1, i + mx[i] - 1), R = min(i + mn[i] - 1, r);
if (L <= R) add.add(L, i), del.add(R, i);
}
rep(i, mid + 1, r){
for (int j = add.head[i]; j; j = add.nxt[j]){
int x = add.val[j];
upd(1, 1, n, x, f[x - 1]);
}
int L = max(l, i - mn[i] + 1), R = min(mid, i - mx[i] + 1);
if (L <= R){
dp res = query(1, 1, n, L, R);
f[i] = merge(f[i], {res.f + 1, res.g});
}
for (int j = del.head[i]; j; j = del.nxt[j]){
int x = del.val[j];
upd(1, 1, n, x, {-INF, 0});
}
}
solve(mid + 1, r);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
rep(i, 1, n) cin >> c[i] >> d[i];
f[0] = {0, 1};
rep(i, 1, n) f[i] = {-INF, 0};
rep(i, 1, n * 4) t[i] = {-INF, 0};
solve(1, n);
if (f[n].f <= 0) cout << "NIE\n";
else cout << f[n].f << ' ' << f[n].g << '\n';
return 0;
}
```
:::
### 其他类
#### [P4655 [CEOI 2017] Building Bridges](https://www.luogu.com.cn/problem/P4655)
##### Description
有 $n$ 根柱子依次排列,每根柱子都有一个高度。第 $i$ 根柱子的高度为 $h_i$。
现在想要建造若干座桥,如果一座桥架在第 $i$ 根柱子和第 $j$ 根柱子之间,那么需要 $(h_i-h_j)^2$ 的代价。
在造桥前,所有用不到的柱子都会被拆除,因为他们会干扰造桥进程。第 $i$ 根柱子被拆除的代价为 $w_i$,注意 $w_i$ 不一定非负,因为(有违禁词毙掉了)。
求出通过桥梁把第 $1$ 根柱子和第 $n$ 根柱子连接的最小代价。注意桥梁不能在端点以外的任何地方相交。
##### Solution
考虑使用 DP。
定义 $f_i$ 为将第 $1$ 根柱子与第 $i$ 根柱子连接的最小代价。
$f_1=0$。计算出的 $f_n$ 即为答案。
易得转移方程 $f_i=\min_{j<i}f_j+\sum_{x=j+1}^{i-1}w_x+(h_i-h_j)^2$。
前缀和优化掉 $\sum_{x=j+1}^{i-1}w_x$。
记 $s_i=\sum_{j=1}^iw_j$。
那么 $\sum_{x=j+1}^{i-1}w_x=s_{i-1}-s_j$。
此时得到一个 $O(n^2)$ 做法。
将转移方程展开,得到 $f_i=\min_{j<i}-2h_ih_j+h_i^2+s_{i-1}+f_j+h_j^2-s_j$。
观察转移方程,发现符合斜率优化的形式。
将等式看成一条直线 $y=kx+b$ ,其中 $y=f_j+h_j^2-s_j,k=2h_i,x=h_j,b=f_i-s_{i-1}-h_i^2$。
由于 $-s_{i-1}-h_i^2$ 是常数,$f_i$ 最小,当且仅当截距 $b$ 最小。
因此,需要维护 $(x_j,y_j)$ 组成的下凸壳,用斜率为 $2h_i$ 的直线去截最小值。
由于 $h$ 是无序的,可以使用平衡树维护凸壳。
但是通过 cdq 分治,同样可以维护。
每次处理区间,只需要考虑左区间的点对右区间的直线进行转移。
可以将左区间的点按 $x$ 排序,右区间的直线按 $k$ 排序,然后使用单调队列维护。
单调队列维护凸壳需要注意边界。
直接排序的总时间是 $O(n\log^2n)$ 的,瓶颈在排序,还不够优秀。
此时可以使用支持分裂的归并排序,这样就可以做到 $O(n \log n)$。
空间复杂度 $O(n)$。
##### Code
:::info[Code]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
#define per(a,b,c) for(int a=(b);a>=(c);--a)
const int N = 1e5 + 5;
const ll INF = 0x3f3f3f3f3f3f3f3fLL;
int n;
int h[N], w[N];
ll f[N], s[N];
int a[N];
bool cmp(const int & x, const int & y){return h[x] < h[y];}
int q[N], ql, qr;
inline ll y(int x){return f[x] + 1LL * h[x] * h[x] - s[x];}
void solve(int l, int r){
if (l >= r) return;
int mid = (l + r) >> 1;
solve(l, mid);
rep(i, l, r) a[i] = i;
sort(a + l, a + mid + 1, cmp);
sort(a + mid + 1, a + r + 1, cmp);
ql = 1, qr = 0;
rep(i, l, mid){
int x = a[i];
while (ql <= qr && h[x] == h[q[qr]] && y(x) <= y(q[qr]))--qr;
if (ql <= qr && h[x] == h[q[qr]]) continue;
while (ql < qr && (y(q[qr]) - y(q[qr - 1])) * (h[x] - h[q[qr]]) >= (y(x) - y(q[qr])) * (h[q[qr]] - h[q[qr - 1]]))--qr;
q[++qr] = x;
}
int j = 1;
rep(i, mid + 1, r){
int x = a[i];
while (j < qr && y(q[j + 1]) - y(q[j]) <= 2LL * h[x] * (h[q[j + 1]] - h[q[j]]))++j;
int y = q[j];
f[x] = min(f[x], f[y] + s[x - 1] - s[y] + 1LL * (h[x] - h[y]) * (h[x] - h[y]));
}
solve(mid + 1, r);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
rep(i, 1, n) cin >> h[i];
rep(i, 1, n) cin >> w[i];
rep(i, 1, n) s[i] = s[i - 1] + w[i];
rep(i, 2, n) f[i] = INF;
solve(1, n);
cout << f[n] << '\n';
return 0;
}
```
:::
:::info[归并优化]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
#define per(a,b,c) for(int a=(b);a>=(c);--a)
const int N = 1e5 + 5;
const ll INF = 0x3f3f3f3f3f3f3f3fLL;
int n;
int h[N], w[N];
ll f[N], s[N];
int a[N], b[N];
bool cmp(const int & x, const int & y){return h[x] < h[y];}
void split(int l, int r){
int mid = (l + r) >> 1;
int i = l, j = mid + 1;
rep(x, l, r) b[a[x] <= mid ? i++ : j++] = a[x];
rep(x, l, r) a[x] = b[x];
}
void merge(int l, int r){
int mid = (l + r) >> 1;
int p = l, j = l;
rep(i, mid + 1, r){
while (j <= mid && h[a[j]] <= h[a[i]]) b[p++] = a[j++];
b[p++] = a[i];
}
while (j <= mid) b[p++] = a[j++];
rep(i, l, r) a[i] = b[i];
}
int q[N], ql, qr;
inline ll y(int x){return f[x] + 1LL * h[x] * h[x] - s[x];}
void solve(int l, int r){
if (l >= r) return;
int mid = (l + r) >> 1;
split(l, r);
solve(l, mid);
ql = 1, qr = 0;
rep(i, l, mid){
int x = a[i];
while (ql <= qr && h[x] == h[q[qr]] && y(x) <= y(q[qr]))--qr;
if (ql <= qr && h[x] == h[q[qr]]) continue;
while (ql < qr && (y(q[qr]) - y(q[qr - 1])) * (h[x] - h[q[qr]]) >= (y(x) - y(q[qr])) * (h[q[qr]] - h[q[qr - 1]]))--qr;
q[++qr] = x;
}
int j = 1;
rep(i, mid + 1, r){
int x = a[i];
while (j < qr && y(q[j + 1]) - y(q[j]) <= 2LL * h[x] * (h[q[j + 1]] - h[q[j]]))++j;
int y = q[j];
f[x] = min(f[x], f[y] + s[x - 1] - s[y] + 1LL * (h[x] - h[y]) * (h[x] - h[y]));
}
solve(mid + 1, r);
merge(l, r);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
rep(i, 1, n) cin >> h[i];
rep(i, 1, n) cin >> w[i];
rep(i, 1, n) s[i] = s[i - 1] + w[i];
rep(i, 2, n) f[i] = INF;
rep(i, 1, n) a[i] = i;
sort(a + 1, a + n + 1, cmp);
solve(1, n);
cout << f[n] << '\n';
return 0;
}
```
:::
#### [P3122 [USACO15FEB] Fencing the Herd G](https://www.luogu.com.cn/problem/P3122)
##### Description
给定一个平面直角坐标系,初始有 $n$ 个点 $(x_i,y_i)$。
$m$ 次操作,分为两种类型。
第一种操作在平面内加入一个点 $(x,y)$。
第二种操作询问是否所有的点都在给定直线 $Ax+By=C$ 同侧。
##### Solution
关注询问。
都在 $Ax+By=C$ 同侧,即 $\forall (x,y),Ax+By>C \lor \forall (x,y), Ax+By<C$($B>0$ 或 $ B=0 \land A>0$)。
那么条件等价于 $\min_{(x,y)}Ax+By>C\lor\max_{(x,y)}Ax+By<C$。
于是只需要对于每一条直线,求出此时 $Ax+By$ 的最值。
可以维护上凸壳求最大值,下凸壳求最小值。
在线查询可以二分,离线的话可以排序然后双指针。
在线加点和询问可以使用平衡树。
这里考虑 cdq 分治转离线。
离线之后,只需要将左区间的点按照 $x$ 排序,右区间的直线按照 $k$ 排序。
这样就可以使用单调队列维护凸壳,然后使用双指针查询。
单层内复杂度是线性的。
由于本题是统计类问题,可以后序遍历分治树并利用归并排序维护点和直线的有序性。
同样需要注意凸壳的边界。
时间复杂度 $O((n+m)\log (n+m))$,空间复杂度 $O(n+m)$。
##### Code
:::info[归并优化]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define rep(a,b,c) for(int a=(b);a<=(c);++a)
#define per(a,b,c) for(int a=(b);a>=(c);--a)
const int N = 1e5 + 5, M = 2e5 + 5;
int n, Q;
struct op {
int x, y;
ll z;
int id;
bool operator < (const op & o) const{return !id ^ !o.id ? id < o.id : id ? 1LL * x * o.y < 1LL * o.x * y : x < o.x;}
} a[M],b[M];
int cnt, cq;
ll mx[N], mn[N];
int q[N], ql, qr;
void cdq(int l, int r){
if (l >= r) return;
int mid = (l + r) >> 1;
cdq(l, mid);
cdq(mid + 1, r);
ql = 1, qr = 0;
rep(i, l, mid) if (!a[i].id){
while (ql <= qr && a[i].x == a[q[qr]].x && a[q[qr]].y <= a[i].y)--qr;
if (ql <= qr && a[i].x == a[q[qr]].x) continue;
while (ql < qr && 1LL * (a[q[qr]].y - a[q[qr - 1]].y) * (a[i].x - a[q[qr]].x) <= 1LL * (a[i].y - a[q[qr]].y) * (a[q[qr]].x - a[q[qr - 1]].x))--qr;
q[++qr] = i;
}
int j = 1;
rep(i, mid + 1, r) if (a[i].id){
while (j < qr && 1LL * (a[q[j + 1]].y - a[q[j]].y) * a[i].y >-1LL * a[i].x * (a[q[j + 1]].x - a[q[j]].x))++j;
if (j <= qr) mx[a[i].id] = max(mx[a[i].id], 1LL * a[i].x * a[q[j]].x + 1LL * a[i].y * a[q[j]].y - a[i].z);
}
ql = 1, qr = 0;
rep(i, l, mid) if (!a[i].id){
while (ql<= qr && a[i].x == a[q[qr]].x && a[q[qr]].y>= a[i].y)--qr;
if (ql <= qr && a[i].x == a[q[qr]].x) continue;
while (ql < qr && 1LL * (a[q[qr]].y - a[q[qr - 1]].y) * (a[i].x - a[q[qr]].x) >= 1LL * (a[i].y - a[q[qr]].y) * (a[q[qr]].x - a[q[qr - 1]].x))--qr;
q[++qr] = i;
}
j = 1;
per(i, r, mid + 1) if (a[i].id){
while (j < qr && 1LL * (a[q[j + 1]].y - a[q[j]].y) * a[i].y < 1LL *-a[i].x * (a[q[j + 1]].x - a[q[j]].x))++j;
if (j <= qr) mn[a[i].id] = min(mn[a[i].id], 1LL * a[i].x * a[q[j]].x + 1LL * a[i].y * a[q[j]].y - a[i].z);
}
int p = j = l;
rep(i, mid + 1, r){
while (j <= mid && a[j] < a[i]) b[p++] = a[j++];
b[p++] = a[i];
}
while (j <= mid) b[p++] = a[j++];
rep(i, l, r) a[i] = b[i];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> Q;
rep(i, 1, n){
int x, y;
cin >> x >> y;
a[++cnt] = {x, y, 0, 0};
}
rep(i, 1, Q){
int typ, x, y;
ll z;
cin >> typ;
if (typ == 1) cin >> x >> y, a[++cnt] = {x, y, 0, 0};
else {
cin >> x >> y >> z;
if (y < 0 || !y && x < 0) x =-x, y =-y, z =-z;
a[++cnt] = {x, y, z, ++cq};
mx[cq] =-4e18, mn[cq] = 4e18;
}
}
cdq(1, cnt);
rep(i, 1, cq) cout << (mx[i] < 0 || mn[i] > 0 ? "YES" : "NO") << '\n';
return 0;
}
```
:::
## 习题
### 统计类
[P1903 【模板】带修莫队 / [国家集训队] 数颜色 / 维护队列](https://www.luogu.com.cn/problem/P1903)
[P3759 [TJOI2017] 不勤劳的图书管理员](https://www.luogu.com.cn/problem/P3759)
[P3658 [USACO17FEB] Why Did the Cow Cross the Road III P](https://www.luogu.com.cn/problem/P3658)
[P4169 [Violet] 天使玩偶/SJY摆棋子](https://www.luogu.com.cn/problem/P4169)
[P6224 [BJWC2014] 数据](https://www.luogu.com.cn/problem/P6224)
[P3157 [CQOI2011] 动态逆序对](https://www.luogu.com.cn/problem/P3157)
[P7312 [COCI 2018/2019 #2] Sunčanje](https://www.luogu.com.cn/problem/P7312)
[P12685 [国家集训队] 排队 加强版](https://www.luogu.com.cn/problem/P12685)
[P10633 BZOJ2989 数列/BZOJ4170 极光](https://www.luogu.com.cn/problem/P10633)
[P10281 [USACO24OPEN] Grass Segments G](https://www.luogu.com.cn/problem/P10281)
[P14340 [JOISC 2019] 考试 / Examination](https://www.luogu.com.cn/problem/P14340)
[P11197 [COTS 2021] 赛狗游戏 Tiket](https://www.luogu.com.cn/problem/P11197)
### DP 类
[P4093 [HEOI2016/TJOI2016] 序列](https://www.luogu.com.cn/problem/P4093)
[P2487 [SDOI2011] 拦截导弹](https://www.luogu.com.cn/problem/P2487)
[P4849 寻找宝藏](https://www.luogu.com.cn/problem/P4849)
[P5621 [DBOI2019] 德丽莎世界第一可爱](https://www.luogu.com.cn/problem/P5621)
[P3769 [CH弱省胡策R2] TATT](https://www.luogu.com.cn/problem/P3769)
### 其他类
[P4027 [NOI2007] 货币兑换](https://www.luogu.com.cn/problem/P4027)