【学习笔记】cdq 分治

一、定义

cdq 分治是一种离线分治算法,一般有三种用途:

  1. 处理点对之间的问题

  2. 优化 1D/1D 动态规划

  3. 将动态问题转为静态问题

对于分治区间 \([l,r]\),确定一个中点 \(mid\),对于左右区间分别递归分治,然后再处理左右区间之间的贡献。啊显然归并排序就是 cdq 分治。

二、例题

1.【模板】树状数组 1

将以此询问拆成两个单点的前缀和,而修改操作就是单点修改。考虑对于一个修改 \((x,y)\) 和询问 \(p\),如果修改发生在询问前并且 \(x\le p\),那么修改就会对询问产生贡献。对于操作序列中的分治区间 \([l,r]\),我们按照类似归并排序的方式去遍历,那么左区间的修改操作就会影响到右区间的查询操作。排序后的序列是按操作的位置升序的,于是我们再记录一个 \(sum\) 表示当前增加了多少,对应地更新到答案中即可。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=5e5+5;
int n,m,ans[maxn];
struct node{
	int typ,pos,val;
	node(int typ=0,int pos=0,int val=0)
		:typ(typ),pos(pos),val(val){}
	il bool operator<(const node &x)const{
		if(pos==x.pos){
			return typ<x.typ;
		}
		else{
			return pos<x.pos;
		}
	}
}a[maxn*3],b[maxn*3];
il void cdq(int l,int r){
	if(l==r){
		return ;
	}
	int mid=(l+r)>>1;
	cdq(l,mid),cdq(mid+1,r);
	int p1=l,p2=mid+1,p3=l,sum=0;
	for(int i=l;i<=r;i++){
		if(p1<=mid&&a[p1]<a[p2]||p2>r){
			if(a[p1].typ==1){
				sum+=a[p1].val;
			}
			b[p3++]=a[p1++];
		}
		else{
			if(a[p2].typ==2){
				ans[a[p2].val]-=sum;
			}
			else if(a[p2].typ==3){
				ans[a[p2].val]+=sum;
			}
			b[p3++]=a[p2++];
		}
	}
	for(int i=l;i<=r;i++){
		a[i]=b[i];
	}
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	int cnt=0,tot=0;
	for(int i=1,x;i<=n;i++){
		cin>>x;
		a[++cnt]=node(1,i,x);
	}
	while(m--){
		int opt,x,y;
		cin>>opt>>x>>y;
		if(opt==1){
			a[++cnt]=node(1,x,y);
		}
		else{
			a[++cnt]=node(2,x-1,++tot);
			a[++cnt]=node(3,y,tot);
		}
	}
	cdq(1,cnt);
	for(int i=1;i<=tot;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

2.[bzoj3262]陌上花开

三维偏序。第一维可以直接排序,第二维用 cdq 维护,第三维存入树状数组。这里不需要像归并排序那样写,只需要按照 \(b\) 排序后,对于右区间的每个位置去找左区间有哪些满足性质即可。注意去重。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e5+5;
int n,m,ans[maxn];
struct node{
	int a,b,c,cnt,ans;
	node(int a=0,int b=0,int c=0,int cnt=0,int ans=0)
		:a(a),b(b),c(c),cnt(cnt),ans(ans){}
	il bool operator<(const node &x)const{
		if(a==x.a){
			if(b==x.b){
				return c<x.c;
			}
			return b<x.b;
		}
		return a<x.a;
	}
}p[maxn],P[maxn];
il bool cmp(const node &x,const node &y){
	if(x.b==y.b){
		return x.c<y.c;
	}
	return x.b<y.b;
}
int tr[maxn];
il int lowbit(int x){
	return x&-x;
}
il void add(int p,int v){
	for(;p<=m;p+=lowbit(p)){
		tr[p]+=v;
	}
}
il int query(int p){
	int res=0;
	for(;p;p-=lowbit(p)){
		res+=tr[p];
	}
	return res;
}
il void cdq(int l,int r){
	if(l==r){
		return ;
	}
	int mid=(l+r)>>1;
	cdq(l,mid),cdq(mid+1,r);
	sort(P+l,P+mid+1,cmp);
	sort(P+mid+1,P+r+1,cmp);
	int pp=l;
	for(int i=mid+1;i<=r;i++){
		while(pp<=mid&&P[i].b>=P[pp].b){
			add(P[pp].c,P[pp].cnt);
			pp++;
		}
		P[i].ans+=query(P[i].c);
	}
	for(int i=l;i<pp;i++){
		add(P[i].c,-P[i].cnt);
	}
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1,a,b,c;i<=n;i++){
		cin>>a>>b>>c;
		p[i]=node(a,b,c);
	}
	sort(p+1,p+n+1);
	int tot=0;
	for(int i=1,num=0;i<=n;i++){
		num++;
		if(p[i].a!=p[i+1].a||p[i].b!=p[i+1].b||p[i].c!=p[i+1].c){
			P[++tot]=node(p[i].a,p[i].b,p[i].c,num);
			num=0;
		}
	}
	cdq(1,tot);
	for(int i=1;i<=tot;i++){
		ans[P[i].cnt+P[i].ans-1]+=P[i].cnt;
	}
	for(int i=0;i<n;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

3.[bzoj1176][Balkan2007]Mokia

类似例 1,将查询差分后并入操作序列中。那么时间是一维,横竖各是一维,一共就有三维。那么就需要 cdq 套树状数组了。

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1.6e5+5,maxm=2e6+5;
int n,m,ans[maxn];
struct node{
	int typ,xp,yp,val;
	node(int typ=0,int xp=0,int yp=0,int val=0)
		:typ(typ),xp(xp),yp(yp),val(val){}
	il bool operator<(const node &x)const{
		if(xp==x.xp){
			return typ<x.typ;
		}
		return xp<x.xp;
	}
}a[maxn<<2],b[maxn<<2];
int tr[maxm];
il int lowbit(int x){
	return x&-x;
}
il void add(int p,int v){
	for(;p<=n;p+=lowbit(p)){
		tr[p]+=v;
	}
}
il int query(int p){
	int res=0;
	for(;p;p-=lowbit(p)){
		res+=tr[p];
	}
	return res;
}
il void cdq(int l,int r){
	if(l==r){
		return ;
	}
	int mid=(l+r)>>1;
	cdq(l,mid),cdq(mid+1,r);
	int p1=l,p2=mid+1,p3=l;
	for(int i=l;i<=r;i++){
		if(p1<=mid&&a[p1]<a[p2]||p2>r){
			if(a[p1].typ==1){
				add(a[p1].yp,a[p1].val);
			}
			b[p3++]=a[p1++];
		}
		else{
			if(a[p2].typ==2){
				ans[a[p2].val]+=query(a[p2].yp);
			}
			else if(a[p2].typ==3){
				ans[a[p2].val]-=query(a[p2].yp);
			}
			b[p3++]=a[p2++];
		}
	}
	for(int i=l;i<=mid;i++){
		if(a[i].typ==1){
			add(a[i].yp,-a[i].val);
		}
	}
//	for(int i=0;i<=n;i++){
//		cout<<query(i)<<" ";
//	}
//	puts("");
	for(int i=l;i<=r;i++){
		a[i]=b[i];
	}
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>m>>n;
	int opt,tot=0,cnt=0;
	while(cin>>opt){
		if(opt==1){
			int x,y,v;
			cin>>x>>y>>v;
			a[++tot]=node(1,x,y,v);
		}
		else if(opt==2){
			int x1,y1,x2,y2;
			cin>>x1>>y1>>x2>>y2;
			ans[++cnt]=m*(x2-x1+1)*(y2-y1+1);
			a[++tot]=node(2,x1-1,y1-1,cnt);
			a[++tot]=node(2,x2,y2,cnt);
			a[++tot]=node(3,x1-1,y2,cnt);
			a[++tot]=node(3,x2,y1-1,cnt);
		}
	}
	cdq(1,tot);
	for(int i=1;i<=cnt;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
signed main(){return asbt::main();}

4.[Violet 3]天使玩偶

两点 \((x_i,y_i),(x_j,y_j)\) 间的距离为 \(|x_i-x_j|+|y_i-y_j|\),当 \(x_i\ge x_j\land y_i\ge y_j\) 时,就变成了 \((x_i+y_i)-(x_j+y_j)\)。于是我们可以对左上,左下,右上,右下各跑一遍 cdq 套树状数组。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e6+5,lim=1e6+1;
int n,m,tot,cnt,ans[maxn];
struct node{
	int typ,xx,yy;
	node(int typ=0,int xx=0,int yy=0)
		:typ(typ),xx(xx),yy(yy){}
	il bool operator<(const node &x)const{
		if(xx==x.xx){
			return typ<x.typ;
		}
		return xx<x.xx;
	}
}hp[maxn],a[maxn],b[maxn];
struct{
	int tr[maxn];
	il int lowbit(int x){
		return x&-x;
	}
	il void init(){
		memset(tr,-0x3f,sizeof tr);
	}
	il void upd(int p,int x){
		for(;p<=lim;p+=lowbit(p)){
			tr[p]=max(tr[p],x);
		}
	}
	il int query(int p){
		int res=-1e9;
		for(;p;p-=lowbit(p)){
			res=max(res,tr[p]);
		}
		return res;
	}
	il void clear(int p){
		for(;p<=lim;p+=lowbit(p)){
			tr[p]=-1e9;
		}
	}
}F;
il void cdq(int l,int r){
	if(l==r){
		return ;
	}
	int mid=(l+r)>>1;
	cdq(l,mid),cdq(mid+1,r);
	int p1=l,p2=mid+1,p3=l;
	for(int i=l;i<=r;i++){
		if(p1<=mid&&a[p1]<a[p2]||p2>r){
			if(!a[p1].typ){
				F.upd(a[p1].yy,a[p1].xx+a[p1].yy);
			}
			b[p3++]=a[p1++];
		}
		else{
			if(a[p2].typ){
				ans[a[p2].typ]=min(ans[a[p2].typ],a[p2].xx+a[p2].yy-F.query(a[p2].yy));
			}
			b[p3++]=a[p2++];
		}
	}
	for(int i=l;i<=mid;i++){
		if(!a[i].typ){
			F.clear(a[i].yy);
		}
	}
	for(int i=l;i<=r;i++){
		a[i]=b[i];
	}
}
il void work(bool flax,bool flay){
	for(int i=1;i<=tot;i++){
		a[i].typ=hp[i].typ;
		if(flax){
			a[i].xx=hp[i].xx;
		}
		else{
			a[i].xx=lim-hp[i].xx+1;
		}
		if(flay){
			a[i].yy=hp[i].yy;
		}
		else{
			a[i].yy=lim-hp[i].yy+1;
		}
	}
	cdq(1,tot);
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1,x,y;i<=n;i++){
		cin>>x>>y;
		hp[++tot]=node(0,x+1,y+1);
	}
	while(m--){
		int opt,x,y;
		cin>>opt>>x>>y;
		if(opt==1){
			hp[++tot]=node(0,x+1,y+1);
		}
		else{
			hp[++tot]=node(++cnt,x+1,y+1);
		}
	}
	memset(ans,0x3f,sizeof ans);
	F.init();
	work(0,0),work(0,1),work(1,0),work(1,1);
	for(int i=1;i<=cnt;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

5.[Cqoi2011]动态逆序对

考虑对答案进行差分,求出每一次删除到下一次删除的逆序对减少数,再做后缀和。

对于两个元素 \(i\)\(j\),如果它们的值为 \(val\),下标为 \(pos\),被删除的时间为 \(time\),那么如果满足 \((val_i<val_j\land pos_i>pos_j)\lor(val_i>val_j\land pos_i<pos_j)\),就可以产生一个逆序对。钦定 \(time_i<time_j\),这就对 \(time_i\) 的答案产生了 \(1\) 的贡献。这是一个三维偏序问题,cdq 即可。

值得注意的是 \(m\) 次操作不一定会把数组删完,而对于最后剩下的那部分元素在 cdq 里是不容易直接计算的(因为 \(time\) 相同)。所以对于剩下的那部分不妨直接用树状数组处理掉。

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m,ans[maxn];
struct node{
	int tim,pos,val;
}a[maxn],b[maxn];
struct{
	int n,tr[maxn];
	il void init(){
		memset(tr,0,sizeof tr);
	}
	il int lowbit(int x){
		return x&-x;
	}
	il void add(int p,int v){
		for(;p<=1e5;p+=lowbit(p)){
			tr[p]+=v;
		}
	}
	il int query(int p){
		int res=0;
		for(;p;p-=lowbit(p)){
			res+=tr[p];
		}
		return res;
	}
}F;
template<typename T>il void cdq(int l,int r,T cmp){
	if(l==r){
		return ;
	}
	int mid=(l+r)>>1;
	cdq(l,mid,cmp),cdq(mid+1,r,cmp);
	int p1=l,p2=mid+1,p3=l;
	for(int i=l;i<=r;i++){
		if(p1<=mid&&cmp(a[p1],a[p2])||p2>r){
			F.add(a[p1].tim,1);
			b[p3++]=a[p1++];
		}
		else{
			ans[a[p2].tim]+=F.query(1e5)-F.query(a[p2].tim);
			b[p3++]=a[p2++];
		}
	}
	for(int i=l;i<=mid;i++){
		F.add(a[i].tim,-1);
	}
	for(int i=l;i<=r;i++){
		a[i]=b[i];
	}
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1,x;i<=n;i++){
		cin>>x;
		a[x].val=x,a[x].pos=i;
	}
	for(int i=1,x;i<=m;i++){
		cin>>x;
		a[x].tim=i;
	}
	for(int i=1;i<=n;i++){
		if(!a[i].tim){
			a[i].tim=m+1;
			ans[m+1]+=F.query(1e5)-F.query(a[i].pos);
			F.add(a[i].pos,1);
		}
	}
	F.init();
	sort(a+1,a+n+1,[](const node &x,const node &y){return x.pos<y.pos;});
	cdq(1,n,[](const node &x,const node &y){return x.val>y.val;});
	sort(a+1,a+n+1,[](const node &x,const node &y){return x.pos>y.pos;});
	cdq(1,n,[](const node &x,const node &y){return x.val<y.val;});
	for(int i=m;i;i--){
		ans[i]+=ans[i+1];
	}
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
signed main(){return asbt::main();}

6.[Tjoi2016&Heoi2016]序列

记每个位置 \(i\) 可能变成的值(不包括原数组)中最大的为 \(mx_i\),最小的为 \(mn_i\),设 \(dp_i\) 表示以 \(i\) 结尾的最长合法子序列的长度,那么有转移方程:

\[dp_i=\max_{j<i\land a_j\le a_i\land mx_j\le a_i\land a_j\le mn_i}\{dp_j+1\} \]

看起来是个四维偏序,不是很好优化(总不能 cdq 套树套树吧?)。考虑将 \(mx_i\)\(mn_i\) 分别与 \(a_i\) 取最大、最小值,那么第二个条件就可以省掉了。也就是说只需满足三维偏序:

\[\begin{cases} j<i\\ mx_j\le a_i\\ a_j\le mn_i \end{cases} \]

那么就可以用 cdq 优化了。每次都需要将自己和左右区间重新排序。注意由于 dp 的转移顺序是固定的,因此一定要先递归左区间,再处理左右区间之间的转移,再递归右区间。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m,dp[maxn];
struct node{
	int val,mx,mn,pos;
}a[maxn];
struct fenwick{
	int tr[maxn];
	fenwick(){
		memset(tr,-0x3f,sizeof tr);
	}
	il int lowbit(int x){
		return x&-x;
	}
	il void upd(int p,int v){
		for(;p<=1e5;p+=lowbit(p)){
			tr[p]=max(tr[p],v);
		}
	}
	il void clear(int p){
		for(;p<=1e5;p+=lowbit(p)){
			tr[p]=-1e9;
		}
	}
	il int query(int p){
		int res=-1e9;
		for(;p;p-=lowbit(p)){
			res=max(res,tr[p]);
		}
		return res;
	}
}F;
il void cdq(int l,int r){
	sort(a+l,a+r+1,[](const node &x,const node &y){return x.pos<y.pos;});
	if(l==r){
//		cout<<a[l].pos<<" "<<dp[a[l].pos]<<"\n";
		dp[a[l].pos]=max(dp[a[l].pos],1);
		return ;
	}
	int mid=(l+r)>>1;
	cdq(l,mid);
	sort(a+l,a+mid+1,[](const node &x,const node &y){return x.mx<y.mx;});
	sort(a+mid+1,a+r+1,[](const node &x,const node &y){return x.val<y.val;});
	int p1=l,p2=mid+1;
	for(int i=l;i<=r;i++){
		if(p1<=mid&&a[p1].mx<=a[p2].val||p2>r){
			F.upd(a[p1].val,dp[a[p1].pos]);
			p1++;
		}
		else{
			dp[a[p2].pos]=max(dp[a[p2].pos],F.query(a[p2].mn)+1);
			p2++;
		}
	}
	for(int i=l;i<=mid;i++){
		F.clear(a[i].val);
	}
	cdq(mid+1,r);
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i].val;
		a[i].mx=a[i].mn=a[i].val;
		a[i].pos=i;
	}
	for(int i=1,p,v;i<=m;i++){
		cin>>p>>v;
		a[p].mx=max(a[p].mx,v);
		a[p].mn=min(a[p].mn,v);
	}
	cdq(1,n);
	int ans=0;
	for(int i=1;i<=n;i++){
//		cout<<dp[i]<<" ";
		ans=max(ans,dp[i]);
	}
//	cout<<"\n";
	cout<<ans;
	return 0;
}
}
int main(){return asbt::main();}
posted @ 2025-04-19 16:14  zhangxy__hp  阅读(147)  评论(0)    收藏  举报