初学cdq分治学习笔记(可能有第二次的学习笔记)

前言骚话

本人蒟蒻,一开始看到模板题就非常的懵逼,链接,学到后面就越来越清楚了。
吐槽,cdq,超短裙分治。。。。(尴尬)

正片开始

思想

和普通的分治,还是分而治之,但是有一点不一样的是一般的分治在合并问题答案是,左右区间是分开来的,也就是左区间的答案不会对右区间的答案造成贡献,但是cdq分治要处理的就是左区间对于右区间的答案。
很多情况下,cdq分治都可以解决掉一维的答案,简单的来说就是直接去掉一个嵌套的数据结构,简直将代码量降至低谷,但是有一个很明显的缺点就是只能实现离线操作。QwQ


还是和题目一起将比较好,我们从\(2\)维偏序一直讲到n维偏序吧,(滑稽)
那么考虑偏序性的问题,最重要的是要保证答案的正确性,因为当前的处理不能影响到后面的状态。
二维偏序:第一维:排序解决,第二维:归并排序cdq或者是树状数组都可以,虽然有一点超出了cdq的范围,但是还是可以用cdq来实现的。
三维偏序:第一维:排序解决,第二维:cdq分治,第三维:树状数组。还有一种写法就是后两维用树套树来做。这个时候就非常明显可以体现出cdq分治的优越性了。树套树:代码直奔100行,cdq一般是不会超过70行。


详细讲一下:https://www.cnblogs.com/chhokmah/p/10571403.html,注意这里讲的不怎么详细
首先将第一维排序,因为不是逆序对,那么就不需要维护编号,然后将所有一样的数都合并起来,因为我们也是要统计所有相同的个数。那么我们开始第二维的操作,因为有了第一维的顺序的限制,那么我们就不能随意的查找,这时候我们就是要开始求逆序对了。还是将现在区间进行排序,从小到大,这样我们分治,在将所有右区间内遍历一遍,如果右区间中有y小于左区间内的数,那么就在当前这个z上加上个数,表示这一段都是由答案的贡献,然后将答案统计在新的数组中就可以了。

