CDQ分治

这是一个比较人类智慧的算法,尽管它大多数时候都不是出题人想要考察的算法,但是绝大部分时候出题人都没办法卡掉你然后愤然强制在线。

在怎样的情况下才能使用 cdq 分治?一般有如下情况:

  • 解决点对问题 (i,j)

    在算点对贡献时,我们将贡献拆成三类

    • i[1,mid],j[1,mid]
    • i[1,mid],j[mid+1,n]
    • i[mid+1,n],j[mid+1,n]

    此时我们发现可以将整个序列拆成两段,(1,mid)(mid+1,n)。我们可以递归地处理第一种和第三种点对,我们只要想办法合并信息处理第二类点对即可。

  • 动态规划的优化和转移

    这类题目一般是一维的 dp 式,时间复杂度达到了 n2 级别,而我们有时可以用 cdq 分治做到 logn 转移。

    下文我们将提到这种应用。

  • 把动态问题转换成静态问题

    这是老套路了,有的题目会有修改操作和查询操作,我们可以离线询问,对时间序列分治,下文也会给出详细解释。

点对问题

经典问题,对每个 d[0,n) 求 满足 ajai,bjbi,cjci,ijj 的个数有 d 个 的 i 的个数。我们发现这是一个三维偏序,先考虑按某一维排序消去一个影响。

solve(l, r) 中,假设我们已经解决了 solve(l, mid)solve(mid + 1, r),问题来到了如何求 ljmid,mid+1ir 这种点对上。由于排序过,左侧点不再对右侧点做出贡献。因此只要算出有多少满足 bjbi,cjcij 的数量。

先将区间内部的点排序,那么对于每个 i,对应的可能的答案是一段前缀,接下来就是如何统计在这个前缀中满足 cjci 的数量,这里显然使用树状数组。

以下是代码,注意一些精细化实现

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] 动态逆序对

给定 ai,同时给定一个删除元素的顺序,问删除某个元素之前逆序对的个数。

依题意,在删除第 i 个点之前,有效的点对必须满足 timj>timi,(i<jai>aj)(i>jai<aj)

这不经典三维偏序?贴出另一种 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 一般也长成偏序样,即

fi=maxj=1i1{[aj<ai][bj<bi]fj}+1

聪明的你发现这就是个二维最长上升子序列的式子,直接转移总复杂度 O(n2)。但是我们发现 (i,j) 也是一种点对,我们同样可以利用类似算法解决。过程比较套路。

  • l=r 时,说明已经计算好了,dpldpl+1
  • 递归处理 l,mid
  • 处理 ljmid,mid+1ir 的转移
  • 递归处理 mid+1,r

第三步你发现其实和求三维偏序是类似的。我们将所有的 ij 按照 a 排序,用双指针将 j 加入树状数组,查询前缀最大值更新 i 即可。

那么这种转移的正确性怎么证明?不会漏转移某些状态吗?

我们在 n2 dp 时是有保证在计算 i 之前,j<i 都已经更新完了,也就是我们要证明,在 cdq 分治下,更新 i 之前,我们已经把所有有用的点加入了树状数组。

注意到我们的算法流程,我们在两次递归的中间,处理了 ljmid,mid+1ir,如果将 cdq 分治看成一棵分治树,那么 cdq 分治的执行过程更像是在中序遍历这棵树。

solve(1,4) 为例,我们尝试分析算法的执行过程

  • 执行 solve(1,1),即计算出 f1
  • 执行 solve(1,2)f2 可以从 f1 推出。
  • 执行 solve(2,2),发现 f2 已经被计算了。
  • solve(1,2) 结束,这段区间内的 f 均计算完成。
  • 执行 solve(1,4)f3 的值可以推出。
  • 执行 solve(3,3)f3 已经被计算了。
  • 执行 solve(3,4)f4 可被 f3 转移。
  • 执行 solve(3,4),因为 f4solve(1,4) 中 已经被 f1,f2 转移过了,因此 f4 已被转移。
  • 执行 solve(4,4)f4 已经被计算了。
  • solve(3,4) 结束,这段区间内 f 均计算完成。
  • solve(1,4) 结束,这段区间内 f 均计算完成。

其实中序遍历一棵树已经说明了我们肯定是按顺序更新的每个值,因此 cdq 分治优化的正确性得证。

[SDOI2011] 拦截导弹

搞得跟 2011年 才发现 cdq 分治似的。

求最长的二维不上升子序列长度,和每个点被包含在多少个最长的二维不上升子序列中。

容易写出朴素 dp:fi=maxj=1i1{[hj<hi][vj<vi]fj}+1,这个东西和 cdq 优化板子如出一辙。

接下来考虑如何求每个点在哪几个最长不上升子序列中。

我们可以考虑优化状态设计:

  • fi 表示以第 i 枚导弹结尾的方案中最长不上升子序列的长度
  • gi 表示以第 i 枚导弹开头的方案中最长不上升子序列的长度
  • si 表示到第 i 枚导弹结尾最长的不上升子序列方案总数
  • ti 表示从第 i 枚导弹开始最长的不上升子序列方案总数

第一问的答案显然是 ans=max{fi},第二问的答案略显复杂,记 S=siti[fi+gi=ans1],答案为 sitiS,减 1 是因为 fg 中都算上了 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 分治的应用远不止于此,事实上,只要满足了性质,大部分时候都是能离线并且分治的。这里就不再赘述了,后续可以通过一些题目来巩固加深。

posted @   MisterRabbit  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示