前言
CDQ是谁呢?一位与莫队,hjt一样自创算法或数据结构的大佬……
学习了好几天,总算对CDQ分治有了一点了解
CDQ真的好有用啊,特别是在三维偏序问题上
(那些会KD-tree和树套树的大佬就不要嘲讽我了……)
参考文献:https://www.cnblogs.com/mlystdcall/p/6219421.html
https://blog.csdn.net/wu_tongtong/article/details/78785836
介绍
CDQ分治是一种非常高级的算法,码量小,常数小,可以顶替许多的数据结构,然而缺点是必须离线操作
一般来说,CDQ分为三个步骤:
1.分。将原问题划分为若干子问题,子问题间相互独立且与原问题形式相同。一般来说这些问题包含修改和查询两个操作,用区间$[l,r]$表示,将其分为$[l,mid]$和$[mid+1,r]$两个区间
2.治。递归解决子问题
3.并。将子问题合并为原问题,并考虑$[l,mid]$区间的操作对$[mid+1,r]$的操作的影响
然而这么叽里呱啦说了一大堆似乎没有什么用……还是详细的讲一讲好了
入门:二维偏序
对于一个点$(x,y)$,若存在点$(x_0,y_0)$满足$x_0>x$且$y_0>y$,则称$(x,y)<(x_0,y_0)$
二维偏序问题,就是给出好多个点$(x,y)$,求有多少个点大于它
ps:关于具体是大于还是小于,以及$x$和$y$之间的互相关系,视具体题目而定
实际上,逆序对就是一个二维偏序问题
为什么呢?我们可以把数组中的每一个点表示为$(x,y)$,其中$x$表示在数组中的位置,$y$表示权值。求逆序对个数,就是求有多少点对满足$x<x'$且$y>y'$。不就是一个二维偏序问题么?
逆序对个数我们是怎么求的呢?归并排序
回忆一下归并排序求逆序对的过程。我们每次合并两个区间的时候,要考虑左子区间对右子区间的影响。即,每次从右子区间中取出一个数时,要把“以这个数结尾的逆序对个数”加上“左子区间内比他小的数的个数”,不就是CDQ分治的过程么
再来考虑一般的二维偏序,对于每一个点$(x,y)$,我们可以先排序,使得$x$有序,这样,我们就可以只考虑$y$元素了(考虑一下归并排序求逆序对的过程,实际上数组的位置是默认有序的,于是我们分治的时候只需要考虑它的值就可以了)
如果不用归并的话,也可以用树状数组求逆序对。从左到右考虑每个点,在权值树状数组中加入,每次加入之前在树状数组中查询,看看前面有多少个数比他大就好了
二维偏序问题的拓展
给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作:
操作1:格式为1 x k,把位置x的元素加上k(位置从1标号到N)。
操作2:格式为2 x y,求出区间[x,y]内所有元素的和。
实际上是一道树状数组的裸题(然而好死不死的非要用CDQ分治来做)
我们把每一个操作看成$(x,y)$,其中$x$表示操作到来的时间,$y$表示操作的点(对于查询操作,我们把它拆成$l-1$和$r$两个区间)。时间这一维是默认有序的,于是我们只要在分治的过程中将$y$这一维从小到大合并就可以了
那么如何表示修改和查询呢?我们在每一个操作上记录一个类型type,type为1表示修改,type为2表示被拆出来的$l-1$的区间,要对答案有一个负的贡献,type为3表示被拆出来的$r$的区间,对答案有一个正的贡献
因为实在懒得码了,代码是抄这里的
1 #include <iostream> 2 #include <cstring> 3 #include <algorithm> 4 #include <cstdio> 5 #include <cstdlib> 6 #include <cmath> 7 8 using namespace std; 9 typedef long long ll; 10 const int MAXN = 500001; // 原数组大小 11 const int MAXM = 500001; // 操作数量 12 const int MAXQ = (MAXM<<1)+MAXN; 13 14 int n,m; 15 16 struct Query { 17 int type, idx; ll val; 18 bool operator<( const Query &rhs ) const { // 按照位置从小到大排序,修改优先于查询 19 return idx == rhs.idx ? type < rhs.type : idx < rhs.idx; 20 } 21 }query[MAXQ]; 22 int qidx = 0; 23 24 ll ans[MAXQ]; int aidx = 0; // 答案数组 25 26 Query tmp[MAXQ]; // 归并用临时数组 27 void cdq( int L, int R ) { 28 if( R-L <= 1 ) return; 29 int M = (L+R)>>1; cdq(L,M); cdq(M,R); 30 ll sum = 0; 31 int p = L, q = M, o = 0; 32 while( p < M && q < R ) { 33 if( query[p] < query[q] ) { // 只统计左边区间内的修改值 34 if( query[p].type == 1 ) sum += query[p].val; 35 tmp[o++] = query[p++]; 36 } 37 else { // 只修改右边区间内的查询结果 38 if( query[q].type == 2 ) ans[query[q].val] -= sum; 39 else if( query[q].type == 3 ) ans[query[q].val] += sum; 40 tmp[o++] = query[q++]; 41 } 42 } 43 while( p < M ) tmp[o++] = query[p++]; 44 while( q < R ) { 45 if( query[q].type == 2 ) ans[query[q].val] -= sum; 46 else if( query[q].type == 3 ) ans[query[q].val] += sum; 47 tmp[o++] = query[q++]; 48 } 49 for( int i = 0; i < o; ++i ) query[i+L] = tmp[i]; 50 } 51 52 int main() { 53 scanf( "%d%d", &n, &m ); 54 for( int i = 1; i <= n; ++i ) { // 把初始元素变为修改操作 55 query[qidx].idx = i; query[qidx].type = 1; 56 scanf( "%lld", &query[qidx].val ); ++qidx; 57 } 58 for( int i = 0; i < m; ++i ) { 59 int type; scanf( "%d", &type ); 60 query[qidx].type = type; 61 if( type == 1 ) scanf( "%d%lld", &query[qidx].idx, &query[qidx].val ); 62 else { // 把查询操作分为两部分 63 int l,r; scanf( "%d%d", &l, &r ); 64 query[qidx].idx = l-1; query[qidx].val = aidx; ++qidx; 65 query[qidx].type = 3; query[qidx].idx = r; query[qidx].val = aidx; ++aidx; 66 } 67 ++qidx; 68 } 69 cdq(0,qidx); 70 for( int i = 0; i < aidx; ++i ) printf( "%lld\n", ans[i] ); 71 return 0; 72 }