CDQ分治
这是一个比较人类智慧的算法,尽管它大多数时候都不是出题人想要考察的算法,但是绝大部分时候出题人都没办法卡掉你然后愤然强制在线。
在怎样的情况下才能使用 cdq 分治?一般有如下情况:
-
解决点对问题
。在算点对贡献时,我们将贡献拆成三类
此时我们发现可以将整个序列拆成两段,
和 。我们可以递归地处理第一种和第三种点对,我们只要想办法合并信息处理第二类点对即可。 -
动态规划的优化和转移
这类题目一般是一维的 dp 式,时间复杂度达到了
级别,而我们有时可以用 cdq 分治做到 转移。下文我们将提到这种应用。
-
把动态问题转换成静态问题
这是老套路了,有的题目会有修改操作和查询操作,我们可以离线询问,对时间序列分治,下文也会给出详细解释。
点对问题
经典问题,对每个
在 solve(l, r)
中,假设我们已经解决了 solve(l, mid)
和 solve(mid + 1, r)
,问题来到了如何求
先将区间内部的点排序,那么对于每个
以下是代码,注意一些精细化实现
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] 动态逆序对
给定
依题意,在删除第
这不经典三维偏序?贴出另一种 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 一般也长成偏序样,即
聪明的你发现这就是个二维最长上升子序列的式子,直接转移总复杂度
时,说明已经计算好了, 。- 递归处理
- 处理
的转移 - 递归处理
第三步你发现其实和求三维偏序是类似的。我们将所有的
那么这种转移的正确性怎么证明?不会漏转移某些状态吗?
我们在
注意到我们的算法流程,我们在两次递归的中间,处理了
以 solve(1,4)
为例,我们尝试分析算法的执行过程
- 执行
solve(1,1)
,即计算出 。 - 执行
solve(1,2)
, 可以从 推出。 - 执行
solve(2,2)
,发现 已经被计算了。 solve(1,2)
结束,这段区间内的 均计算完成。- 执行
solve(1,4)
, 的值可以推出。 - 执行
solve(3,3)
, 已经被计算了。 - 执行
solve(3,4)
, 可被 转移。 - 执行
solve(3,4)
,因为 在solve(1,4)
中 已经被 转移过了,因此 已被转移。 - 执行
solve(4,4)
, 已经被计算了。 solve(3,4)
结束,这段区间内 均计算完成。solve(1,4)
结束,这段区间内 均计算完成。
其实中序遍历一棵树已经说明了我们肯定是按顺序更新的每个值,因此 cdq 分治优化的正确性得证。
[SDOI2011] 拦截导弹
搞得跟 2011年 才发现 cdq 分治似的。
求最长的二维不上升子序列长度,和每个点被包含在多少个最长的二维不上升子序列中。
容易写出朴素 dp:
接下来考虑如何求每个点在哪几个最长不上升子序列中。
我们可以考虑优化状态设计:
表示以第 枚导弹结尾的方案中最长不上升子序列的长度 表示以第 枚导弹开头的方案中最长不上升子序列的长度 表示到第 枚导弹结尾最长的不上升子序列方案总数 表示从第 枚导弹开始最长的不上升子序列方案总数
第一问的答案显然是
用 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 分治的应用远不止于此,事实上,只要满足了性质,大部分时候都是能离线并且分治的。这里就不再赘述了,后续可以通过一些题目来巩固加深。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探