CDQ分治

CDQ

思想概述-基于离线的分治算法

首先明确几个概念:
动态问题:即带有修改操作的问题
静态问题:即无修改操作的问题
当一个问题是动态问题并且并不强制在线的,我们可以进行离线操作——对问题的某一维度进行分治
一般来说,CDQ分治分为三步

  1. 统计分治的前段区间\([l,mid]\)的修改操作对\([mid+1,r]\)中查询操作的影响
  2. 递归解决区间\([l,mid]\)\([mid+1,r]\)
    当然,我们并不要求统计一定要在递归解决子区间前面,什么时候方便统计影响就什么时候统计
    很显然的是,若我们按照时间轴进行分治计算一个动态问题,这个算法的正确性就很显然了,具体详见《算法竞赛进阶指南》
    而,统计影响便是一个静态问题,因为我们保证了所有的修改都在查询之前。所以说,CDQ算法本质上是:

将动态问题离线化,对时间轴进行分治,达到将动态问题转变为若干个静态问题的目的
很明显,静态问题显然更好解决

一般来说,若能够保证解决区间\([l,r]\)的静态问题的复杂度仅仅与\([l,r]\)有关,CDQ分治的复杂度便只比解决\([1,n]\)的静态问题的复杂度多了\(O(log M)\)
下面通过三道题来感受CDQ分治的魅力

经典例题

  1. 动态二维前缀和问题:莫基亚
    题目描述
    第一行两个整数 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\)的权值和

这个问题实质上是二维偏序的一个变式,按照套路,我们以一维树状数组和排序解决,具体的:

  1. 设将\(S,S2\)放于一个数组\(a\)中,按照\(x\)进行递增排序
  2. 从小到大扫描每一个\(i\),进行分类讨论
  3. \(a_i\)原属于\(S\)(即\(a_i\)的操作编号小于等于分治值\(mid\)),则将树状数组中\(a_{i,y}\)的位置加上\(a_i\)的权值
  4. \(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;
}
  1. 陌上花开:三维偏序问题

\(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;
}
posted @ 2022-11-30 22:31  spdarkle  阅读(242)  评论(0编辑  收藏  举报