前言

  辣鸡蒟蒻__stdcall终于会CDQ分治啦!

      CDQ分治是我们处理各类问题的重要武器。它的优势在于可以顶替复杂的高级数据结构,而且常数比较小;缺点在于必须离线操作。

  CDQ分治的基本思想和实现都很简单,但是因为没有人给本蒟蒻详讲,所以我对着几篇论文头疼了一个下午,最终在menci和sxysxy大佬的帮助下学会了CDQ分治。本文介绍一些非常simple的CDQ分治问题,目的在于帮助新手更快地入门CDQ分治,希望对大家有帮助。

  转载请注明作者:__stdcall。

基本思想

  CDQ分治的基本思想十分简单。如下:

  1. 我们要解决一系列问题,这些问题一般包含修改和查询操作,可以把这些问题排成一个序列,用一个区间[L,R]表示。
  2. 分。递归处理左边区间[L,M]和右边区间[M+1,R]的问题。
  3. 治。合并两个子问题,同时考虑到[L,M]内的修改对[M+1,R]内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。

  这就是CDQ分治的基本思想。和普通分治不同的地方在于,普通分治在合并两个子问题的过程中,[L,M]内的问题不会对[M+1,R]内的问题产生影响。

具体实现和用途

  二维偏序问题

  给定N个有序对(a,b),求对于每个(a,b),满足a2<ab2<b的有序对(a2,b2)有多少个。

  我们从归并排序求逆序对来引入二维偏序问题。

  回忆一下归并排序求逆序对的过程,我们在合并两个子区间的时候,要考虑到左边区间的对右边区间的影响。即,我们每次从右边区间的有序序列中取出一个元素的时候,要把“以这个元素结尾的逆序对的个数”加上“左边区间有多少个元素比他大”。这是一个典型的CDQ分治的过程。

  现在我们把这个问题拓展到二维偏序问题。在归并排序求逆序对的过程中,每个元素可以用一个有序对(a,b)表示,其中a表示数组中的位置,b表示该位置对应的值。我们求的就是“对于每个有序对(a,b),有多少个有序对(a2,b2)满足a2<a且b2>b”,这就是一个二维偏序问题。

  注意到在求逆序对的问题中,a元素是默认有序的,即我们拿到元素的时候,数组中的元素是默认从第一个到最后一个按顺序排列的,所以我们才能在合并子问题的时候忽略a元素带来的影响。因为我们在合并两个子问题的过程中,左边区间的元素一定出现在右边区间的元素之前,即左边区间的元素的a都小于右边区间元素的a。

  那么对于二维偏序问题,我们在拿到所有有序对(a,b)的时候,先把a元素从小到大排序。这时候问题就变成了“求顺序对”,因为a元素已经有序,可以忽略a元素带来的影响,和“求逆序对”的问题是一样的。

  考虑二维偏序问题的另一种解法,用树状数组代替CDQ分治,即常用的用树状数组求顺序对。在按照a元素排序之后,我们对于整个序列从左到右扫描,每次扫描到一个有序对,求出“扫描过的有序对中,有多少个有序对的b值小于当前b值”,可以用 权值树状数组/权值线段树 实现。然而当b的值非常大的时候,空间和时间上就会吃不消,便可以用CDQ分治代替,就是我们所说的“顶替复杂的高级数据结构”。别急,一会儿我们会看到CDQ分治在这方面更大的用途。

  二维偏序问题的拓展

  给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作:

  操作1:格式为1 x k,把位置x的元素加上k(位置从1标号到N)。

  操作2:格式为2 x y,求出区间[x,y]内所有元素的和。

  这是一个经典的树状数组问题,可以毫无压力地秒掉,现在,我们用CDQ分治解决它——带修改和查询的问题。

  我们把他转化成一个二维偏序问题,每个操作用一个有序对(a,b)表示,其中a表示操作到来的时间,b表示操作的位置,时间是默认有序的,所以我们在合并子问题的过程中,就按照b从小到大的顺序合并。

  问题来了:如何表示修改与查询?

  具体细节请参见代码,这里对代码做一些解释,请配合代码来看。我们定义结构体Query包含3个元素:type,idx,val,其中idx表示操作的位置,type为1表示修改,val表示“加上的值”。而对于查询,我们用前缀和的思想把他分解成两个操作:sum[1,y]-sum[1,x-1],即分解成两次前缀和的查询。在合并的过程中,type为2表示遇到了一个查询的左端点x-1,需要把该查询的结果减去当前“加上的值的前缀和”,type为3表示遇到了一个查询的右端点y,需要把查询的结果加上当前“加上的值的前缀和”,val表示“是第几个查询”。这样,我们就把每个操作转换成了带有附加信息的有序对(时间,位置),然后对整个序列进行CDQ分治。

  有几点需要注意:

  1. 对于位置相同的操作,要先修改后查询。
  2. 代码中为了方便,使用左闭右开区间。
  3. 合并问题的时候统计“加上的值的前缀和”,只能统计左边区间内的修改操作,改动查询结果的时候,只能修改右边区间内的查询结果。因为只有左边区间内的修改值对右边区间内的查询结果的影响还没有统计。
  4. 代码中,给定的数组是有初始值的,可以把每个初始值变为一个修改操作。

  代码如下:

 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 }

  三维偏序问题

  给定N个有序三元组(a,b,c),求对于每个三元组(a,b,c),有多少个三元组(a2,b2,c2)满足a2<ab2<bc2<c

  不用CDQ分治的方法:先按照a元素排序,从左到右扫描。按照b元素构造权值树状数组,树状数组每个节点按照c元素构造平衡树。树套树的解法不仅常数大,而且代码量巨大,还容易写错。

  类似二维偏序问题,先按照a元素从小到大排序,忽略a元素的影响。然后CDQ分治,按照b元素从小到大的顺序进行归并操作。但是这时候没办法像 求逆序对 一样简单地统计 个数 了,c元素如何处理呢?

  这时候比较好的方案就是借助权值树状数组。每次从右边的序列中取出三元组(a,b,c)时,对树状数组查询c值小于(a,b,c)的三元组有多少个;每次从左边序列取出三元组(a,b,c)的时候,根据c值在树状数组中进行修改。注意,每次使用完树状数组记得把树状数组归零!详细代码我会放在下面一道例题中。

  三维偏序问题的拓展

  平面上有N个点,每个点的横纵坐标在[0,1e7]之间,有M个询问,每个询问为查询在指定矩形之内有多少个点,矩形用(x1,y1,x2,y2)的方式给出,其中(x1,y1)为左下角坐标,(x2,y2)为右上角坐标。

  不用CDQ分治的话可以用二维线段树或者二维树状数组来做,然而空间是明显吃不消的。用CDQ分治如何做呢?

  到这里大家应该比较清楚了吧,把每个点的位置变成一个修改操作,用三元组(时间,横坐标,纵坐标)来表示,把每个查询分解成4个前缀和查询,同样用三元组来表示。对于修改操作,每个三元组没有附加信息;对于查询操作,每个三元组的附加信息为“第几个查询”和“对结果的影响是+还是-,用+1表示+,用-1表示-”。操作到来的时间是默认有序的,分治过程中按照横坐标从小到大排序,用树状数组维护纵坐标的信息。代码如下:

  1 #include <iostream>
  2 #include <cstring>
  3 #include <algorithm>
  4 #include <cstdio>
  5 #include <cmath>
  6 #include <cstdlib>
  7 #include <cctype>
  8
  9 using namespace std;
 10 const int MAXN = 500001; // 点的数量
 11 const int MAXM = 500001; // 询问数量
 12 const int MAXQ = MAXN+(MAXM<<2);
 13 const int MAXL = 10000002; // 树状数组大小
 14
 15 int n, m, maxy = -1;
 16
 17 namespace IO { // 快读相关
 18     const int BUFSZ = 1e7;
 19     char buf[BUFSZ]; int idx, end;
 20     void init() { idx = BUFSZ; }
 21     char getch() {
 22         if( idx == BUFSZ ) {
 23             end = fread( buf, 1, BUFSZ, stdin ); idx = 0;
 24         }
 25         if( idx == end ) return EOF;
 26         return buf[idx++];
 27     }
 28     int getint() {
 29         int num = 0; char ch;
 30         while( isspace(ch=getch()) );
 31         do { num = num*10 + ch-'0'; } while( isdigit(ch=getch()) );
 32         return num;
 33     }
 34 }
 35 using IO::getint;
 36
 37 struct Query {
 38     int type, x, y, w, aid; // w表示对查询结果贡献(+还是-),aid是“第几个查询”
 39     bool operator<( const Query &rhs ) const {
 40         return x == rhs.x ? type < rhs.type : x < rhs.x;
 41     }
 42 }query[MAXQ];
 43 int qidx = 0;
 44 void addq( int type, int x, int y, int w, int aid ) {
 45     query[qidx++] = (Query){type,x,y,w,aid};
 46 }
 47
 48 int ans[MAXM], aidx = 0;
 49
 50 namespace BIT { // 树状数组相关
 51     int arr[MAXL];
 52     inline int lowbit( int num ) { return num&(-num); }
 53     void add( int idx, int val ) {
 54         while( idx <= maxy ) {
 55             arr[idx] += val;
 56             idx += lowbit(idx);
 57         }
 58     }
 59     int query( int idx ) {
 60         int ans = 0;
 61         while( idx ) {
 62             ans += arr[idx];
 63             idx -= lowbit(idx);
 64         }
 65         return ans;
 66     }
 67     void clear( int idx ){
 68         while( idx <= maxy ) {
 69             if( arr[idx] ) arr[idx] = 0; else break;
 70             idx += lowbit(idx);
 71         }
 72     }
 73 }
 74
 75 Query tmp[MAXQ];
 76 void cdq( int L, int R ) {
 77     if( R-L <= 1 ) return;
 78     int M = (L+R)>>1; cdq(L,M); cdq(M,R);
 79     int p = L, q = M, o = L;
 80     while( p < M && q < R ) {
 81         if( query[p] < query[q] ) {
 82             if( query[p].type == 0 ) BIT::add( query[p].y, 1 );
 83             tmp[o++] = query[p++];
 84         } else {
 85             if( query[q].type == 1 ) ans[query[q].aid] += query[q].w * BIT::query( query[q].y );
 86             tmp[o++] = query[q++];
 87         }
 88     }
 89     while( p < M ) tmp[o++] = query[p++];
 90     while( q < R ) {
 91         if( query[q].type == 1 ) ans[query[q].aid] += query[q].w * BIT::query( query[q].y );
 92         tmp[o++] = query[q++];
 93     }
 94     for( int i = L; i < R; ++i ) {
 95         BIT::clear( tmp[i].y ); // 清空树状数组
 96         query[i] = tmp[i];
 97     }
 98 }
 99
