离线分治算法——CDQ分治详解

a 夏天 提交于 2020-01-17 18:02:55

以前做过的许多题目,都是在线算法,也就是对与一系列的询问,利用某种数据结构逐个求出答案。

而今天学了一种离线算法——CDQ分治,是将全部的询问放在一起,利用分治一同处理。

CDQ分治,由2008年国际信息学奥林匹克竞赛(IOI)金牌女选手陈丹琦在国家集训队中引入而得名,为算法竞赛界中的一个广泛称呼。

CDQ分治有两种,分别是基于时间的分治基于值域的整体分治

下面看就开始吧。


1.基于时间的分治

BZOJ2716–天使玩具
【题意】
Ayu在七年前曾经收到过一个天使玩偶,当时她把它当做时间囊埋在了地下。
而七年后的今天,Ayu却忘了她把天使玩偶埋在了哪里,所以她决定仅凭一点模糊的记忆来寻找它。
我们把Ayu生活的小镇看做一个二维平面直角坐标系,而Ayu会不定时的记起可能在某个点(x,y)埋下了天使玩偶。
或者Ayu会询问你,假如她在(x,y),那么她离最近的天使玩偶可能埋下的地方有多远。
因为Ayu只会沿着平行坐标轴的方向来行动,所以在这个问题里我们定义两个点之间的距离为曼哈顿距离:
dist(A,B)=|Ax−Bx|+|Ay−By|
其中Ax,Ay表示点A的横坐标,其余类似。
【输入格式】
第一行包含两个整数n和m,在刚开始时,Ayu已经知道有n个点可能埋着天使玩偶,接下来Ayu要进行m次操作。
接下来n行,每行两个非负整数xi,yi,表示初始n个点的坐标。
再接下来m行,每行三个非负整数 t,x,y 。
如果t=1,表示Ayu又回忆起了一个可能埋着玩偶的点(x,y)。
如果t=2,表示Ayu询问如果她在坐标(x,y),那么在已经回忆出的点里,离她最近的那个点有多远。
【输出格式】
对于每个t=2的询问,在单独的一行内输出该询问的结果。
【数据范围】
n,m5105n,m≤5∗10^5,坐标范围为01060 -10^6
【输入样例】
2 3
1 1
2 3
2 1 2
1 3 3
2 4 2
【输出样例】
1
2

我们一般先解决该问题的简化版——假设没有t=1的操作。这时,平面上有n个点(xi,yi)(x_i,y_i),然后询问与(x,y)(x,y)最近的点有多远,答案为:
min(abs(x-xi)+abs(y-yi))
于是我们考虑将绝对值拆开,分为左上、右上、左下、右下四个位置
对于左下方为例,此时要求的式子变为:
min(x-xi+y-yi)
化简得到:
(x+y)-max(xi+yi)
然后就好办了,我们先将每一个坐标按x为第一关键字,y为第二关键字排序。然后用树状数组来维护[0,y]从左到右不断维护就可以了。
对于其他三个方向,做法也是如此。

然后我们加入修改操作。我们先将插入和询问合并在一起,然后进行分治。为什么要分治呢,举个例子:
在这里插入图片描述
在这个图中,我们假设橙色是查询,绿色是修改
然后我们就对其他进行分治
在这里插入图片描述
先是把它分成1-4和5-8,用树状数组记录1-4中的修改,给5-8的询问加上这些修改。
然后再分(拿1-4为例子),分成1-2和3-4,用树状数组记录,给3-4中的询问加上
后面同理。
总的来讲,就是把当前的分成两份,用树状数组记录1-mid里面的修改操作,然后给mid+1-r里面的询问操作加上这些结果
因此,我们刚好使每个询问都经过了全部的操作,时间复杂度O(nlog2n)O(nlog^2n)

参考代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
using namespace std;
const int u = 1000010;
struct rec { int x, y, z; };
rec a[u]; // 原始问题的操作序列(长度为n+m)
rec b[u]; // 静态问题的坐标(按横坐标排序)及其在a中的下标
int c[u], tot; // 树状数组、坐标的最大范围
int ans[u], n, m, t;
 
