CDQ分治
这是一个比较人类智慧的算法,尽管它大多数时候都不是出题人想要考察的算法,但是绝大部分时候出题人都没办法卡掉你然后愤然强制在线。
在怎样的情况下才能使用 cdq 分治?一般有如下情况:
-
解决点对问题 \((i,j)\)。
在算点对贡献时,我们将贡献拆成三类
- \(i\in[1,mid],j\in[1,mid]\)
- \(i\in[1,mid],j\in[mid + 1, n]\)
- \(i\in[mid + 1,n],j\in[mid + 1, n]\)
此时我们发现可以将整个序列拆成两段,\((1,mid)\) 和 \((mid + 1, n)\)。我们可以递归地处理第一种和第三种点对,我们只要想办法合并信息处理第二类点对即可。
-
动态规划的优化和转移
这类题目一般是一维的 dp 式,时间复杂度达到了 \(n^2\) 级别,而我们有时可以用 cdq 分治做到 \(\log n\) 转移。
下文我们将提到这种应用。
-
把动态问题转换成静态问题
这是老套路了,有的题目会有修改操作和查询操作,我们可以离线询问,对时间序列分治,下文也会给出详细解释。
点对问题
经典问题,对每个 \(d\in[0,n)\) 求 满足 \(a_j\le a_i,b_j\le b_i,c_j\le c_i,i\not=j\) 的 \(j\) 的个数有 \(d\) 个 的 \(i\) 的个数。我们发现这是一个三维偏序,先考虑按某一维排序消去一个影响。
在 solve(l, r) 中,假设我们已经解决了 solve(l, mid) 和 solve(mid + 1, r),问题来到了如何求 \(l \le j \le mid,mid + 1\le i\le r\) 这种点对上。由于排序过,左侧点不再对右侧点做出贡献。因此只要算出有多少满足 \(b_j \le b_i,c_j\le c_i\) 的 \(j\) 的数量。
先将区间内部的点排序,那么对于每个 \(i\),对应的可能的答案是一段前缀,接下来就是如何统计在这个前缀中满足 \(c_j \le c_i\) 的数量,这里显然使用树状数组。
以下是代码,注意一些精细化实现
const int N = 1e5 + 5;
int n, k, cnt, f[N];
struct node {
int a, b, c, val, ans;
node() {}
node(int _a, int _b, int _c) : a(_a), b(_b), c(_c) {}
il bool operator!=(const node& _eps) {return a ^ _eps.a || b ^ _eps.b || c ^ _eps.c; }
}o[N], lsh[N];
struct BIT {
#define lowbit(x) (x & (-x))
int tr[N << 1], n;
il void resize(const unsigned& lim) { n = lim; }
il void add(int p, int v) { for(; p <= n; p += lowbit(p)) tr[p] += v; }
il int query(int p) { int sum = 0; for(; p; p -= lowbit(p)) sum += tr[p]; return sum; }
#undef lowbit
} bit;
il bool cmp1(const node& x, const node& y) { return x.a ^ y.a ? x.a < y.a : (x.b ^ y.b ? x.b < y.b : x.c < y.c); }
il bool cmp2(const node& x, const node& y) { return x.b ^ y.b ? x.b < y.b : x.c < y.c; }
il void solve(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
solve(l, mid); solve(mid + 1, r);
sort(lsh + l, lsh + mid + 1, cmp2); sort(lsh + mid + 1, lsh + r + 1, cmp2);
int ul = l;
for (int i = mid + 1; i <= r; ++i) {
while (ul <= mid && lsh[ul].b <= lsh[i].b) bit.add(lsh[ul].c, lsh[ul].val), ++ul;
lsh[i].ans += bit.query(lsh[i].c);
}
for (int i = l; i < ul; ++i) bit.add(lsh[i].c, -lsh[i].val);
}
int main() {
read(n), read(k);
for (int i = 1; i <= n; ++i) o[i] = node(read(), read(), read());
sort(o + 1, o + n + 1, cmp1);
for (int i = 1; i <= n; ++i) {
if (o[i] != o[i - 1]) lsh[++cnt] = o[i];
++lsh[cnt].val;
}
bit.resize(200000);
solve(1, cnt);
for (int i = 1; i <= cnt; ++i) f[lsh[i].ans + lsh[i].val - 1] += lsh[i].val;
for (int i = 0; i < n; ++i) write(f[i]);
return 0;
}
当然我们在上述提到过 cdq 分治可以化动为静,因此我们可以将点对变成动态的。比如下面这道例题
[CQOI2011] 动态逆序对
给定 \(a_i\),同时给定一个删除元素的顺序,问删除某个元素之前逆序对的个数。
依题意,在删除第 \(i\) 个点之前,有效的点对必须满足 \(tim_j > tim_i,(i<j\land a_i > a_j) \lor (i > j \land a_i<a_j)\)。
这不经典三维偏序?贴出另一种 cdq 分治实现方式
点我看代码
const int N = 1e5 + 5;
int n, m;
ll Ans;
struct node {
int t, v, i, ans;
node(){ ans = 0; }
node(int _t, int _v, int _i) : t(_t), v(_v), i(_i) {}
} a[N];
struct BIT {
#define lowbit(x) (x & (-x))
int tr[N], n;
il void resize(const int lim) { n = lim; }
il void add(int p, const int v) { for(; p <= n; p += lowbit(p)) tr[p] += v; }
il int query(int p) { int sum = 0; for (; p; p -= lowbit(p)) sum += tr[p]; return sum; }
#undef lowbit
}bit;
il bool cmp1(const node& x, const node& y) { return x.t < y.t; }
il bool cmp2(const node& x, const node& y) { return x.v < y.v; }
il bool cmp3(const node& x, const node& y) { return x.i < y.i; }
il void solve(int l, int r) {
if (r - l == 1) return;
int mid = (l + r) >> 1;
solve(l, mid); solve(mid, r);
int i = l + 1, j = mid + 1;
while (i <= mid) {
while (a[i].v > a[j].v && j <= r) bit.add(a[j].t, 1), ++j;
a[i].ans += bit.query(m) - bit.query(a[i].t); ++i;
}
i = l + 1, j = mid + 1;
while (i <= mid) {
while (a[i].v > a[j].v && j <= r) bit.add(a[j].t, -1), ++j;
++i;
}
i = mid; j = r;
while (j > mid) {
while(a[j].v < a[i].v && i > l) bit.add(a[i].t, 1), --i;
a[j].ans += bit.query(m) - bit.query(a[j].t); --j;
}
i = mid; j = r;
while (j > mid) {
while(a[j].v < a[i].v && i > l) bit.add(a[i].t, -1), --i;
--j;
}
sort(a + l + 1, a + r + 1, cmp2);
}
int rnk[N];
int main() {
read(n), read(m);
for (int i = 1; i <= n; ++i) read(a[i].v), rnk[a[i].v] = i;
for (int i = 1; i <= m; ++i) a[rnk[read()]].t = i;
for (int i = 1; i <= n; ++i) if (!a[i].t) a[i].t = m;
bit.resize(n);
for (int i = 1; i <= n; ++i) {
Ans += bit.query(n) - bit.query(a[i].v);
bit.add(a[i].v, 1);
}
for (int i = 1; i <= n; ++i) bit.add(a[i].v, -1);
solve(0, n);
sort(a + 1, a + n + 1, cmp1);
for (int i = 1; i <= m; ++i) {
write(Ans); Ans -= a[i].ans;
}
return 0;
}
我也不知道为什么,反正我用板子题写法没过,换成这种过了。
动态规划
cdq 能解决的 dp 一般也长成偏序样,即
\(f_i=\max\limits_{j=1}^{i-1}\{[a_j<a_i][b_j<b_i]f_j\} + 1\)
聪明的你发现这就是个二维最长上升子序列的式子,直接转移总复杂度 \(\mathcal O(n^2)\)。但是我们发现 \((i,j)\) 也是一种点对,我们同样可以利用类似算法解决。过程比较套路。
- \(l=r\) 时,说明已经计算好了,\(dp_l \gets dp_l+1\)。
- 递归处理 \(l,mid\)
- 处理 \(l\le j \le mid, mid + 1 \le i \le r\) 的转移
- 递归处理 \(mid + 1, r\)
第三步你发现其实和求三维偏序是类似的。我们将所有的 \(i\),\(j\) 按照 \(a\) 排序,用双指针将 \(j\) 加入树状数组,查询前缀最大值更新 \(i\) 即可。
那么这种转移的正确性怎么证明?不会漏转移某些状态吗?
我们在 \(n^2\) dp 时是有保证在计算 \(i\) 之前,\(\forall j < i\) 都已经更新完了,也就是我们要证明,在 cdq 分治下,更新 \(i\) 之前,我们已经把所有有用的点加入了树状数组。
注意到我们的算法流程,我们在两次递归的中间,处理了 \(l\le j \le mid, mid + 1\le i \le r\),如果将 cdq 分治看成一棵分治树,那么 cdq 分治的执行过程更像是在中序遍历这棵树。
以 solve(1,4) 为例,我们尝试分析算法的执行过程

