【总结】CDQ分治

来源:互联网 发布:丧尸围城2优化补丁 编辑:程序博客网 时间:2024/05/21 06:28

总的来说,CDQ分治与普通分治不一样的地方在于,CDQ分治的对象是时间。即对于一个时间段[L, R],我们取mid = (L + R) / 2,(用数据结构题举例)分治的每层只考虑mid之前的修改对mid之后的查询的贡献,然后递归到[L,mid],(mid,R]。

显然,CDQ分治是一种离线算法,我们需要将所有的修改/查询存下来,一起进行操作。

同时,CDQ分治还需要满足:操作之间相互独立,即一个操作的存在不会影响到另一个操作的存在。


那么这是一个经典入门题:【BZOJ1176: [Balkan2007]Mokia】

二维单点修改,矩形查询。

首先修改/查询之间的顺序就是时间,我们按时间来分治。

但是由于是二维的,为了省空间不开二维的数据结构,我们一开始把所有操作按x轴排序,每次处理我们只需要扫描一下x轴,把y轴的修改/查询放到一个一维树状数组里就好了。

(另外,矩形查询拆为4个前缀和查询)

具体步骤如下:

(1)把操作离线下来(离线的时候要记录每个操作的时间,即操作的编号),然后按x轴排序。(必要时离散化)

(2)分治区间[L, R](L, R为时间,即操作的编号),取mid,从L到R遍历操作,将mid之前的修改加入一维树状数组,将mid之后的查询在一维树状数组里查询。

(3)注意要还原修改操作,从L到R遍历操作,遇到修改操作,将修改操作改回去。(如果是+c,那么-c)

(4)开一个临时数组,两个指针(一个指针指向左区间的左端点,另一个指针指向右区间的左端点),从L到R遍历操作,将mid之前的操作放到左区间,将mid之后的操作放到右区间。最后把临时数组赋值给操作数组。

(5)递归[L, mid],[mid + 1, R],回到(2)。

递归的终点是L==R。

注意,在分治的每一层的区间内,x轴都是有序的。虽然时间无序,但是我们遍历操作时按照mid来划分了。(这个大概是CDQ分治比较难懂的地方)

看代码吧。

/* Pigonometry */#include <cstdio>#include <algorithm> using namespace std; const int maxn = 2000005, maxm = 640005, maxq = 10005; int n, tr[maxn], ans[maxq]; struct _data {    int opt, id, qid, x, y, c;} c[maxm], tmp[maxm]; inline int iread() {    int f = 1, x = 0; char ch = getchar();    for(; ch < '0' || ch > '9'; ch = getchar()) f = ch == '-' ? -1 : 1;    for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';    return f * x;} inline void add(int x, int c) {    for(; x <= n; x += x & -x) tr[x] += c;} inline int sum(int x) {    int res = 0;    for(; x; x -= x & -x) res += tr[x];    return res;} inline int cmp(_data a, _data b) {    return a.x != b.x ? a.x < b.x : a.y != b.y ? a.y < b.y : a.opt < b.opt;} inline void cdq(int l, int r) {    if(l == r) return;    int mid = l + r >> 1;    for(int i = l; i <= r; i++)        if(c[i].opt == 1 && c[i].id <= mid) add(c[i].y, c[i].c);        else if(c[i].opt == 2 && c[i].id > mid) ans[c[i].qid] += c[i].c * sum(c[i].y);    for(int i = l; i <= r; i++)        if(c[i].opt == 1 && c[i].id <= mid) add(c[i].y, -c[i].c);    int h1 = l, h2 = mid;    for(int i = l; i <= r; i++)        if(c[i].id <= mid) tmp[h1++] = c[i];        else tmp[++h2] = c[i];    for(int i = l; i <= r; i++) c[i] = tmp[i];    cdq(l, mid); cdq(mid + 1, r);} int main() {    int s = iread(); n = iread(); int tot = 0, id = 0;    while(1) {        int opt = iread();        if(opt == 3) break;         if(opt == 1) {            int x = iread(), y = iread(), w = iread();            c[++tot] = (_data){1, tot, 0, x, y, w};        } else {            int x1 = iread(), y1 = iread(), x2 = iread(), y2 = iread(); id++;            c[++tot] = (_data){2, tot, id, x2, y2, 1};            c[++tot] = (_data){2, tot, id, x1 - 1, y2, -1};            c[++tot] = (_data){2, tot, id, x2, y1 - 1, -1};            c[++tot] = (_data){2, tot, id, x1 - 1, y1 - 1, 1};        }    }    sort(c + 1, c + 1 + tot, cmp);    cdq(1, tot);    for(int i = 1; i <= id; i++) printf("%d\n", ans[i]);    return 0;}
此题的S并没有用。


