【转】cdq分治

https://wenku.baidu.com/view/860cfcd976a20029bd642d9c?mark_pay_doc=0&mark_rec_position=10&clear_uda_param=1#13&isbtn=2

https://www.cnblogs.com/mlystdcall/p/6219421.html

前言

  辣鸡蒟蒻__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/hua-dong/p/8184457.html

时间: 2024-10-29 06:20:45

【转】cdq分治的相关文章

CDQ分治与整体二分小结

前言 这是一波强行总结. 下面是一波瞎比比. 这几天做了几道CDQ/整体二分,感觉自己做题速度好慢啊. 很多很显然的东西都看不出来 分治分不出来 打不出来 调不对 上午下午晚上的效率完全不一样啊. 完蛋.jpg 绝望.jpg. 关于CDQ分治 CDQ分治,求的是三维偏序问题都知道的. 求法呢,就是在分治外面先把一维变成有序 然后分治下去,左边(l,mid)关于右边(mid+1,r)就不存在某一维的逆序了,所以只有两维偏序了. 这个时候来一波"树状数组求逆序对"的操作搞一下二维偏序 就可

【BZOJ3963】[WF2011]MachineWorks cdq分治+斜率优化

[BZOJ3963][WF2011]MachineWorks Description 你是任意性复杂机器公司(Arbitrarily Complex Machines, ACM)的经理,公司使用更加先进的机械设备生产先进的机器.原来的那一台生产机器已经坏了,所以你要去为公司买一台新的生产机器.你的任务是在转型期内尽可能得到更大的收益.在这段时间内,你要买卖机器,并且当机器被ACM公司拥有的时候,操控这些机器以获取利润.因为空间的限制,ACM公司在任何时候都只能最多拥有一台机器. 在转型期内,有若

BZOJ 2225 [Spoj 2371]Another Longest Increasing(CDQ分治)

[题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=2225 [题目大意] 给定N个数对(xi,yi),求最长上升子序列的长度. 上升序列定义为{(xi,yi)}满足对i<j有xi<xj且yi<yj. [题解] CDQ分治,将每个区间按照a排序,用区间左边的数据来更新右边的最长上升序列, 为排除a相等但是b上升情况的误统计,在排序时加入下标作为第二关键字, 使得a相等的情况下标小的后更新. [代码] #include <cs

BZOJ 3262: 陌上花开 [CDQ分治 三维偏序]

Description 有n朵花,每朵花有三个属性:花形(s).颜色(c).气味(m),又三个整数表示.现要对每朵花评级,一朵花的级别是它拥有的美丽能超过的花的数量.定义一朵花A比另一朵花B要美丽,当且仅当Sa>=Sb,Ca>=Cb,Ma>=Mb.显然,两朵花可能有同样的属性.需要统计出评出每个等级的花的数量. Input 第一行为N,K (1 <= N <= 100,000, 1 <= K <= 200,000 ), 分别表示花的数量和最大属性值. 以下N行,每

HDU 5618:Jam&#39;s problem again(CDQ分治+树状数组处理三维偏序)

http://acm.hdu.edu.cn/showproblem.php?pid=5618 题意:-- 思路:和NEUOJ那题一样的.重新写了遍理解了一下,算作处理三维偏序的模板了. 1 #include <cstdio> 2 #include <algorithm> 3 #include <iostream> 4 #include <cstring> 5 using namespace std; 6 #define INF 0x3f3f3f3f 7 #d

ACdream1157 Segments(CDQ分治 + 线段树)

题目这么说的: 进行如下3种类型操作:1)D L R(1 <= L <= R <= 1000000000) 增加一条线段[L,R]2)C i (1-base) 删除第i条增加的线段,保证每条插入线段最多插入一次,且这次删除操作一定合法3) Q L R(1 <= L <= R <= 1000000000) 查询目前存在的线段中有多少条线段完全包含[L,R]这个线段,线段X被线段Y完全包含即LY <= LX <= RX <= RY) 初学CDQ分治是看了B

BZOJ 2726: [SDOI2012]任务安排( dp + cdq分治 )

考虑每批任务对后面任务都有贡献, dp(i) = min( dp(j) + F(i) * (T(i) - T(j) + S) ) (i < j <= N)  F, T均为后缀和. 与j有关的量只有t = dp(j) - F(i) * T(j) , 我们要最小化它. dp(j)->y, T(j)->x, 那么y = F(i) * x + t, 就是给一些点和一个斜率...然后最小化截距, 显然维护下凸包就可以了. 然后因为无比坑爹的出题人....时间可以为负数, 所以要用平衡树维护(

SPOJ LIS2 Another Longest Increasing Subsequence Problem 三维偏序最长链 CDQ分治

Another Longest Increasing Subsequence Problem Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.hust.edu.cn/vjudge/problem/visitOriginUrl.action?id=19929 Description Given a sequence of N pairs of integers, find the length of the longest incre

CDQ分治与整体二分总结

Cdq分治和整体二分是两个很奇妙的东西.他们都是通过离线的思想来进行优化,从而更快的求出解. 整体二分通俗的讲就是二分答案,但是它了不起的地方是一下子把所有的答案都二分出来了,从而可以一下子得出所有查询. CDQ分治通俗的讲就是二分查询.通常的做法是把所有的查询分成两半,然后通过递归先计算出左边一半的所有的查询,然后通过这些已知的左半边的值来更新右半边的值.这里,最最重要的思想是通过左半边来更新右半边.更具体一点,就是用左半边的修改来更新右半边的查询. 重要的事情说话三遍: CDQ分治就是通过左

Acdream1157---Segments (CDQ分治)

陈丹琦分治~~~其实一些数据小的时候可以用二维或者多维树状数组做的,而数据大的时候就无力的题目,都可以用陈丹琦分治解决. 题目:由3钟类型操作:1)D L R(1 <= L <= R <= 1000000000) 增加一条线段[L,R]2)C i (1-base) 删除第i条增加的线段,保证每条插入线段最多插入一次,且这次删除操作一定合法3) Q L R(1 <= L <= R <= 1000000000) 查询目前存在的线段中有多少条线段完全包含[L,R]这个线段,线