- 执行
solve(1,1),即计算出 \(f_1\)。 - 执行
solve(1,2),\(f_2\) 可以从 \(f_1\) 推出。 - 执行
solve(2,2),发现 \(f_2\) 已经被计算了。 solve(1,2)结束,这段区间内的 \(f\) 均计算完成。- 执行
solve(1,4),\(f_3\) 的值可以推出。 - 执行
solve(3,3),\(f_3\) 已经被计算了。 - 执行
solve(3,4),\(f_4\) 可被 \(f_3\) 转移。 - 执行
solve(3,4),因为 \(f_4\) 在solve(1,4)中 已经被 \(f_1,f_2\) 转移过了,因此 \(f_4\) 已被转移。 - 执行
solve(4,4),\(f_4\) 已经被计算了。 solve(3,4)结束,这段区间内 \(f\) 均计算完成。solve(1,4)结束,这段区间内 \(f\) 均计算完成。
其实中序遍历一棵树已经说明了我们肯定是按顺序更新的每个值,因此 cdq 分治优化的正确性得证。
[SDOI2011] 拦截导弹
搞得跟 2011年 才发现 cdq 分治似的。
求最长的二维不上升子序列长度,和每个点被包含在多少个最长的二维不上升子序列中。
容易写出朴素 dp:\(f_i = \max\limits_{j=1}^{i-1}\{[h_j<h_i][v_j<v_i]f_j\} + 1\),这个东西和 cdq 优化板子如出一辙。
接下来考虑如何求每个点在哪几个最长不上升子序列中。
我们可以考虑优化状态设计:
- \(f_i\) 表示以第 \(i\) 枚导弹结尾的方案中最长不上升子序列的长度
- \(g_i\) 表示以第 \(i\) 枚导弹开头的方案中最长不上升子序列的长度
- \(s_i\) 表示到第 \(i\) 枚导弹结尾最长的不上升子序列方案总数
- \(t_i\) 表示从第 \(i\) 枚导弹开始最长的不上升子序列方案总数
第一问的答案显然是 \(ans=\max\{f_i\}\),第二问的答案略显复杂,记 \(S = \sum s_i\cdot t_i[f_i+g_i=ans-1]\),答案为 \(\frac {s_i\cdot t_i}{S}\),减 \(1\) 是因为 \(f\) 和 \(g\) 中都算上了 \(i\) 的贡献。\(f,s\) 两个的 dp 式子都很好写出,\(g,t\) 无非就是对称一下。
用 cdq 分治的时候树状数组内更新会比较麻烦。要注意一下。
点我看代码
const int N = 5e4 + 5;
struct node {
int h, v, id, len; double pro;
} a[N];
int n;
struct BIT {
#define lowbit(p) (p & (-p))
int trf[N]; double trg[N];
il void add(int p, int f, double g) {
for (; p <= n; p += lowbit(p))
if (trf[p] == f) trg[p] += g;
else if (trf[p] < f) trf[p] = f, trg[p] = g;
}
il int askf(int p) {
int ans = 0;
for (; p; p -= lowbit(p)) ans = max(ans, trf[p]);
return ans;
}
il double askg(int p, int f) {
double ans = 0;
for (; p; p -= lowbit(p)) if (trf[p] == f) ans += trg[p];
return ans;
}
il void erase(int p) {
for (; p <= n; p += lowbit(p)) trf[p] = trg[p] = 0;
}
#undef lowbit
} bit;
auto cmp1 = [](const node& x, const node& y) {
return x.h ^ y.h ? x.h > y.h : (x.v ^ y.v ? x.v > y.v : x.id < y.id);
};
auto cmp2 = [](const node& x, const node& y) {
return x.h ^ y.h ? x.h < y.h : (x.v ^ y.v ? x.v < y.v : x.id < y.id);
};
auto cmp3 = [](const node &x, const node& y){ return x.v > y.v; };
auto cmp4 = [](const node &x, const node& y){ return x.v < y.v; };
bool chk(int i, int j, int opt) {
return opt == 1 ? a[i].v >= a[j].v : a[i].v <= a[j].v;
}
il void solve(int l, int r, int opt) {
if (l == r) return;
int mid = (l + r) >> 1;
solve(l, mid, opt);
if (opt == 1) {
sort(a + l, a + mid + 1, cmp3);
sort(a + mid + 1, a + r + 1, cmp3);
}
else {
sort(a + l, a + mid + 1, cmp4);
sort(a + mid + 1, a + r + 1, cmp4);
}
int i = l, j = mid + 1;
while (i <= mid && j <= r) {
while (chk(i, j, opt) && i <= mid) {
bit.add(a[i].id, a[i].len, a[i].pro); ++i;
}
int mx = bit.askf(a[j].id) + 1;
if (a[j].len < mx) {
a[j].len = mx; a[j].pro = bit.askg(a[j].id, mx - 1);
}
else if (a[j].len == mx)
a[j].pro += bit.askg(a[j].id, mx - 1);
++j;
}
while (j <= r) {
int mx = bit.askf(a[j].id) + 1;
if (a[j].len < mx) {
a[j].len = mx; a[j].pro = bit.askg(a[j].id, mx - 1);
}
else if (a[j].len == mx)
a[j].pro += bit.askg(a[j].id, mx - 1);
++j;
}
for (int j = l; j < i; ++j) bit.erase(a[j].id);
if (opt == 1) sort(a + mid + 1, a + r + 1, cmp1);
else sort(a + mid + 1, a + r + 1, cmp2);
solve(mid + 1, r, opt);
}
int f[N], g[N];
double s[N], t[N];
int main() {
read(n);
for (int i = 1; i <= n; ++i) {
read(a[i].h), read(a[i].v); a[i].id = i; a[i].len = 1; a[i].pro = 1;
}
sort(a + 1, a + n + 1, cmp1);
solve(1, n, 1);
int ans = 0;
for (int i = 1; i <= n; ++i) {
f[a[i].id] = a[i].len;
s[a[i].id] = a[i].pro;
ans = max(ans, f[a[i].id]);
}
printf("%d\n", ans);
for (int i = 1; i <= n; ++i) {
a[i].id = n - a[i].id + 1;
a[i].len = a[i].pro = 1;
}
sort(a + 1, a + n + 1, cmp2);
solve(1, n, 2);
double tot = 0;
for (int i = 1; i <= n; ++i) {
g[n - a[i].id + 1] = a[i].len;
t[n - a[i].id + 1] = a[i].pro;
if (g[n - a[i].id + 1] == ans) tot += t[n - a[i].id + 1];
}
for (int i = 1; i <= n; ++i) {
if (f[i] + g[i] == ans + 1)
printf("%.5lf ", 1. * s[i] * t[i] / tot);
else printf("0.00000 ");
}
return 0;
}
当然 cdq 分治的应用远不止于此,事实上,只要满足了性质,大部分时候都是能离线并且分治的。这里就不再赘述了,后续可以通过一些题目来巩固加深。

浙公网安备 33010602011771号