C/C++教程

【Coel.学习笔记】【半途跑路】CDQ 分治

本文主要是介绍【Coel.学习笔记】【半途跑路】CDQ 分治,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

最近在刷状压 DP,结果发现太难不会做,跑来学点别的。
反正 CSP-S2 之前刷完就行了,吧?
放在数据结构里面是因为 CDQ 分治和数套树能解决的问题差不多,所以放了进去(绝不是因为懒得开一个“离线算法”的 Tag!)

引入

CDQ 分治是一种通过把动态询问/点对问题等离线处理,并分治求解的算法。这种思想最早于 IOI2008 国家集训队选手陈丹琦在其论文 从《Cash》谈一类分治算法的应用 中总结,故得名。

例题讲解

虽然原论文给出的例题是[NOI2007] 货币兑换,但我们先从另一个经典题目——三维偏序讲起。

【模板】三维偏序

洛谷传送门
有 $ n $ 个元素,第 $ i $ 个元素有 $ a_i,b_i,c_i $ 三个属性,设 $ f(i) $ 表示满足 $ a_j \leq a_i $ 且 $ b_j \leq b_i $ 且 $ c_j \leq c_i $ 且 $ j \ne i $ 的 \(j\) 的数量。

对于 $ d \in [0, n-1] $,求 $ f(i) = d $ 的数量。(这里描述用 \(n-1\) 代替右开)

解析:这题有很多做法,如树套树、K-D Tree。这里当然只介绍 CDQ 分治的做法。
假设只有一个属性,即对每个 \(i\) 找到 \(a_j\leq a_i\) 的数量。显然我们只要按照 \(a_i\) 大小排序,那么对于 \(a_i\) 就有 \(i-1\) 个数字比它小。
当属性有两个的时候,同样按照 \(a_i\) 排序,然后从前往后扫描。那么对于 \(i\) 前面的元素 \(j\) 一定有 \(a_j\leq a_i\),任务就变成找 \(b_j\leq b_i\) 的数量。一种办法是先把 \(b\) 离散化,然后用类似树状数组求逆序对的方式求解即可。


当然也可以像归并排序求逆序对一样,先做排序,然后根据 \(i,j\) 在两个分治区间内的情况分类讨论:

  1. 若 \(i,j\) 均在左区间,则对左区间分治求解;
  2. 若 \(i,j\) 均在右区间,则对右区间分治求解;
  3. 若 \(i,j\) 分别在左、右区间(显然 \(i\) 一定在 \(j\) 右边),那么对于左区间的任何一个元素,寻找 \(b_j\leq b_i\) 的数量即可。由于分治同时会把 \(b\) 排序,所以对 \(i,j\) 各开一个指针,双指针扫描一遍就可以求出答案。

这样每次分治都要做一次 \(O(n)\) 的双指针,总复杂度为 \(O(n\log n)\)。


现在来到三维。类比上面提到的归并排序方法,先做一遍排序。那么对于每个 \(i\),满足条件的 \(j\) 一定在左边。同样,我们对 \(i,j\) 的情况做分类讨论:同在左区间,同在右区间,分别位于两个区间。前两个分治即可,重点看第三个。

由于做了排序,所以 \(a_j\leq a_i\) 一定满足。 接下来对于 \(b_j\leq b_i\),用双指针寻找第一个 \(b_j> b_i\) 的位置,那么每个 \(i\) 都可以用 \(O(n)\) 找到对应的 \(j\),即答案在左区间边界到 \(j-1\) 的范围内。最后对于 \(c_j\leq c_i\),用树状数组就可求出。

这样每次分治都要做 \(O(n)\) 双指针,且每次移动指针都要用 \(O(\log n)\) 的树状数组维护答案,故总时间复杂度为 \(O(n\log ^2 n)\)。

总结一下,CDQ 分治的本质其实就是消除偏序维度。对于一维,直接排序;对于二维。在一维的基础上做树状数组/分治;对于三维,再在二维基础上加。从某种程度上说,CDQ 分治也可以看作归并排序一类分治的扩展算法。顺带一提,树套树求三维偏序的本质也是消除维度,不过是用权值线段树代替分治、支持强制在线罢了。

