CDQ分治
CDQ
思想概述-基于离线的分治算法
首先明确几个概念:
动态问题:即带有修改操作的问题
静态问题:即无修改操作的问题
当一个问题是动态问题并且并不强制在线的,我们可以进行离线操作——对问题的某一维度进行分治
一般来说,CDQ分治分为三步
- 统计分治的前段区间\([l,mid]\)的修改操作对\([mid+1,r]\)中查询操作的影响
- 递归解决区间\([l,mid]\)和\([mid+1,r]\)
当然,我们并不要求统计一定要在递归解决子区间前面,什么时候方便统计影响就什么时候统计
很显然的是,若我们按照时间轴进行分治计算一个动态问题,这个算法的正确性就很显然了,具体详见《算法竞赛进阶指南》
而,统计影响便是一个静态问题,因为我们保证了所有的修改都在查询之前。所以说,CDQ算法本质上是:
将动态问题离线化,对时间轴进行分治,达到将动态问题转变为若干个静态问题的目的
很明显,静态问题显然更好解决
一般来说,若能够保证解决区间\([l,r]\)的静态问题的复杂度仅仅与\([l,r]\)有关,CDQ分治的复杂度便只比解决\([1,n]\)的静态问题的复杂度多了\(O(log M)\)
下面通过三道题来感受CDQ分治的魅力
经典例题
- 动态二维前缀和问题:莫基亚
题目描述
第一行两个整数 S,W,其中 S 为矩阵初始值,W 为矩阵大小。
接下来每行为以下三种输入之一:
“1 x y a”——把第 x 行第 y 列的格子 (x,y) 权值增加 a;
“2 x1 y1 x2 y2”——询问以 (x1,y1) 为左下角,(x2,y2) 为右上角的矩阵内所有格子的权值和;
“3”——输入结束。
S始终为0
好的下面我们将会使用CDQ分治算法解决这道题
使用CDQ分治转变为静态问题的话,我们的问题就变成了:
给定\(n\)个点(设为\(S\))需要将值增加,然后查询若干个矩形的和,很自然的能够想到二维前缀和的思路,将一个矩形的和拆为四个类似\((1,1,x,y)\)的矩形的差分,那么,需要考虑的是:若一个点\(S_i\)会对矩形\((1,1,x,y)\)产生影响,当且仅当\(S_{i,x}\le x,S_{i,y}\le y\),故我们的问题又转化为了:
给定两个数组\(S,S2\),每个数组有两个参数\(x,y\),问对于每一个\(S_2\)的元素\(j\),对\(S\)中满足\(S_{i,x}\le S2_{j,x},S_{i,y}\le S2_{j,y}\)的\(i\)的权值和
这个问题实质上是二维偏序的一个变式,按照套路,我们以一维树状数组和排序解决,具体的:
- 设将\(S,S2\)放于一个数组\(a\)中,按照\(x\)进行递增排序
- 从小到大扫描每一个\(i\),进行分类讨论
- 若\(a_i\)原属于\(S\)(即\(a_i\)的操作编号小于等于分治值\(mid\)),则将树状数组中\(a_{i,y}\)的位置加上\(a_i\)的权值
- 若\(a_i\)原属于\(S2\),则对于树状数组求\(ask(a_{i,y})\)即为\((1,1,a_{i,x},a_{i,y})\)的权值和
这个做法的正确性证明:
这个做法,等同于对于树状数组的每一个值,都变成了维护一个向量的和(也即每一个y坐标维护的是平面直角坐标系中那一条直线上的权值和),因为有着x的递增排序在,于是我们每一次统计矩阵和的时候,每一个向量的最大\(x\)坐标都是不超过\(a_{i,x}\)的最大值(有意义的(比如0就没有意义)),这样就可以保证不会多统计,也不会漏掉,简单来说就是排序维护了一维,树状数组维护了一维,并且维护了权值和
这样的做法复杂度已经非常可观了,但我们还可以继续优化!很明显的,对于统计这个信息,我也可以在递归解决子区间之前统计,而对于时间轴的分治,我们其实并不如何关心它一定得有严格单调顺序,只需要保证左边区间的所有操作的时间轴小于右边即可,那么我们也就可以将对x的排序放在CDQ分治之前,然后在分治过程中,将时间轴按照一种类似于逆归并排序的思想,将每一步的时间分割成\(\le mid\)和\(>mid\)两部分
综上所述,代码如下:有一些设计上的技巧
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,o,tot=0,cnt=0,ans[2000005],c[2000005];
struct node {
int x,y,z,op,id;
bool operator<(const node b)const {
return x<b.x||(x==b.x&&(y<b.y||(y==b.y&op<b.op)));
}
}a[2000005],b[2000005];
void add(int x,int y){
while(x<=n){
c[x]+=y;
x+=x&-x;
}
}
int ask(int x){
int ans=0;
while(x){
ans+=c[x];
x-=x&-x;
}
return ans;
}
void CDQ(int l,int r){
if(l==r)return;
int mid=(l+r)>>1,lt=l,rt=mid+1;
for(int i=l;i<=r;i++){
if(a[i].id<=mid&&!a[i].op)add(a[i].y,a[i].z);
else if(a[i].id>mid&&a[i].op)ans[a[i].op]+=ask(a[i].y)*a[i].z;
}
for(int i=l;i<=r;i++){
if(a[i].id<=mid&&!a[i].op)add(a[i].y,-a[i].z);
}
for(int i=l;i<=r;i++){
if(a[i].id<=mid)b[lt++]=a[i];
else b[rt++]=a[i];
}
for(int i=l;i<=r;i++)a[i]=b[i];
CDQ(l,mid);
CDQ(mid+1,r);//这里必须自顶向下,因为如果自底向上就破坏了x的有序性
}
int main(){
scanf("%d%d",&n,&n);
while(scanf("%d",&o)&&o!=3)
if(o==1){
a[++tot].op=0;
a[tot].id=tot;
scanf("%d %d %d",&a[tot].x,&a[tot].y,&a[tot].z);
}
else {
int x1,y1,x2,y2;
scanf("%d %d %d %d",&x1,&y1,&x2,&y2);
a[++tot]={--x1,--y1,1,++cnt,tot};
a[++tot]={x1,y2,-1,cnt,tot};
a[++tot]={x2,y1,-1,cnt,tot};
a[++tot]={x2,y2,1,cnt,tot};
}
sort(a+1,a+tot+1);
CDQ(1,tot);
for(int i=1;i<=cnt;i++)printf("%d\n",ans[i]);
return 0;
}
- 陌上花开:三维偏序问题
有\(n\)个元素,第\(i\)个元素有\(a_i,b_i,c_i\)三个属性设\(f(i)\)表示满足\(a_j \leq a_i\)且\(b_j \leq b_i\)且\(c_j\leq c_i\)且\(j\ne i\)的\(j\)的数量。
对于\(d\in[0,n)\)求\(f(i)=d\)的数量。
考虑如何做此题:
首先,此题非常坑人,需要去重,并且去重之后因为\(\leq\)还得加上重复的元素个数-1
其次,我们按照降维的方式来考虑
先按照\(x\)为第一关键字,\(y,z\)为次要关键字排序,这样就消去了一个维度
下面的两个维度使用CDQ分治的思想
在每一次CDQ分治cdq(1,mid);cdq(mid+1,r);
后,可以保证的是[l,mid]
中所有的\(x\)都会小于[mid+1,r]
中的\(x\),满足第一个条件
下面我们思考第二个条件\(y\)如何维护:
将两个区间各自按照\(y\)坐标从小到大排序,借助一个一维的权值树状数组,然后类似于归并排序式合并即可
代码如下:
#define maxn 100010
#define maxk 200010
#define ll long long
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(isdigit(ch)==0 && ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
return x*f;
}
inline void write(int x){
int f=0;char ch[20];
if(!x){puts("0");return;}
if(x<0){putchar('-');x=-x;}
while(x)ch[++f]=x%10+'0',x/=10;
while(f)putchar(ch[f--]);
putchar('\n');
}
typedef struct node{
int x,y,z,ans,w;
}stnd;
stnd a[maxn],b[maxn];
int n,cnt[maxk];
int k,n_;
bool cmpx(stnd u,stnd v){
if(u.x==v.x){
if(u.y==v.y)
return u.z<v.z;
return u.y<v.y;
}
return u.x<v.x;
}
bool cmpy(stnd u,stnd v){
if(u.y==v.y)
return u.z<v.z;
return u.y<v.y;
}
struct treearray{
int tre[maxk],kk;
int lwbt(int x){return x&(-x);}
int ask(int i){int ans=0; for(;i;i-=lwbt(i))ans+=tre[i];return ans;}
void add(int i,int k){for(;i<=kk;i+=lwbt(i))tre[i]+=k;}
}t;
void cdq(int l,int r){
if(l==r)return;
int mid=(l+r)>>1;
cdq(l,mid);cdq(mid+1,r);
sort(a+l,a+mid+1,cmpy);
sort(a+mid+1,a+r+1,cmpy);
int i=mid+1,j=l;
for(;i<=r;i++){
while(a[j].y<=a[i].y && j<=mid)
t.add(a[j].z,a[j++].w);
a[i].ans+=t.ask(a[i].z);
}
for(i=l;i<j;i++)
t.add(a[i].z,-a[i].w);
}
int main(){
n_=read(),k=read();t.kk=k;
for(int i=1;i<=n_;i++)
b[i].x=read(),b[i].y=read(),b[i].z=read();
sort(b+1,b+n_+1,cmpx);
int c=0;
for(int i=1;i<=n_;i++){
c++;
if(b[i].x!=b[i+1].x || b[i].y!=b[i+1].y || b[i].z!=b[i+1].z )
a[++n]=b[i],a[n].w=c,c=0;
}
cdq(1,n);
for(int i=1;i<=n;i++)
cnt[a[i].ans+a[i].w-1]+=a[i].w;
for(int i=0;i<n_;i++)
write(cnt[i]);
return 0;
}