前言

  辣鸡蒟蒻__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. 代码中,给定的数组是有初始值的,可以把每个初始值变为一个修改操作。

  代码如下:

 #include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath> using namespace std;
typedef long long ll;
const int MAXN = ; // 原数组大小
const int MAXM = ; // 操作数量
const int MAXQ = (MAXM<<)+MAXN; int n,m; struct Query {
int type, idx; ll val;
bool operator<( const Query &rhs ) const { // 按照位置从小到大排序,修改优先于查询
return idx == rhs.idx ? type < rhs.type : idx < rhs.idx;
}
}query[MAXQ];
int qidx = ; ll ans[MAXQ]; int aidx = ; // 答案数组 Query tmp[MAXQ]; // 归并用临时数组
void cdq( int L, int R ) {
if( R-L <= ) return;
int M = (L+R)>>; cdq(L,M); cdq(M,R);
ll sum = ;
int p = L, q = M, o = ;
while( p < M && q < R ) {
if( query[p] < query[q] ) { // 只统计左边区间内的修改值
if( query[p].type == ) sum += query[p].val;
tmp[o++] = query[p++];
}
else { // 只修改右边区间内的查询结果
if( query[q].type == ) ans[query[q].val] -= sum;
else if( query[q].type == ) ans[query[q].val] += sum;
tmp[o++] = query[q++];
}
}
while( p < M ) tmp[o++] = query[p++];
while( q < R ) {
if( query[q].type == ) ans[query[q].val] -= sum;
else if( query[q].type == ) ans[query[q].val] += sum;
tmp[o++] = query[q++];
}
for( int i = ; i < o; ++i ) query[i+L] = tmp[i];
} int main() {
scanf( "%d%d", &n, &m );
for( int i = ; i <= n; ++i ) { // 把初始元素变为修改操作
query[qidx].idx = i; query[qidx].type = ;
scanf( "%lld", &query[qidx].val ); ++qidx;
}
for( int i = ; i < m; ++i ) {
int type; scanf( "%d", &type );
query[qidx].type = type;
if( type == ) scanf( "%d%lld", &query[qidx].idx, &query[qidx].val );
else { // 把查询操作分为两部分
int l,r; scanf( "%d%d", &l, &r );
query[qidx].idx = l-; query[qidx].val = aidx; ++qidx;
query[qidx].type = ; query[qidx].idx = r; query[qidx].val = aidx; ++aidx;
}
++qidx;
}
cdq(,qidx);
for( int i = ; i < aidx; ++i ) printf( "%lld\n", ans[i] );
return ;
}

  三维偏序问题

  给定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表示-”。操作到来的时间是默认有序的,分治过程中按照横坐标从小到大排序,用树状数组维护纵坐标的信息。代码如下:

 #include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cctype> using namespace std;
const int MAXN = ; // 点的数量
const int MAXM = ; // 询问数量
const int MAXQ = MAXN+(MAXM<<);
const int MAXL = ; // 树状数组大小 int n, m, maxy = -; namespace IO { // 快读相关
const int BUFSZ = 1e7;
char buf[BUFSZ]; int idx, end;
void init() { idx = BUFSZ; }
char getch() {
if( idx == BUFSZ ) {
end = fread( buf, , BUFSZ, stdin ); idx = ;
}
if( idx == end ) return EOF;
return buf[idx++];
}
int getint() {
int num = ; char ch;
while( isspace(ch=getch()) );
do { num = num* + ch-''; } while( isdigit(ch=getch()) );
return num;
}
}
using IO::getint; struct Query {
int type, x, y, w, aid; // w表示对查询结果贡献(+还是-),aid是“第几个查询”
bool operator<( const Query &rhs ) const {
return x == rhs.x ? type < rhs.type : x < rhs.x;
}
}query[MAXQ];
int qidx = ;
void addq( int type, int x, int y, int w, int aid ) {
query[qidx++] = (Query){type,x,y,w,aid};
} int ans[MAXM], aidx = ; namespace BIT { // 树状数组相关
int arr[MAXL];
inline int lowbit( int num ) { return num&(-num); }
void add( int idx, int val ) {
while( idx <= maxy ) {
arr[idx] += val;
idx += lowbit(idx);
}
}
int query( int idx ) {
int ans = ;
while( idx ) {
ans += arr[idx];
idx -= lowbit(idx);
}
return ans;
}
void clear( int idx ){
while( idx <= maxy ) {
if( arr[idx] ) arr[idx] = ; else break;
idx += lowbit(idx);
}
}
} Query tmp[MAXQ];
void cdq( int L, int R ) {
if( R-L <= ) return;
int M = (L+R)>>; cdq(L,M); cdq(M,R);
int p = L, q = M, o = L;
while( p < M && q < R ) {
if( query[p] < query[q] ) {
if( query[p].type == ) BIT::add( query[p].y, );
tmp[o++] = query[p++];
} else {
if( query[q].type == ) ans[query[q].aid] += query[q].w * BIT::query( query[q].y );
tmp[o++] = query[q++];
}
}
while( p < M ) tmp[o++] = query[p++];
while( q < R ) {
if( query[q].type == ) ans[query[q].aid] += query[q].w * BIT::query( query[q].y );
tmp[o++] = query[q++];
}
for( int i = L; i < R; ++i ) {
BIT::clear( tmp[i].y ); // 清空树状数组
query[i] = tmp[i];
}
} int main() {
IO::init(); n = getint(); m = getint();
while( n-- ) {
int x,y; x = getint(); y = getint(); ++x; ++y; // 为了方便,把坐标转化为[1,1e7+1]
addq(,x,y,,); maxy = max( maxy, y ); // 修改操作无附加信息
}
while( m-- ) {
int x1,y1,x2,y2; x1 = getint(); y1 = getint(); x2 = getint(); y2 = getint(); ++x1; ++y1; ++x2; ++y2;
addq(,x1-,y1-,,aidx); addq(,x1-,y2,-,aidx); addq(,x2,y1-,-,aidx); addq(,x2,y2,,aidx); ++aidx;
maxy = max( maxy, max(y1,y2) );
}
cdq(,qidx);
for( int i = ; i < aidx; ++i ) printf( "%d\n", ans[i] );
return ;
}