100 int main() {
101     IO::init(); n = getint(); m = getint();
102     while( n-- ) {
103         int x,y; x = getint(); y = getint(); ++x; ++y; // 为了方便,把坐标转化为[1,1e7+1]
104         addq(0,x,y,0,0); maxy = max( maxy, y ); // 修改操作无附加信息
105     }
106     while( m-- ) {
107         int x1,y1,x2,y2; x1 = getint(); y1 = getint(); x2 = getint(); y2 = getint(); ++x1; ++y1; ++x2; ++y2;
108         addq(1,x1-1,y1-1,1,aidx); addq(1,x1-1,y2,-1,aidx); addq(1,x2,y1-1,-1,aidx); addq(1,x2,y2,1,aidx); ++aidx;
109         maxy = max( maxy, max(y1,y2) );
110     }
111     cdq(0,qidx);
112     for( int i = 0; i < aidx; ++i ) printf( "%d\n", ans[i] );
113     return 0;
114 }

总结

  对于经典的多维偏序问题和多维数据结构的查询和修改,我们可以用一步步“降维”的方式解决。排序,数据结构,CDQ分治都是我们降维的工具。

  CDQ分治还有其他很多强大的功能,比如多重嵌套CDQ分治,用CDQ分治加速动态规划等等。总的来说就是可以顶一层数据结构,降维用。由于本文是面向我这样的新手的教程,而且我也没有学这些用法(我好弱啊QAQ),所以对于这些更难一点的问题不作介绍。