#include <bits/stdc++.h>
#define ll long long
#define ms(a, b) memset(a, b, sizeof(a))
#define inf 0x3f3f3f3f
using namespace std;
template <typename T>
inline void read(T &x) {
    x = 0; T fl = 1;
    char ch = 0;
    while (ch < '0' || ch > '9') {
        if (ch == '-') fl = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    x *= fl;
}
#define N 100005
struct data {
    int x, y, z, res, cnt;
    data() {
        x = y = z = res = cnt = 0;
    }
}a[N], v[N];
int n, k;
int ans[N];
struct BIT{
    #define lowbit(x) (x&-x)
    int n, tr[N];
    void add(int x, int val) {
        for (; x <= n; x += lowbit(x)) tr[x] += val;
    }
    int query(int x) {
        int res = 0;
        for (; x; x -= lowbit(x)) res += tr[x];
        return res;
    }
}tr;
bool cmp1(const data &a, const data &b) {
    if (a.x == b.x) 
        if (a.y == b.y) return a.z < b.z;
        else return a.y < b.y;
    else return a.x < b.x;
}
bool cmp2(const data &a, const data &b) {
    if (a.y == b.y) return a.z < b.z;
    else return a.y < b.y;
}
void cdq(int l, int r) {
    if (l == r) return;
    int mid = (l + r) >> 1;
    cdq(l, mid); 
    cdq(mid + 1, r);
    sort(v + l, v + mid + 1, cmp2);
    sort(v + mid + 1, v + r + 1, cmp2);
    int l1 = l, l2 = mid + 1;
    while (l2 <= r) {
        while (l1 <= mid && v[l1].y <= v[l2].y) 
            tr.add(v[l1].z, v[l1].cnt), l1 ++;
        v[l2].res += tr.query(v[l2].z); 
        l2 ++;
    }
    for (int i = l; i < l1; i ++) tr.add(v[i].z, -v[i].cnt);
}
int main() {
    read(n); read(k);
    tr.n = k;
    for (int i = 1; i <= n; i ++) {
        read(a[i].x); read(a[i].y); read(a[i].z);
    }
    sort(a + 1, a + 1 + n, cmp1);
    int tot = 0;
    for (int i = 1, j = 1; i <= n; i = j) {
        v[++ tot] = a[i];
        while (a[i].x == a[j].x && a[i].y == a[j].y && a[i].z == a[j].z && j <= n) 
            j ++, v[tot].cnt ++;
    }
    cdq(1, tot);
    for (int i = 1; i <= tot; i ++) 
        ans[v[i].res + v[i].cnt] += v[i].cnt;
    for (int i = 1; i <= n; i ++) printf("%d\n", ans[i]);
    return 0;
}

关于四位偏序:第一维:排序,第二维:cdq分治,第三维:套一个cdq分治,第四位,树状数组。
和上面的思路差不多,只是代码更加冗长,但是想到树套树套树可能要飙到200+就感觉到非常欣慰。
没有时间写,就不写了。


关于五维偏序:我选择\(O(n^2)\)。这样可能更快。如果是用cdq来做的话,复杂度目测是\({log_2^n}^4\),而且代码复杂度为cdq套cdq套树套树,想想就可怕。


对于cdq分治的注意事项:千万不要用复杂度大的c++自带函数,比如说是memset,这样你会找都找不到自己错在哪里。
为了简化时间复杂度与常数,有几种方法可以参考:
(1)在分治之前先按照某一关键字排序,之后在分治过程中,将信息按照时间分成前后两部分,这样避免了多次排序。
(2)在分治过程中,利用归并排序的方式将两个有序序列合并,将O(nlog)的排序变为O(n)的归并。
(3)在分治过程中,利用树状数组解决问题,除非必须用到别的东西。
(4)在分治过程中,利用有序的性质可以发现,逆序也是有序的,并且满足一些正好与正序相反,这样可以避免重复排序。
(5)在分治之前尽可能的简化不必要的信息,这样能减少整个代码的常数。
(6)另外,在更新右区间或者合并的时候,尽量选择常数与时间复杂度较小的算法,比如说能用单调队列就不要用斜率优化,能用斜率优化就不要用决策单调性。


在举一个例子:动态逆序对
题解咕咕一会:【传送门】

#include <bits/stdc++.h>
#define ll long long
#define N 100005
using namespace std;
struct BIT{
    #define lowbit(x) (x&-x)
    int n, tr[N];
    void add(int x, int val) {
        for (; x <= n; x += lowbit(x)) tr[x] += val;
    }
    int query(int x) {
        int res = 0;
        for (; x; x -= lowbit(x)) res += tr[x];
        return res;
    }
}tr;
struct Que {
    int cnt, v, d, id, t;
}q[N << 1];
int n, m;
ll ans[N];
int a[N], pos[N];
bool cmp(const Que &a, const Que &b) {
    return a.d < b.d;
}
void cdq(int l, int r) {
    if (l == r) return;
    int mid = (l + r) >> 1;
    cdq(l, mid); 
    cdq(mid + 1, r);
    sort(q + l, q + mid + 1, cmp);
    sort(q + mid + 1, q + 1 + r, cmp);
    int l1 = l, l2 = mid + 1;
    while (l2 <= r) {
        while (l1 <= mid && q[l1].d <= q[l2].d) tr.add(q[l1].v, q[l1].cnt), ++ l1;
        ans[q[l2].id] += q[l2].cnt * (tr.query(n) - tr.query(q[l2].v));
        l2 ++;
    }
    for (int i = l; i < l1; i ++) tr.add(q[i].v, -q[i].cnt);
    l1 = r; l2 = mid;
    while (l1 > mid) {
        while (l2 >= l && q[l2].d >= q[l1].d) tr.add(q[l2].v, q[l2].cnt), -- l2;
        ans[q[l1].id] += q[l1].cnt * tr.query(q[l1].v - 1);
        l1 --;
    } 
    for (int i = mid; i > l2; i --) tr.add(q[i].v, -q[i].cnt);
}
int main() {
    scanf("%d%d", &n, &m);
    int tot = 0;
    tr.n = n;
    for (int i = 1; i <= n; i ++) {
        scanf("%d", &a[i]);
        pos[a[i]] = i;
        q[++ tot] = (Que){1, a[i], i, 0, tot};
    }
    for (int i = 1; i <= m; i ++) {
        int x; scanf("%d", &x);
        q[++ tot] = (Que){-1, x, pos[x], i, tot};
    }
    cdq(1, tot);
    for (int i = 1; i <= m; i ++) ans[i] += ans[i - 1];
    for (int i = 0; i < m; i ++) printf("%lld\n", ans[i]);
    return 0;
}
posted @ 2019-03-21 15:58 chhokmah 阅读(...) 评论(...) 编辑 收藏