#include<cctype>
#include<cstdio>
#include<algorithm>
#include<cstring>

struct __Coel_FastIO {
#ifndef LOCAL
#define _getchar_nolock getchar_unlocked
#define _putchar_nolock putchar_unlocked
#endif

    inline __Coel_FastIO& operator>>(int& x) {
        x = 0;
        bool f = false;
        char ch = _getchar_nolock();
        while (!isdigit(ch)) {
            if (ch == '-') f = true;
            ch = _getchar_nolock();
        }
        while (isdigit(ch)) {
            x = x * 10 + ch - '0';
            ch = _getchar_nolock();
        }
        if (f) x = -x;
        return *this;
    }

    inline __Coel_FastIO& operator<<(int x) {
        if (x < 0) {
            x = -x;
            _putchar_nolock('-');
        }
        static int buf[35];
        int top = 0;
        do {
            buf[top++] = x % 10;
            x /= 10;
        } while (x);
        while (top) _putchar_nolock(buf[--top] + '0');
        return *this;
    }

    inline __Coel_FastIO& operator<<(char x) {
        return _putchar_nolock(x), *this;
    }

} qwq;

const int maxn = 2e5 + 10;

int n, m, top = 1;
int ans[maxn];

struct node {
    int a, b, c;
    int res, cnt; //记录出现次数,处理属性相等的状态
    bool operator<(const node &x) const {
        if (a != x.a) return a < x.a;
        if (b != x.b) return b < x.b;
        return c < x.c;
    }
    bool operator==(const node &x) const {
        return a == x.a && b == x.b && c == x.c;
    }
} q[maxn], tem[maxn];

class Fenwick_Tree {
    private:
#define lowbit(x) (x & (-x))
        int c[maxn];
    public:
        void add(int x, int v) {
            for (int i = x; i < maxn; i += lowbit(i)) c[i] += v;
        }
        int query(int x) {
            int res = 0;
            for (int i = x; i; i -= lowbit(i)) res += c[i];
            return res;
        }
} T;

void CDQ_Divide(int l, int r) {
    if (l >= r) return;
    int mid = (l + r) >> 1;
    CDQ_Divide(l, mid), CDQ_Divide(mid + 1, r);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r) //两个区间都没遍历完时
        if (q[i].b <= q[j].b) //不满足条件,把信息存入树状数组
            T.add(q[i].c, q[i].cnt), tem[k++] = q[i++];
        else // 满足条件,在树状数组上得到答案
            q[j].res += T.query(q[j].c), tem[k++] = q[j++];
    while (i <= mid) T.add(q[i].c, q[i].cnt), tem[k++] = q[i++];
    while (j <= r) q[j].res += T.query(q[j].c), tem[k++] = q[j++]; //将没有处理完的部分继续处理
    for (i = l; i <= mid; i++) T.add(q[i].c, -q[i].cnt); //还原树状数组
    for (i = l, j = 0; j < k; i++, j++) q[i] = tem[j]; //将归并排序后结果复制到原数据中
}

int main(void) {
    qwq >> n >> m;
    for (int i = 0; i < n; i++)
        qwq >> q[i].a >> q[i].b >> q[i].c, q[i].cnt = 1;
    std::sort(q, q + n);
    for (int i = 1; i < n; i++) { // 特判属性相等时的答案
        // 这里的 top 其实也起到了去重的作用
        if (q[i] == q[top - 1]) q[top - 1].cnt++;
        else q[top++] = q[i];
    }
    CDQ_Divide(0, top - 1);
    for (int i = 0; i < top; i++)
        ans[q[i].res + q[i].cnt - 1] += q[i].cnt;
    for (int i = 0; i < n; i++)
        qwq << ans[i] << '\n';
    return 0;
}
这篇关于【Coel.学习笔记】【半途跑路】CDQ 分治的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!