CDQ分治
与普通分治的区别
普通分治中,每一个子问题只解决它本身(可以说是封闭的)
分治中,对于划分出来的两个子问题,前一个子问题用来解决后一个子问题而不是它本身适用的情况
在很多问题中(比如大多数数据结构题),经常需要处理一些动态问题
然而对动态问题的处理总是不如静态问题来的方便,于是就有了分治
但使用分治的前提是问题必须具有以下两个性质:- 修改操作对询问的贡献独立,修改操作互不影响效果
- 题目允许使用离线算法。
一般步骤
- 将整个操作序列分为两个长度相等的部分(分)
- 递归处理前一部分的子问题(治1)
- 计算前一部分的子问题中的修改操作对后一部分子问题的影响(治2)
- 递归处理后一部分子问题(治3)
特别说明:
在整个过程中,最核心的就是步骤3
此时前一部分子问题中的修改操作相对后一部分子问题来说是静态处理,因此可以更加方便地计算后一部分子问题
CDQ 分治:
入门篇:l0nl1f3:简谈CDQ分治
1 概述
CDQ分治是啥?
会处理操作对于询问的影响吗?
对于一坨操作和询问,分成两半,单独处理左半边和处理左半边对于右半边的影响,就叫CDQ分治。
名字是为了纪念金牌选手陈丹琦
2 实现
将左右区间按一定规律排序后分开处理,递归到底时直接计算答案,对于一个区间,按照第二关键字split成两个区间,先处理左区间,之后因为整个区间是有序的,就可以根据左区间来推算右区间的答案,最后递归处理右区间即可。拿此题做比方,先把全区间按照x坐标排序,然后自左向右用前一半(按时间排序)的修改来更新后一半的查询,之后将整个区间按照时间分成两个部分,递归处理。归纳起来就是
1. 区间分成两半
2. 计算左区间对右区间的贡献
3. 撤销修改
4. 把操作分成两半
5. 递归下去做
CDQ分治 && HDU 5126
参考文献:从《Cash》谈一类分治算法的应用 — 陈丹琦
也因此简称CDQ分治。
个人理解CDQ分治和普通分治最大的区别为:
普通分治可以将问题拆成几个相互独立的子问题
CDQ分治将问题分成了两个部分:
- 相互独立的两个子问题
- 两个子问题之间的联系
因此,CDQ分治的算法流程也分为以下几步:
- 算法开始
- 取一个中间点mid, 将问题分为[L, mid] 和 [mid + 1, R]两个部分。分别处理两个子问题
- 处理[L, mid] 和 [mid + 1, R]之间的联系
举个例子:在二维坐标系中,有n个点,m个询问。对于每个询问(x, y),求点(x1, x2) ,x1<= x && y1 <= y 有多少个。
当然,这题用树状数组就直接能写了。但是我们在这里用CDQ分治来解决一下这个问题。
我们将n个点看做是n个更新,并和m个询问放在一起。定义一个结构体用vector存起来。
-
void sovle1(int l, int r, vector<Struct>vec1) {
-
//在这里将vec1[l, r]按x从小到大排序
-
int mid = (l + r) / 2;
-
solve1(l, mid);
-
solve1(mid + 1, r); //划分成相互独立的子问题
-
//将vec1[l, mid]中的更新操作 和 vec1[mid + 1, r]中的询问操作,存到一个vec2中
-
solve2(l, r, vec2); //左半边的更新操作和右半边的询问操作之间有联系
-
}
-
void solve2(int l, int r, vector<Struct>vec2) {
-
//此时在solve2中已经不需要管x了。在vec2中所有的更新操作的x必然小于等于询问操作的x。
-
-
//在这里将vec2[l, r]按y从小到大排序
-
int mid = (l + r) / 2;
-
sovle2(l, mid);
-
solve2(mid + 1, r); //独立子问题
-
//将vec2[l, mid]中的更新操作和vec2[mid + 1, r]中得询问操作存到一个vec3中
-
solve3(l, r, vec3); //
-
}
-
void solve3( int l, int r, vector<Struct>vec3) {
-
//此时在solve3中,x和y都是已经排好序的。从前到后遍历该更新更新该询问询问就好。
-
int cal = 0;
-
for(Struct tmp: vec3) {
-
if(tmp.type == Update) ++cal;
-
else tmp.query_anser += cal;
-
}
-
}
-
//这段代码其中有许多可以优化的地方,但是不要在意细节,主要是为了更加方便地理解算法
虽然解法不如树状数组,但是我们得看到题目扩展后的好处。
我们看到,当到solve2时,函数中的参数vec2
已经不用再管他的x了(请务必深刻理解为什么不用再管了
)。
这样的好处在哪里?没错,降维!
假如我们要处理的不是二维,而是三维四维依旧可以这么处理,每一个维度只不过加了O(log)的复杂度而已。
如果是随时可以更新查询的操作,只不过也只是把时间当成一个维度而已。(光是这一点就已经牛逼哄哄够解决大部分题目了。)
然后就是下面这道题,三维且可随时更新查询:
HDU 5126 starts: 传送门
-
-
-
-
-
-
-
using namespace std;
-
-
-
-
-
typedef long long ll;
-
typedef pair< int, int>pii;
-
const int N = 5e4 + 100;
-
int que[N << 1];
-
int ans[N];
-
int z_num;
-
int node[N << 1];
-
struct Query {
-
int x, y, z, kind, id;
-
Query() {}
-
Query( int _x, int _y, int _z, int _kind, int _id) : x(_x), y(_y), z(_z), kind(_kind), id(_id) {}
-
}queries[N << 3], queries_x[N<< 3], queries_y[N << 3];
-
bool cmpx(Query a, Query b) {
-
return a.x < b.x || (a.x == b.x && a.kind < b.kind);
-
}
-
bool cmpy(Query a, Query b) {
-
return a.y < b.y || (a.y == b.y && a.kind < b.kind);
-
}
-
inline int lowbit(int x) { return x&-x;}
-
void update(int pos, int value) {
-
while(pos <= z_num) {
-
node[pos] += value;
-
pos += lowbit(pos);
-
}
-
}
-
int query(int pos) {
-
int ret = 0;
-
while(pos) {
-
ret += node[pos];
-
pos -= lowbit(pos);
-
}
-
return ret;
-
}
-
void solve3(int l, int r) { //只剩下二维y和z,直接就是二维的更新查询题目,树状数组解决就好了
-
if(l >= r) return;
-
FOR(i, l, r)
-
if(queries_y[i].kind == 0) update(queries_y[i].z, 1);
-
else if(queries_y[i].kind == 1) ans[ queries_y[i].id ] -= query(queries_y[i].z);
-
else if(queries_y[i].kind == 2) ans[ queries_y[i].id ] += query(queries_y[i].z);
-
FOR(i, l, r)
-
if(queries_y[i].kind == 0) update(queries_y[i].z, -1);
-
}
-
void solve2(int l, int r) { //把x这一维度消除
-
if(l >= r) return;
-
int mid = (l+r)>> 1;
-
solve2(l, mid);
-
solve2(mid + 1, r);
-
int num = 0;
-
FOR(i, l, mid) if(queries_x[i].kind == 0) queries_y[num++] = queries_x[i];
-
FOR(i, mid+ 1, r) if(queries_x[i].kind) queries_y[num++] = queries_x[i];
-
sort(queries_y, queries_y + num, cmpy);
-
solve3( 0, num - 1);
-
}
-
void solve1(int l, int r) { //已经默认时间从小到大排序了,把时间这一维度消除
-
if(l >= r) return;
-
int mid = (l+r)>> 1;
-
solve1(l, mid);
-
solve1(mid + 1, r);
-
int num = 0;
-
FOR(i, l, mid) if(queries[i].kind == 0) queries_x[num++] = queries[i];
-
FOR(i, mid + 1, r) if(queries[i].kind) queries_x[num++] = queries[i];
-
sort(queries_x, queries_x + num, cmpx); //对x从小到大排序
-
solve2( 0, num - 1);
-
}
-
int main() {
-
int casnum, n;
-
int x1, x2, y1, y2, z1, z2, kind;
-
cin >> casnum;
-
while(casnum--) {
-
memset(ans, -1, sizeof(ans));
-
cin >> n;
-
int id = 0;
-
z_num = 0;
-
REP(i, n) {
-
scanf( "%d", &kind);
-
if(kind == 1) {
-
scanf( "%d%d%d", &x1, &y1, &z1);
-
queries[id++] = Query(x1, y1, z1, 0, i);
-
que[z_num++] = z1;
-
} else {
-
ans[i] = 0;
-
scanf( "%d%d%d%d%d%d", &x1, &y1, &z1, &x2, &y2, &z2);
-
queries[id++] = Query(x2 , y2 , z2 , 2, i);
-
queries[id++] = Query(x2 , y2 , z1 - 1, 1, i);
-
queries[id++] = Query(x2 , y1 - 1, z2 , 1, i);
-
queries[id++] = Query(x1 - 1, y2 , z2 , 1, i);
-
queries[id++] = Query(x2 , y1 - 1, z1 - 1, 2, i);
-
queries[id++] = Query(x1 - 1, y2 , z1 - 1, 2, i);
-
queries[id++] = Query(x1 - 1, y1 - 1, z2 , 2, i);
-
queries[id++] = Query(x1 - 1, y1 - 1, z1 - 1, 1, i);
-
que[z_num++] = z2;
-
que[z_num++] = z1 - 1;
-
}
-
}
-
sort(que, que + z_num);
-
z_num = unique(que, que + z_num) - que;
-
REP(i, id) queries[i].z = lower_bound(que, que + z_num, queries[i].z) - que + 1;
-
solve1( 0, id - 1);
-
REP(i, n)
-
if(ans[i] != -1)
-
printf( "%d\n", ans[i]);
-
}
-
return 0;