总结

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

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

习题(参考menci博客)

  园丁的烦恼 SHOI2007 BZOJ 1935

  【模板】树状数组 1 luogu P3374

  Mokia BZOJ 1176

  陌上花开 BZOJ 3262

  简单题BZOJ 2683

  动态逆序对 CQOI2011 BZOJ 3295

【教程】简易CDQ分治教程&学习笔记的更多相关文章

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

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

  2. CDQ分治(学习笔记)

    离线算法——CDQ分治 CDQ (SHY)显然是一个人的名字,陈丹琪(MM)(NOI2008金牌女选手). 从归并开始(这里并没有从逆序对开始,是想直接引入分治思想,而不是引入处理对象) 一个很简单的 ...

  3. Linux简易APR内存池学习笔记(带源码和实例)

    先给个内存池的实现代码,里面带有个应用小例子和画的流程图,方便了解运行原理,代码 GCC 编译可用.可以自己上网下APR源码,参考代码下载链接: http://pan.baidu.com/s/1hq6 ...

  4. cdq分治入门学习 cogs 1752 Mokia nwerc 2015-2016 G 二维偏序

    /* CDQ分治的对象是时间. 即对于一个时间段[L, R],我们取mid = (L + R) / 2. 分治的每层只考虑mid之前的修改对mid之后的查询的贡献,然后递归到[L,mid],(mid, ...

  5. 分治 FFT学习笔记

    先给一道luogu板子题:P4721 [模板]分治 FFT 今天模拟有道题的部分分做法是分治fft,于是就学了一下.感觉不是很难,国赛上如果推出式子的话应该能写出来. 分治fft用来解决这么一个式子\ ...

  6. 分治FFT学习笔记

    用途 在\(O(n\log^2 n)\)的时间内做诸如 \[ f_n=\sum_{i=0}^{n-1} f_ig_{n-i} \] 或是 \[ f_n=\sum_{i=0}^{n-1} f_if_{n ...

  7. 多项式求逆/分治FFT 学习笔记

    一.多项式求逆 给定一个多项式 \(F(x)\),请求出一个多项式 \(G(x)\), 满足 \(F(x) * G(x) \equiv 1 ( \mathrm{mod\:} x^n )\).系数对 \ ...

  8. 【Android官方Training教程】Getting Started部分学习笔记

    Getting Started Welcome to Training for Android developers. Here you'll find sets of lessons within ...

  9. 《jQuery基础教程(第四版)》学习笔记

    本书代码参考:Learning jQuery Code Listing Browser 原书: jQuery基础教程 目录: 第2章 选择元素 1. 使用$()函数 2. 选择符 3. DOM遍历方法 ...

随机推荐

  1. Guass列选主元消去法和三角分解法

    最近数值计算学了Guass列主消元法和三角分解法解线性方程组,具体原理如下: 1.Guass列选主元消去法对于AX =B 1).消元过程:将(A|B)进行变换为,其中是上三角矩阵.即: k从1到n-1 ...

  2. Win10 UWP开发系列:使用VS2015 Update2+ionic开发第一个Cordova App

    安装VS2015 Update2的过程是非常曲折的.还好经过不懈的努力,终于折腾成功了. 如果开发Cordova项目的话,推荐大家用一下ionic这个框架,效果还不错.对于Cordova.PhoneG ...

  3. C++ 最小化到托盘

    #define WM_SHOWTASK (WM_USER + 1) void CTestDlg::OnSysCommand(UINT nID, LPARAM lParam) { if ((nID &a ...

  4. css设置table表格tr分离

    table { border-collapse:separate; border-spacing:10px 50px; }

  5. 仿QQ空间根据位置弹出PopupWindow显示更多操作效果

    我们打开QQ空间的时候有个箭头按钮点击之后弹出PopupWindow会根据位置的变化显示在箭头的上方还是下方,比普通的PopupWindow弹在屏幕中间显示好看的多. 先看QQ空间效果图:       ...

  6. iOS开发常用快捷键

    二. Xcode基本快捷键 新建项目 Shift + CMD + N 项目中新建文件 CMD + N 运行 CMD + R 编译 CMD + B 停止运行 CMD + . 清除缓存 Shift + C ...

  7. id,clientid 和 uniqueid 的区别

    一. ID是设计的时候所指定的ID. ClientID是当这个控件生成到客户端页面时候,需要在客户端访问时候用的. UniqueID是当需要参与服务端回传的时候用的. 备注:当控件是子控件的时候(例如 ...

  8. C#求斐波那契数列第30项的值(递归和非递归)

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  9. EF里查看/修改实体的当前值、原始值和数据库值以及重写SaveChanges方法记录实体状态

    本文目录 查看实体当前.原始和数据库值:DbEntityEntry 查看实体的某个属性值:GetValue<TValue>方法 拷贝DbPropertyValues到实体:ToObject ...

  10. [Android] 怎么在应用中实现密码隐藏?

    [Android] 怎么在应用中实现密码隐藏? 在安卓应用中,用户注册或者登录时,需要把密码隐藏,实现一定的保密效果.在安卓中,可以通过设置EditText组件的TransformationMetho ...