另外CDQ分治还可以处理偏序问题。

比如【BZOJ3262: 陌上花开】

有n个物品,每个物品有三个属性a, b, c。对于物品X,需要查询有多少物品Y满足Xa >= Ya, Xb >= Yb, Xc >= Yc。物品Y的个数即为X的等级。

我们发现,这个问题里并没有时间,但是我们可以转化一下,比如将属性a当做时间(时间本来也是一种序)。

所以思路是,先按a排序,将a编号,把a当做时间。然后按b排序(如果不理解,多多理解上个题),那么我们遍历操作的时候,只需要在一维树状数组里面查询有多少比c小的物品就行了。

下面是代码(很久之前的)

#include <cstdio>#include <algorithm>#define rec(i, x, n) for(int i = x; i <= n; i++)const int maxn = 100005, maxk = 200005;int n, k, tot, tr[maxk], ans[maxn];struct _flower {int a, b, c, cnt, ans;bool operator != (const _flower &x) const {return a != x.a || b != x.b || c != x.c;}} f[maxn], c[maxn];inline int iread() {int f = 1, x = 0; char ch = getchar();for(; ch < '0' || ch > '9'; ch = getchar()) f = ch == '-' ? -1 : 1;for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';return f * x;}bool cmp1(_flower x, _flower y) {return x.a != y.a ? x.a < y.a : x.b != y.b ? x.b < y.b : x.c < y.c;}bool cmp2(_flower x, _flower y) {return x.b != y.b ? x.b < y.b : x.a < y.a;}void add(int x, int w) {for(; x <= k; x += x & -x) tr[x] += w;}int sum(int x) {int res = 0;for(; x; x -= x & -x) res += tr[x];return res;}void cdq(int l, int r) {if(l == r) {f[l].ans += f[l].cnt - 1;return;}int mid = l + r >> 1, q1 = l, q2 = mid;rec(i, l, r) f[i].a <= mid ? c[q1++] = f[i] : c[++q2] = f[i];rec(i, l, r) f[i] = c[i];for(int i = mid + 1, j = l; i <= r; i++) {for(; j <= mid && f[j].b <= f[i].b; j++)add(f[j].c, f[j].cnt);f[i].ans += sum(f[i].c);}for(int i = l; i <= mid && f[i].b <= f[r].b; i++) add(f[i].c, -f[i].cnt);cdq(l, mid); cdq(mid + 1, r);}int main() {n = iread(); k = iread();rec(i, 1, n) f[i].a = iread(), f[i].b = iread(), f[i].c = iread();std::sort(f + 1, f + 1 + n, cmp1);rec(i, 1, n) {if(f[i] != f[i - 1]) f[++tot] = f[i];++f[tot].cnt;}rec(i, 1, tot) f[i].a = i;std::sort(f + 1, f + 1 + tot, cmp2);cdq(1, tot);rec(i, 1, tot) ans[f[i].ans] += f[i].cnt;rec(i, 0, n - 1) printf("%d\n", ans[i]);return 0;}
注意给a编号那句。


另外还有四维偏序

【BZOJ1790: [Ahoi2008]Rectangle 矩形藏宝地】

