cdq分治
CDQ 分治的核心思想是“用时间换空间,用分治降维度”。
当我们面对多维偏序问题(如三维 \(x_i \le x_j, y_i \le y_j, z_i \le z_j\))时:
第一维 (\(x\)):初始排序(通常是数组原本的顺序或按 \(x\) 排序)。
第二维 (\(y\)):在分治过程中进行排序(合并时保证左半边和右半边分别按 \(y\) 有序),用双指针扫描。
第三维 (\(z\)):用树状数组(BIT)维护,左边负责加入,右边负责查询。
流派适用场景执行顺序为什么这么排?
流派 A:统计类求三维偏序对数量(如陌上花开)左 -> 右 -> 合并无后效性,各块内部的对数是独立的,加起来就行。
流派 B:DP类LIS 变体,求带偏序限制的最长子序列/最大价值左 -> 合并 -> 右严格的拓扑序! 右边内部的转移必须建立在左边已经完全更新它的基础之上。如果不还原原序,时间线就乱了。
赛场模板一:DP 优化型 CDQ (求最大值 / LIS)适用特征:\(DP[j] = \max(DP[i] + val)\),且要求 \(i < j, y_i \le y_j, z_i \le z_j\)。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
using i64 = long long;
const i64 INF = 1e18;
const i64 LIMIT = 3000005; // 树状数组最大范围,务必根据题目开够!
struct node {
i64 x, y, z;
i64 id; // 原数组下标(如果是离线查询题可能需要)
} p[100005];
i64 dp[100005];
i64 tr[LIMIT + 5];
// 基础维度比较:按原序(x)排
bool cmp_x(const node& a, const node& b) {
return a.x < b.x;
}
// 分治维度比较:按 y 排,y 相同务必按 z 排!
bool cmp_y(const node& a, const node& b) {
if (a.y != b.y) return a.y < b.y;
return a.z < b.z;
}
i64 lowbit(i64 x) { return x & -x; }
// 【关键】求最大值的 BIT
void update(i64 x, i64 k) {
if (x <= 0) x = 1; // 赛场保命装:防 0 死循环,防负数越界
for (; x <= LIMIT; x += lowbit(x)) tr[x] = max(tr[x], k);
}
i64 query(i64 x) {
if (x <= 0) return -INF;
i64 ans = -INF;
for (; x; x -= lowbit(x)) ans = max(ans, tr[x]);
return ans;
}
void clear(i64 x) {
if (x <= 0) x = 1;
for (; x <= LIMIT; x += lowbit(x)) tr[x] = -INF; // DP求最大值必须恢复极小值
}
void cdq_dp(i64 l, i64 r) {
if (l == r) {
// 【基底处理】如果是从前往后的序列DP,在这里可以继承上一个状态或赋予初始值
// if (p[l].x > 0) dp[p[l].x] = max(dp[p[l].x - 1], dp[p[l].x]);
return;
}
i64 mid = (l + r) >> 1;
// 1. 递归左半边 (算出左半边的最终 DP 值)
cdq_dp(l, mid);
// 2. 左右两半分别按 y 排序,准备合并
sort(p + l, p + mid + 1, cmp_y);
sort(p + mid + 1, p + r + 1, cmp_y);
// 3. 经典双指针转移 (左边提供数据,右边查询并更新)
int i = l, j = mid + 1;
while (j <= r) {
while (i <= mid && p[i].y <= p[j].y) {
update(p[i].z, dp[p[i].x]); // 这里填入你要转移的权值
i++;
}
i64 mx = query(p[j].z);
if (mx != -INF) {
dp[p[j].x] = max(dp[p[j].x], mx + 1); // 这里填转移方程
}
j++;
}
// 4. 只清空本次用过的 BIT,保证 O(Nlog^2N)
for (i64 k = l; k < i; k++) clear(p[k].z);
// 5. 【极其致命】把右半边恢复为按 x 排序!保护拓扑序!
sort(p + mid + 1, p + r + 1, cmp_x);
// 6. 递归右半边
cdq_dp(mid + 1, r);
}
赛场模板二:统计类 CDQ (偏序对统计)适用特征:求满足 \(i < j\) 且 \(y_i \le y_j, z_i \le z_j\) 的点对数量。(注:纯统计类通常使用类似归并排序的写法,更短,且不需要恢复 x 排序)
// BIT 使用普通的 += 求和
void update(i64 x, i64 k) { for (; x <= LIMIT; x += lowbit(x)) tr[x] += k; }
i64 query(i64 x) { i64 ans = 0; for (; x; x -= lowbit(x)) ans += tr[x]; return ans; }
void clear(i64 x) { for (; x <= LIMIT; x += lowbit(x)) tr[x] = 0; } // 计数恢复为 0
i64 ans[100005]; // 记录每个点的符合条件的对数
void cdq_count(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
// 1. 无脑先左右递归
cdq_count(l, mid);
cdq_count(mid + 1, r);
// 2. 排序
sort(p + l, p + mid + 1, cmp_y);
sort(p + mid + 1, p + r + 1, cmp_y);
// 3. 双指针合并统计
int i = l, j = mid + 1;
while (j <= r) {
while (i <= mid && p[i].y <= p[j].y) {
update(p[i].z, 1); // 左边符合条件,入库
i++;
}
ans[p[j].id] += query(p[j].z); // 右边查询累加
j++;
}
// 4. 清空
for (int k = l; k < i; k++) clear(p[k].z);
// 5. 【注意】无需恢复 cmp_x,因为统计问题无后效性!
}

浙公网安备 33010602011771号