古典叙事 · 技术札记

序章

山河有卷
人间有声

写代码,也写长风、旧城与未熄的灯。
愿每一篇随笔,都有自己的山水与回声。
阅览随笔

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,因为统计问题无后效性!
}
posted @ 2026-09-08 20:51  Morphis‘  阅读(6)  评论(0)    收藏  举报