学习笔记:cdq 分治

· · 算法·理论

介绍

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<ij=i 特殊处理,作为单点贡献)。
左区间下标范围是 [l,mid],右区间下标范围是 [mid+1,r]
由于 mid<mid+1,显然 \forall j \in [l,mid],i \in [mid+1,r]ji 都有贡献。
因此 cdq 分治过程的更新显然是正确的。

将分治树画出来:(借用一下 OI Wiki 的图)

可以发现,\forall j \le iji 的贡献在 [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)