bool operator <(const rec &a, const rec &b) {
    return a.x < b.x || a.x == b.x && a.y < b.y;
}
 
int ask(int x) {
    int y = -(1 << 30);
    for (; x; x -= x & -x) y = max(y, c[x]);
    return y;
}
 
void insert(int x, int y) {
    for (; x < tot; x += x & -x) c[x] = max(c[x], y);
}
 
// 求解简化版问题,需要考虑b[st~ed]的坐标,根据4个方向的不同,
// 横坐标顺序为de(±1),树状数组维护的信息用系数dx,dy(±1)指定
void calc(int st, int ed, int de, int dx, int dy) {
    for (int i = st; i != ed; i += de) {
        int y = dy == 1 ? b[i].y : tot - b[i].y;
        int temp = dx*b[i].x + dy*b[i].y;
        if (a[b[i].z].z == 1) insert(y, temp);
        else ans[b[i].z] = min(ans[b[i].z], abs(temp - ask(y)));
    }
    for (int i = st; i != ed; i += de) { // 还原树状数组
        int y = dy == 1 ? b[i].y : tot - b[i].y;
        if (a[b[i].z].z == 1) // 撤销修改
            for (int j = y; j < tot; j += j & -j) c[j] = -(1 << 30);
    }
}
 
void cdqdiv(int l, int r) {
    int mid = (l + r) >> 1;
    if (l < mid) cdqdiv(l, mid);
    if (mid + 1 < r) cdqdiv(mid + 1, r);
    t = 0;
    for (int i = l; i <= r; i++)
        if (i <= mid && a[i].z == 1 || i > mid && a[i].z == 2)
            b[++t] = a[i], b[t].z = i;
    // 此处排序可以优化掉(放在外边并适当修改写法)
    sort(b + 1, b + t + 1);
    calc(1, t + 1, 1, 1, 1), calc(t, 0, -1, -1, -1);
    calc(1, t + 1, 1, 1, -1), calc(t, 0, -1, -1, 1);
}
 
int main() {
    cin >> n >> m; m += n;
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &a[i].x, &a[i].y), a[i].y++, a[i].z = 1;
    for (int i = n + 1; i <= m; i++)
        scanf("%d%d%d", &a[i].z, &a[i].x, &a[i].y), a[i].y++;
    for (int i = 1; i <= m; i++) tot = max(tot, a[i].y);
    tot++;
    memset(c, 0xcf, sizeof(c));
    memset(ans, 0x3f, sizeof(ans));
    cdqdiv(1, m);
    for (int i = 1; i <= m; i++)
        if (a[i].z == 2) printf("%d\n", ans[i]);
}


2.基于值域的整体分治

其实,这种方式和前一种是大同小异的。
只不过这种是以值进行分治,上一种是以时间进行分治

bzoj1901
【题意】
公司动态排名开发了一种新型计算机,不再满足于简单地找到给定N个数的第k个最小数。他们开发了一个更强大的系统,对于N个数a[1],a[2],…,a[N],你可以问它:a[i],a[i+1],…,a[j]的第k个最小数是多少?(i<=j,0<k<=j-i+1)。你甚至可以改变一些a[i]的值,并继续查询。
你的任务是为这台计算机编写一个程序
从输入中读取N个数字(1 <= N <= 50000)
处理输入的M指令(1 <= M <= 10000)。这些指令包括查询a[i],a[i+1],…,a[j]的第k个最小数,或将a[i]改为t。
【输入】
输入的第一行是单个数字X(0<X<=4),即输入的测试用例的数量。然后每个X块代表一个测试用例。
每个块的第一行包含两个整数N和M,表示N个数和M个指令。接下来是N行。第i+1行代表数字a[i]。然后是M行,格式如下
Q i j k
或者
C i t
它表示查询a[i],a[i+1],…,a[j]的第k个数,并分别将a[i]变为t。保证在任何操作时间,任何数字a[i]是小于1000000000的非负整数。
两个连续测试用例之间没有空行。
【输出】
对于每个查询操作,输出一个整数来表示结果。(即a[i],a[i+1],…,a[j]的第k个最小数)
两个连续测试用例之间没有空行。
【输入样例】
2
5 3
3 2 1 4 7
Q 1 4 3
C 2 6
Q 2 5 3
5 3
3 2 1 4 7
Q 1 4 3
C 2 6
Q 2 5 3