习题(参考menci博客)

  园丁的烦恼 SHOI2007 BZOJ 1935

  【模板】树状数组 1 luogu P3374

  Mokia BZOJ 1176

  陌上花开 BZOJ 3262

  简单题BZOJ 2683

  动态逆序对 CQOI2011 BZOJ 3295

转载于:https://www.cnblogs.com/mlystdcall/p/6219421.html

【教程】简易CDQ分治教程学习笔记相关推荐

  1. [偏序关系与CDQ分治]【学习笔记】

    组合数学真是太棒了 $CDQ$真是太棒了(雾 参考资料: 1.<组合数学> 2.论文 课件 很容易查到 3.sro __stdcall 偏序关系 关系: 集合$X$上的关系是$X$与$X$ ...

  2. 《嵌入式系统设计师教程 (第2版)》学习笔记

    <嵌入式系统设计师教程 (第2版).pdf>链接:https://download.csdn.net/download/AnChenliang_1002/87864728 该资源在上传时涉 ...

  3. Git最新教程通俗易懂----狂神说Java -- ---学习笔记

    [狂神说Java]Git最新教程通俗易懂 https://www.bilibili.com/video/BV1FE411P7B3 P1.前言之版本控制 06:12 P2.Git和SVN的区别 07:3 ...

  4. [转]Verilog数字系统设计教程(大连理工一博士学习笔记)

    写在前面 学习Verilog HDL有一些时间,大概一年前的的这个时候开始的吧,从一点都不懂开始学,主要还是看夏宇闻老师的这本书入的门--<Verilog数字系统设计教程>,书写的特别好. ...

  5. 阿里云ECS服务器安装AMH5.3面板并搭建WordPress站点详细教程(卞懂的学习笔记)...

    很多站长在购买阿里云ECS服务器后不知道该怎么搭建网站环境,又或是搭建了AMH5.3面板后不知道该怎么配置站点,下面这个教程将详细介绍阿里云ECS服务器安装AMH5.3面板并搭建WordPress站点 ...

  6. 二十一世纪大学英语读写教程(第二册)学习笔记(原文)——7 - Thinking: A Neglected Art(思考——被忽视的艺术)

    Unit 7 - Thinking: A Neglected Art(思考--被忽视的艺术) Thinking: A Neglected Art Carolyn Kane It is generall ...

  7. 二十一世纪大学英语读写教程(第二册)学习笔记(原文)——8 - Unwritten Rules(不成文的规矩)

    Unit 8 - Unwritten Rules(不成文的规矩) Unwritten Rules Bob Greene The restaurant was almost full. A steady ...

  8. 二十一世纪大学英语读写教程(第二册)学习笔记(原文)——4 - Turning failure into Success(反败为胜)

    Unit 4 - Turning failure into Success(反败为胜) Turning failure into Success Fredelle Maynard Vicky - be ...

  9. 二十一世纪大学英语读写教程(第二册)学习笔记(原文)——6 - A Brief History of Stephen Hawking(斯蒂夫·霍金简传)

    Unit 6 - A Brief History of Stephen Hawking(斯蒂夫·霍金简传) A Brief History of Stephen Hawking Michael Whi ...

最新文章

  1. 如何统统扩充loop设备的size(linux loop resize2fs)
  2. Sublime text3 Emmet使用
  3. VTK:相互作用之WorldPointPicker
  4. 机器学习(二十一)——Optimizer, 单分类SVM多分类SVM, 时间序列分析
  5. 《网络安全原理与实践》一1.2 资产确定
  6. 默认帐户生成器帐户来源
  7. 2176. 统计数组中相等且可以被整除的数对
  8. eclipse下的mybatis插件:MyBatipse
  9. 如何查看CentOS版本方法
  10. 筛选法建立初始堆_MTT法检测细胞增殖
  11. 计算机系酒店管理毕业论文,计算机专业酒店管理系统毕业论文讲述.doc
  12. 360桌面隐藏应用 android代码,360手机桌面上的隐藏应用怎么设置?
  13. linux蓝牙遥控,蓝牙遥控器连接流程分析
  14. L2-016 愿天下有情人都是失散多年的兄妹【DFS】
  15. Google的GDrive美如水中之月
  16. 【rpm】源码包制作rpm包|修改rpm、重新制作rpm包
  17. 华为Mate20系列赢得各界盛誉,棋圣聂卫平也对其AI性能称赞
  18. java解压obb_obb文件怎么解压 打开obb文件的注意事项 - 驱动管家
  19. 怎样成为一名优秀的平面设计师
  20. 关于传奇皓月GOM引擎登录器配置与生成完整教程

热门文章

  1. 用了N年的接口,你知道接口是什么吗?——一个简单实例说明接口的伟大意义...
  2. wpf Datagrid绑定 Dictionary
  3. 农产品流通信息化及农超对接体系的现状
  4. 关于equals与hashcode的重写
  5. request 和response
  6. php源码之计算两个文件的相对路径
  7. 暑期大作战 第五天(第四天待补)
  8. Redis与Memcached简要比较
  9. Android 7.0 WifiMonitor工作流程分析
  10. AI让交通管理省时、省心、省力