现在有了四个属性a, b, c, d,对于物品X,问是否有物品Y满足Xa >= Ya, Xb >= Yb, Xc <= Yc, Xd <= Yd。

我们沿用上题的思路,还是将一维当做时间。但注意,我们这里选择将c作为时间轴,原因在下面。

将c排序,编号,当做时间。然后将d排序。

对于a,b的处理,我们发现我们只需要知道“存在性”即可,那么对于物品X,我们就需要在[1, Xa]里查找是否有物品Y满足Xb >= Yb,这是个RMQ问题,我们用线段树维护。

即把a当做线段树的下标,把b当做线段树的权值,线段树维护区间最小值。

代码(还是很久之前的)

#include <cstdio>#include <cstring>#include <algorithm>#define rec(i, x, n) for(int i = x; i <= n; i++)const int maxn = 200005, inf = 0x3f3f3f3f;int n, tot, rank[maxn], tr[maxn << 2];struct _mat {int a, b, c, d, ans; // a up, b up, c down, d down} c[maxn], tmp[maxn];inline int iread() {int f = 1, x = 0; char ch = getchar();for(; ch < '0' || ch > '9'; ch = getchar()) f = ch == '-' ? -1 : 1;for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';return f * x;}void pushup(int p) {tr[p] = std::min(tr[p << 1], tr[p << 1 | 1]);}bool cmp1(_mat x, _mat y) {return x.c > y.c;}bool cmp2(_mat x, _mat y) {return x.d > y.d;}void change(int p, int l, int r, int x, int w) {if(l == r && r == x) {tr[p] = w;return;}int mid = l + r >> 1;if(x <= mid) change(p << 1, l, mid, x, w);else change(p << 1 | 1, mid + 1, r, x, w);pushup(p);}int query(int p, int l, int r, int x, int y) {if(x > y) return inf;if(x <= l && r <= y) return tr[p];int mid = l + r >> 1;int res = inf;if(x <= mid) res = std::min(res, query(p << 1, l, mid, x, y));if(y > mid) res = std::min(res, query(p << 1 | 1, mid + 1, r, x, y));return res;}void cdq(int l, int r) {if(l == r) return;int mid = l + r >> 1, q1 = l, q2 = mid;rec(i, l, r) tmp[c[i].c <= mid ? q1++ : ++q2] = c[i];rec(i, l, r) c[i] = tmp[i];for(int i = mid + 1, j = l; i <= r; i++) {for(; j <= mid && c[j].d > c[i].d; j++) change(1, 1, tot, c[j].a, c[j].b);if(c[i].b > query(1, 1, tot, 1, c[i].a - 1)) c[i].ans = 1;}for(int i = l; i <= mid && c[i].d > c[r].d; i++) change(1, 1, tot, c[i].a, inf);cdq(l, mid); cdq(mid + 1, r);}int main() {n = iread();rec(i, 1, n) {int x1 = iread(), y1 = iread(), x2 = iread(), y2 = iread();c[i] = (_mat){rank[i] = x1, y1, x2, y2};}std::sort(rank + 1, rank + 1 + n);tot = std::unique(rank + 1, rank + 1 + n) - rank - 1;std::sort(c + 1, c + 1 + n, cmp1);rec(i, 1, n) c[i].c = i, c[i].a = std::lower_bound(rank + 1, rank + 1 + tot, c[i].a) - rank;std::sort(c + 1, c + 1 + n, cmp2);memset(tr, 0x3f, sizeof(tr));cdq(1, n);int ans = 0;rec(i, 1, n) ans += c[i].ans;printf("%d\n", ans);return 0;}
回顾一下为什么把c当做时间。

如果我们把a当做时间,把b排序,那么我们就得在[Xc, inf)里寻找是否有物品Y满足Xd <= Yd。这个太复杂了,inf有可能非常大,即使离散化了,常数也会比较大。


还有一个题,和Mokia一样,而且数据范围更小,当做CDQ分治的练习吧。

【BZOJ2683: 简单题】

0 0
原创粉丝点击