【输出样例】
3
6
3
6

本来,这道题是要用主席树来做的,但由于没有要求强制在线,所以我们可以用CDQ分治来做。
和前面一样,我们先不看修改操作,就当成不用修改的区间第k小。
引入一个问题,让你在一个区间里面求第k小的数是几。
有这样一种想法,二分每一个数,求小于等于这个数的数有多少个,我们令这个值为t
如果t<=k,就在mid右边,否则就在左边。
加上修改后,同样,我们可以把这个思想放在分治里面
在当前的分治中,我们不断统计值在l-mid中数的个数,如果当前的询问k小于等于这个数,就把这个询问放到l-mid里面分治,否则就令k减去这个数,放到mid+1到r中分治
时间复杂度也是O(nlog2n)O(nlog^2n)

参考代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100006, INF = 1e9;
struct rec { int op, x, y, z; } q[3*N], lq[3*N], rq[3*N];
int T, n, m, t, p, a[N], c[N], ans[N];
int cnt, b[3*N];

int ask(int x) {
	int y = 0;
	for (; x; x -= x & -x) y += c[x];
	return y;
}

void change(int x, int y) {
	for (; x <= n; x += x & -x) c[x] += y;
}

void solve(int l, int r, int st, int ed) {
	if (st > ed) return;
	if (l == r) {
		for (int i = st; i <= ed; i++) 
			if (q[i].op > 0) ans[q[i].op] = l;
		return;
	}
	int mid = (l + r)>>1;
	int lt = 0, rt = 0;
	for (int i = st; i <= ed; i++)
		if (q[i].op <= 0) {
			if (q[i].y <= mid) change(q[i].x, q[i].z), lq[++lt] = q[i];
			else rq[++rt] = q[i];
		}
		else {
			int cnt = ask(q[i].y) - ask(q[i].x - 1);
			if (cnt >= q[i].z) lq[++lt] = q[i];
			else q[i].z -= cnt, rq[++rt] = q[i];
		}
	for (int i = ed; i >= st; i--)
		if (q[i].op <= 0 && q[i].y <= mid) change(q[i].x, -q[i].z);
	for (int i = 1; i <= lt; i++) q[st+i-1] = lq[i];
	for (int i = 1; i <= rt; i++) q[st+lt+i-1] = rq[i];
	solve(l, mid, st, st + lt - 1);
	solve(mid + 1, r, st + lt , ed);
}

int main() {
	cin >> T;
	while (T--) {
		cin >> n >> m;
		t = p = cnt = 0;
		for (int i = 1; i <= n; i++) {
			int val; scanf("%d", &val);
			q[++t].op = 0, q[t].x = i, q[t].y = val, q[i].z = 1;
			a[i] = val; b[++cnt] = val;
		}
		for (int i = 1; i <= m; i++) {
			char op[2]; scanf("%s", op);
			if (op[0] == 'Q') {
				int l, r, k; scanf("%d%d%d", &l, &r, &k);
				q[++t].op = ++p, q[t].x = l, q[t].y = r, q[t].z = k;
			}
			else {
				int x, y; scanf("%d%d", &x, &y); b[++cnt] = y;
				q[++t].op = -1, q[t].x = x, q[t].y = a[x], q[t].z = -1;
				q[++t].op = 0, q[t].x = x, q[t].y = y, q[t].z = 1;
				a[x] = y;
			}
		}
		sort (b + 1, b + cnt + 1);
		int k = unique(b + 1, b + cnt + 1) - b - 1;
		for (int i = 1; i <= t; i++)
			if (q[i].op <= 0) q[i].y = lower_bound(b + 1, b + k + 1, q[i].y) - b;
		solve(0, INF, 1, t);
		for (int i = 1; i <= p; i++) printf("%d\n", b[ans[i]]);
	}
	return 0;
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!