其他分享
首页 > 其他分享> > CDQ分治学习笔记

CDQ分治学习笔记

作者:互联网

CDQ分治

用于解决偏序问题。
《算法竞赛进阶指南》中,称CDQ分治为“基于时间的分治算法”,其实是偏序问题的一种特殊形式。

二维偏序

在学习线段树和树状数组时,已经可以利用排序+数据结构 \(O(N\log{N})\) 解决二维偏序问题。同样,CDQ分治也行。
有 \(N\) 个元素,每个元素有 \(a,b,c\) 三个属性。求对于每个元素 \(i\),满足 \(a_j≤a_i\) 且 \(b_j≤b_i\) 且 \(j≠i\) 的 \(j\) 的数量。
先将所有元素按 \(a\) 为第一关键字,\(b\) 为第二关键字排序。再在排序好的数组上进行以 \(b\) 为关键字的归并排序,并在归并时进行统计。
具体来说,将数组分成两段,对两段分别进行归并排序。此步骤结束后,要求每段中 \(b\) 有序。而由于原数组已经按照 \(a\) 排序,所以左段中的 \(a\) 一定小于等于右段中的 \(a\)。那么利用双指针可以 \(O(N)\) 求出:对于右段的每个数,左段中有多少个数 \(b\) 不大于它。
是不是感觉思路很像求逆序对?实际上逆序对也是二维偏序问题,而且其中一维(下标)已经有序。
另外,关于两个元素相等的情况,如上的算法显然会漏算。解决的方法也非常简单,只需将相等的元素合并,做上标记,更新答案时特殊处理即可。

三维偏序

如果真正理解了二维偏序,三维偏序也很好解决。套上一个树状数组即可。
将所有元素按 \(a\) 为第一关键字,\(b\) 为第二关键字,\(c\) 为第三关键字排序。再在排序好的数组上进行以 \(b\) 为关键字的归并排序。在归并的过程中维护以 \(c\) 为“下标”,“权值”为出现次数的树状数组,并利用其性质更新答案。
可以自己验证其正确性。

模板题
Code

#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e5 + 5, M = 2e5 + 5;
int n, m, t[M], ans[N];
struct Node {
    int a, b, c, cnt, res;
    bool operator <(const Node &o) const {
        return a != o.a ? a < o.a : (b != o.b ? b < o.b : c < o.c);
    }
    bool operator ==(const Node &o) const {
        return a == o.a && b == o.b && c == o.c;
    }
} x[N], tmp[N];
int read() {
    int x = 0; char c = getchar();
    while (c < '0' || c > '9') c = getchar();
    while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
    return x;
}
void Init() {
    sort(x + 1, x + n + 1);
    int j = 0;
    for (int i = 1; i <= n; i++)
        if (j && x[i] == x[j]) x[j].cnt++;
        else x[++j] = x[i];
    n = j;
}
void Add(int x, int v) {
    for (; x <= m; x += x & -x) t[x] += v;
}
int Ask(int x) {
    int res = 0;
    for (; x; x -= x & -x) res += t[x];
    return res;
}
void CDQ(int l, int r) {
    if (l == r) return ;
    int mid = l + r >> 1;
    CDQ(l, mid); CDQ(mid + 1, r);
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r)
        if (x[i].b <= x[j].b) {
            Add(x[i].c, x[i].cnt); tmp[k++] = x[i++];
        }
        else {
            x[j].res += Ask(x[j].c); tmp[k++] = x[j++];
        }
    while (i <= mid) {
        Add(x[i].c, x[i].cnt); tmp[k++] = x[i++];
    }
    while (j <= r) {
        x[j].res += Ask(x[j].c); tmp[k++] = x[j++];
    }
    for (i = l; i <= mid; i++) Add(x[i].c, -x[i].cnt); //memset很慢,减回去更快
    for (i = l; i <= r; i++) x[i] = tmp[i];
}
int main() {
    int s;
    s = n = read(); m = read();
    for (int i = 1; i <= n; i++) x[i] = (Node){read(), read(), read(), 1};
    Init();
    CDQ(1, n);
    for (int i = 1; i <= n; i++) ans[x[i].res + x[i].cnt - 1] += x[i].cnt;
    for (int i = 0; i < s; i++) printf("%d\n", ans[i]);
    return 0;
}

基于时间的分治算法

将时间(时间戳)看作一个维度。对查询操作产生影响的修改操作一定在其之前,可以发现有偏序的性质。那么许多在线问题可以转化为离线,利用CDQ分治求解。

例:树状数组模板
先将区间求和转化为求前缀和。加入时间维度,可以发现问题变成了二维偏序,分别为“时间”和“下标”。另外,其实此题还有一个隐藏的维度 \(k\),表示操作种类。设修改为0,查询为1,则可以抽象成更具普适性的三维偏序。然而此题 \(k\) 只有两种取值,无需再维护树状数组,只需朴素地判断即可。做法同二维偏序。

点击查看代码
#include<cstdio>
using namespace std;
const int N = 1e5 + 5;
int n, m, ans[N], q;
struct Node {
    int k, p, v, id;
    bool operator <(const Node &oth) const {
        return p != oth.p ? p < oth.p : k < oth.k;
    }
} a[3 * N], tmp[3 * N];
int read() {
    int x = 0, f = 1; char c = getchar();
    while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
    while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
    return x * f;
}
void CDQ(int l, int r) {
    if (l == r) return ;
    int mid = l + r >> 1;
    CDQ(l, mid); CDQ(mid + 1, r);
    int sum = 0, i = l, j = mid + 1, tot = l;
    while (i <= mid && j <= r)
        if (a[i] < a[j]) {
            if (a[i].k == 1) sum += a[i].v;
            tmp[tot++] = a[i++];
        }
        else {
            if (a[j].k == 2) ans[a[j].id] += sum * a[j].v;
            tmp[tot++] = a[j++];
        }
    while (i <= mid) tmp[tot++] = a[i++];
    while (j <= r) {
        if (a[j].k == 2) ans[a[j].id] += sum * a[j].v;
        tmp[tot++] = a[j++];
    }
    for (i = l; i <= r; i++) a[i] = tmp[i];
}
int main() {
    n = read(); m = read();
    for (int i = 1; i <= n; i++) a[i] = (Node){1, i, read()};
    for (int i = 1; i <= m; i++) {
        int opt = read(), x = read(), y = read();
        if (opt == 1) a[++n] = (Node){1, x, y};
        else {
            a[++n] = (Node){2, y, 1, ++q};
            a[++n] = (Node){2, x - 1, -1, q};
        }
    }
    CDQ(1, n);
    for (int i = 1; i <= q; i++) printf("%d\n", ans[i]);
    return 0;
}

同样,可以再加一维,变成四维(三维)偏序。例:莫基亚

点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 2e5 + 5, M = 5e5 + 5;
int n, m, t[M], idx, ans[N];
struct Node {
    int k, x, y, t, v, id;
    bool operator <(const Node &o) const {
        return t != o.t ? t < o.t : x != o.x ? x < o.x : y != o.y ? y < o.y : k < o.k;
    }
} q[N << 2], tmp[N << 2];
int read() {
    int x = 0; char c = getchar();
    while (c < '0' || c > '9') c = getchar();
    while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
    return x;
}
void Add(int x, int v) {
    for (; x <= m; x += x & -x) t[x] += v;
}
int Ask(int x) {
    int res = 0;
    for (; x; x -= x & -x) res += t[x];
    return res;
}
void CDQ(int l, int r) {
    if (l == r) return ;
    int mid = l + r >> 1;
    CDQ(l, mid); CDQ(mid + 1, r);
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r)
        if (q[i].x <= q[j].x) {
            if (!q[i].k) Add(q[i].y, q[i].v);
            tmp[k++] = q[i++];
        }
        else {
            if (q[j].k) ans[q[j].id] += Ask(q[j].y) * q[j].v;
            tmp[k++] = q[j++];
        }
    while (i <= mid) {
        if (!q[i].k) Add(q[i].y, q[i].v);
        tmp[k++] = q[i++];
    }
    while (j <= r) {
        if (q[j].k) ans[q[j].id] += Ask(q[j].y) * q[j].v;
        tmp[k++] = q[j++];
    }
    for (i = l; i <= mid; i++)
        if (!q[i].k) Add(q[i].y, -q[i].v);
    for (i = l; i <= r; i++) q[i] = tmp[i];
}
int main() {
    int opt;
    m = read();
    while (scanf("%d", &opt), opt != 3) {
        if (opt == 1) q[++n] = (Node){0, read(), read(), n, read()};
        else {
            int x1 = read(), y1 = read(), x2 = read(), y2 = read();
            q[++n] = (Node){1, x2, y2, n, 1, ++idx};
            q[++n] = (Node){1, x2, y1 - 1, n, -1, idx};
            q[++n] = (Node){1, x1 - 1, y2, n, -1, idx};
            q[++n] = (Node){1, x1 - 1, y1 - 1, n, 1, idx};
        }
    }
    sort(q + 1, q + n + 1);
    CDQ(1, n);
    for (int i = 1; i <= idx; i++) printf("%d\n", ans[i]);
    return 0;
}

CDQ分治的思想还是挺简单的。

标签:偏序,int,分治,mid,笔记,CDQ,数组,排序
来源: https://www.cnblogs.com/Fisher-Y/p/16125895.html