暑假集训__cdq分治

cdq分治

一种分治方法,主要解决有着过于强大的偏序限制可离线操作问题。

主要思想

和所有分治思想一样,将一个大问题分为两个小问题,归纳解决小问题,然后计算出其之间的贡献,即可合并为这个问题的答案。

大概是

  1. 找到区间[l,r]的mid

  2. 解决 [l,mid] , [mid+1,r] 两个小问题

  3. 计算左区间对于右区间贡献并更新右区间的ans

问题是如何计算两个小问题后并之后的贡献。

我们可以看一道题:

P1908 逆序对

大家肯定都学过了归并排序求逆序对
那么我们在思考一下,这其实也是一种分治:首先解决了 $ [l,mid],[mid+1,r] $,然后在合并时需要计算左边对右边的影响, 具体则是类似于双指针,右边挨个看左边有多少比它大的 ,可以参考代码理解以下过程 。

具体可看代码:

// 贺的
inline void cdq(int l,int r){
	if(l==r)return;
	int mid=(l+r)>>1;
	cdq(l,mid);//递归左区间
	cdq(mid+1,r);//递归右区间
	int i=l,j=mid+1,now=l;//i,j表示左右指针,now表示b数组的指针
	while(i<=mid&&j<=r){
		if(a[i]<=a[j])b[now++]=a[i++];//不产生贡献
		else{//产生贡献
			b[now++]=a[j++];
			ans+=mid-i+1;//计算贡献
		}
	}
	while(i<=mid)b[now++]=a[i++];
	while(j<=r)b[now++]=a[j++];
	for(int i=l;i<=r;i++)a[i]=b[i];
	return;
}

\[\]

首先我们对 $ [l,mid],[mid+1,r] $ 进行完操作后,他们应该是已经排好序的。

如 [1,4,6] , [2,3,8]

初始状态,双指针都在最开始,$ i -> l , j -> mid+1 $

屏幕截图 2026-07-12 204224

首先比较 $ i $和 $ j $ 两指针所指向的谁比较小,可以看到 $ i $ 所指的比较小 ,左侧不需要考虑新贡献,所以只需要将其加入b里面以方便排序,将 $ i $ 向右移动一格。

屏幕截图 2026-07-12 211453

再次进行比较,发现 $ j $ 所指的比较小,需要计算贡献,此时 $ > j $ 的有 $ mid - i + 1 $ 个,加在 $ ans $ 上即可,然后将 $ j $ 所指的加到 $ b $ 数组里即可 , $ j $ 右移。

然后依次进行比较:

  1. $ j $
  2. $ i $
  3. $ i $
  4. $ j $

最后输出 $ ans $ 即可 , 复杂度 $ O ( n \log n ) $

那么我们大概可以理解cdq的主要思想了,接下来看几道例题。

例题

P4390 [BalkanOI 2007] Mokia 摩基亚

首先不难想到一个矩形 $ [a,b] , [c,d] , ( a <= c , b <= d ) $ , 可以拆成 $ [c,d] , [c,b-1] , [a-1,d] , [a-1,b-1] $ 四个与 $ [1,1] $ 组合的大矩形计算。

于是题目转化为了,给出对任意 $ [x,y] $ 的操作和询问,涉及二维偏序(其实还有时间一维)。

类似于上面,可以在cdq双指针里套一层树状数组解决,可以做到 $ n \log ^2 n $ 。

关于时间复杂度的证明: 首先cdq本身是一个log,在每个双指针进行操作时,我们将直接操作改为树状数组,相当于每个操作都乘了一个 $ \log m , m $ 是不确定的,但 $ m \le n $ ,所以其实复杂度是介于 $ n\log n , n \log^2 n $ 之间的,但基本认为是近似于 $ n \log^2 n $ 。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
// #define int ll
const int N=2e5+10,inf=0x3f3f3f3f,mod=1e9+7;
inline ll read(){
	char c=getchar();ll x=0;bool f=0;
	while(!isdigit(c))f=c=='-'?1:0,c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return f?-x:x;
}
mt19937 rd(233);
int s,w,cnt,num;
struct jj{
	int a,b,op,id;
	inline void out(){cout<<a<<' '<<b<<' '<<op<<' '<<id<<endl;}
}q[N],tp[N];
int c[N*10],ans[N];
inline void add(int x,int y){while(x<=w)c[x]+=y,x+=x&-x;}
inline int ask(int x){int ans=0;while(x)ans+=c[x],x^=x&-x;return ans;}
inline void cdq(int l,int r){
	if(l==r)return;
	int mid=l+r>>1;
	cdq(l,mid),cdq(mid+1,r);
	int i=l,j=mid+1,k=l;
	while(i<=mid&&j<=r){
		if(q[i].a<=q[j].a){
			if(q[i].op>0)add(q[i].b,q[i].op);
			tp[k++]=q[i++];
		}
		else{
			if(q[j].op==-1)ans[q[j].id]+=ask(q[j].b);
			else if(q[j].op==-2)ans[q[j].id]-=ask(q[j].b);
			tp[k++]=q[j++];
		}
	}
	while(j<=r){
		if(q[j].op==-1)ans[q[j].id]+=ask(q[j].b);
		else if(q[j].op==-2)ans[q[j].id]-=ask(q[j].b);
		tp[k++]=q[j++];
	}
	for(int p=l;p<i;++p)if(q[p].op>0)add(q[p].b,-q[p].op);
	while(i<=mid)tp[k++]=q[i++];
	while(l<=r)q[l]=tp[l],++l;
}
signed main(){
	s=read(),w=read();
	int op=0,x,y,xx,yy;
	while(op!=3){
		op=read();
		if(op==1)x=read(),y=read(),xx=read(),q[++cnt]={x,y,xx,0};
		else if(op==2)x=read(),y=read(),xx=read(),yy=read(),q[++cnt]={xx,yy,-1,++num},q[++cnt]={xx,y-1,-2,num},q[++cnt]={x-1,yy,-2,num},q[++cnt]={x-1,y-1,-1,num},ans[num]=(xx*yy-(xx*(y-1))-((x-1)*yy)+((x-1)*(y-1)))*s;
	}
	cdq(1,cnt);
	for(int i=1;i<=num;++i)
		cout<<ans[i]<<'\n';
}

P3810 【模板】三维偏序 / 陌上花开

其实同样这次是真的三维偏序,只需先sort解决一维,然后和上面一样cdq+树状数组干掉两维。

需要注意的是,挂=就很难处理,一个不错的思路是分为操作和询问分类进行处理,在sort里优先将操作放在前面以产生对后面的影响,最后答案-1即可 , $ n \log ^2 n $

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
const int N=2e5+10,inf=0x3f3f3f3f,mod=1e9+7;
inline ll read(){
	char c=getchar();ll x=0;bool f=0;
	while(!isdigit(c))f=c=='-'?1:0,c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return f?-x:x;
}
mt19937 rd(233);
int n,k;
struct jj{
	int a,b,c,ans,op;
	//op : 1 -> ask, 0-> do
	inline void out(){cout<<a<<' '<<b<<' '<<c<<' '<<ans<<endl;}
}q[N],qq[N];
int c[N],anss[N];
inline void add(int x,int v){while(x<=k)c[x]+=v,x+=x&-x;}
inline int ask(int x){int ans=0;while(x)ans+=c[x],x^=x&-x;return ans;}
inline void cdq(int l,int r){
	if(l==r)return;
	int mid=l+r>>1;
	cdq(l,mid),cdq(mid+1,r);
	int i=l,j=mid+1,kk=l;
	while(j<=r){
		while(i<=mid&&q[i].b<=q[j].b){if(!q[i].op)add(q[i].c,1);qq[kk++]=q[i++];}
		if(q[j].op){
			q[j].ans+=ask(q[j].c);
		}
		qq[kk++]=q[j++];
	}
	for(int p=l;p<i;++p)
		if(!q[p].op){add(q[p].c,-1);}
	while(i<=mid)qq[kk++]=q[i++];
	while(l<=r)q[l]=qq[l],++l;
}
int main(){
	#ifndef ONLINE_JUDGE
	freopen("in.in","r",stdin);
	freopen("out.out","w",stdout);
	#endif
	n=read(),k=read();
	for(int i=1;i<=n;++i)
		q[i*2-1]={read(),read(),read(),0,0},q[i<<1]=q[(i<<1)-1],q[i<<1].op=1;
	sort(q+1,q+1+n*2,[](jj x,jj y){return x.a==y.a?x.b==y.b?x.c==y.c?x.op<y.op:x.c<y.c:x.b<y.b:x.a<y.a;});
	// 如果你WA了,可以看看这里的排序(看不懂的可以去了解一下 lamda 表达式,GGrun懒得改了),我们想要在相同情况下严格排序,以使得所有可造成贡献的全都在他左边,此处需慎重处理
	cdq(1,n*2);
	for(int i=1;i<=n*2;++i){
		q[i].ans-=1;
		if(q[i].op){
			++anss[q[i].ans];
		}
	}
	for(int i=0;i<n;++i)
		cout<<anss[i]<<'\n';
}

进阶(从三维偏序到四维偏序)

根据上面的题目,我们似乎只会最多三维偏序,接下来学习四维偏序怎么处理。

P14957 【模板】离线静态四维数点

首先转化一下题意,可以将 $ x2 , y2 $ 取负,这样我们的条件都是 $ x \le X , y \le Y $ 这样后面也不用考虑谁大谁小了。

事实上,cdq是可以嵌套的。

啥意思? 其实我们可以想一下,对于计数的条件是四维都满足 $ \le $ 的关系,那么我们首先 $ sort + cdq $ 解决一维,然后变成了三维,如果再套一层 $ cdq + 双指针 + 树状数组 $ 即可,可是从第一层cdq如何转向第二层cdq,并保证第一层的cdq所排序的信息有效。

在 cdq1 时,我们首先递归解决了 $ [l,mid] , [mid+1,r] $ 接下来如何计算左边对右边的影响? 我们知道,经过递归,左右序列均已按照 $ y1 $ (这里我定义 $ y1 $ 为第二关键字,别的也可以) 排好序了,并且第一层的排序使得只能左边对右边产生影响,剩下的两维需要传到 cdq2 里面去解决。

可是在cdq2之前,我们已经按第二关键字,排完序了,我们需要保住第一层的信息,即 区分好左边和右边 ,所以我们在进入cdq2之前要先给 $ [l,mid] $ 部分打上 左 标记 , 给 $ [mid+1,r] $ 打上 右 标记,然后传入cdq2后只能由既有左标记,又在新排序后左侧的数给既有右标记,又在新排序后右侧的数造成贡献。这样,在同时保证一二维的情况下可以用双指针加树状数组解决后两维,复杂度 $ O (n \log^3 n) $

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
const int N=8e5+10,inf=0x3f3f3f3f,mod=1e9+7;
inline ll read(){
	char c=getchar();ll x=0;bool f=0;
	while(!isdigit(c))f=c=='-'?1:0,c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return f?-x:x;
}
struct jj{
	int x1,y1,x2,y2,op;
	bool man;
	inline void out(){cout<<x1<<' '<<y1<<' '<<x2<<' '<<y2<<' '<<op<<' '<<man<<endl;}
	// op : 0->do 1->ask
	// man : 0->l 1->r
}q[N],tp[N];
int n,m,ans[N],cnt;
unordered_map<int,int> ma;
inline bool cmp(jj x,jj y){
	return x.x1==y.x1?x.y1==y.y1?x.x2==y.x2?x.y2==y.y2?x.op<y.op:x.y2<y.y2:x.x2<y.x2:x.y1<y.y1:x.x1<y.x1;
}
inline bool cmp1(jj x,jj y){
	return x.y1==y.y1?x.x2==y.x2?x.y2==y.y2?x.op<y.op:x.y2<y.y2:x.x2<y.x2:x.y1<y.y1;
}
int c[N];
inline void add(int x,int v){while(x<=cnt)c[x]+=v,x+=x&-x;}
inline int ask(int x){int ans=0;while(x)ans+=c[x],x^=x&-x;return ans;}
inline void sol(int l,int r){
	if(l==r)return;
	int mid=l+r>>1;
	sol(l,mid),sol(mid+1,r);
	int i=l,j=mid+1,k=l;
	while(i<=mid&&j<=r){
		if(q[i].x2<=q[j].x2){
			if(!q[i].man&&!q[i].op)add(q[i].y2,1);
			tp[k++]=q[i++];
		}
		else{
			if(q[j].man&&q[j].op)ans[q[j].op]+=ask(q[j].y2);
			tp[k++]=q[j++];
		}
	}
	while(j<=r){
		if(q[j].man&&q[j].op)ans[q[j].op]+=ask(q[j].y2);
		tp[k++]=q[j++];
	}
	for(int p=l;p<i;++p)
		if(!q[p].man&&!q[p].op)add(q[p].y2,-1);
	while(i<=mid)tp[k++]=q[i++];
	while(l<=r)q[l]=tp[l],++l;
}
inline void cdq(int l,int r){
	if(l==r)return;
	int mid=l+r>>1;
	cdq(l,mid),cdq(mid+1,r);
	for(int i=l;i<=mid;++i)
		q[i].man=0;
	for(int i=mid+1;i<=r;++i)
		q[i].man=1;
	stable_sort(q+l,q+r+1,cmp1);
	sol(l,r);
}
vector<int> v;
int main(){
	#ifndef ONLINE_JUDGE
	freopen("in.in","r",stdin);
	freopen("out.out","w",stdout);
	#endif
	n=read(),m=read();
	for(int i=1;i<=n;++i)
		q[i].x1=read(),q[i].y1=read(),q[i].x2=-read(),q[i].y2=-read(),q[i].op=0;
	for(int i=1;i<=m;++i)
		q[i+n].x1=read(),q[i+n].y1=read(),q[i+n].x2=-read(),q[i+n].y2=-read(),q[i+n].op=i;
	for(int i=1;i<=n+m;++i)
		v.ps(q[i].y2);
	sort(v.begin(),v.end());unique(v.begin(),v.end());
	for(auto i:v)
		ma[i]=++cnt;
	for(int i=1;i<=n+m;++i)
		q[i].y2=ma[q[i].y2];
	sort(q+1,q+1+n+m,cmp);
	cdq(1,n+m);
	for(int i=1;i<=m;++i)
		cout<<ans[i]<<'\n';

}

再给出一道四维偏序的例题:

其实大家可以先做一下 P4093 [HEOI2016/TJOI2016] 序列 , 可以学习一下cdq优化dp的思路。

P5621 [DBOI2019] 德丽莎世界第一可爱

首先我们已经可以处理四维偏序,但这题困难其实在于DP部分的思想和之前cdq惯用部分不同,之前是单纯的计数,先记哪个部分没有影响,但DP是有严格的转移顺序的,必须左边全都做完了,才能做右边。

也就是说,我们不能上来就处理 $ [l,mid] , [mid+1,r] $ ,因为右边部分内部转移时,并未进行左边到右边的转移,顺序是错的,事实上,应该先处理了 $ [l,mid] $ ,毕竟这部分和右边无关,然后计算左边到右边的转移,然后再处理 $ [mid+1,r] $ 的内部转移。

具体题解可以看luogu

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
#define rint register int
#define G cout<<"-------------------"<<endl
inline ll read(){
	char c=getchar();ll x=0,f=1;
	while(!isdigit(c))(c=='-'?f=-1:0),c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}
#define int ll
const int N=1e6+10,inf=0x7fffffff,B=1e5+1,BB=B<<1;
const ll linf=0x3f7f7f7f7f7f7f7f,mod=1e9+7;
ll f[N],w[N];
int n;
struct BIT{
	ll c[BB];
	inline void add(int x,ll y){
		// cout<<x<<' '<<y<<endl;
		while(x<BB)
			c[x]=max(c[x],y),x+=x&-x;
	}
	inline ll ask(int x){
		ll ans=-linf;
		while(x)
			ans=max(ans,c[x]),x^=x&-x;
		return ans;
	}
	inline void clear(int x){
		while(x<BB)
			c[x]=-linf,x+=x&-x;
	}
}T;
struct jj{
	int a,b,c,d,op,id;
	inline void print(){
		cout<<a<<' '<<b<<' '<<c<<' '<<d<<' '<<op<<' '<<id<<endl;
	}
}q[N],tp[N],tpp[N];
inline bool cmpb(const jj&x,const jj&y){
	if(x.b!=y.b)return x.b<y.b;
	if(x.c!=y.c)return x.c<y.c;
	if(x.d!=y.d)return x.d<y.d;
	if(x.a!=y.a)return x.a<y.a;
	return w[x.id]>w[y.id];
}
inline bool cmpa(const jj&x,const jj&y){
	if(x.a!=y.a)return x.a<y.a;
	if(x.b!=y.b)return x.b<y.b;
	if(x.c!=y.c)return x.c<y.c;
	if(x.d!=y.d)return x.d<y.d;
	return w[x.id]>w[y.id];
}
inline bool cmpc(const jj&x,const jj&y){
	if(x.c!=y.c)return x.c<y.c;
	if(x.d!=y.d)return x.d<y.d;
	if(x.a!=y.a)return x.a<y.a;
	if(x.b!=y.b)return x.b<y.b;
	return w[x.id]>w[y.id];
}
inline void man(int l,int r){
	if(l==r)return;
	int mid=l+r>>1;
	man(l,mid);
	for(int i=l;i<=r;++i)
		tpp[i]=tp[i];
	stable_sort(tpp+l,tpp+mid+1,cmpc);
	stable_sort(tpp+mid+1,tpp+r+1,cmpc);
	for(int j=mid+1,i=l;j<=r;++j){
		while(i<=mid&&tpp[i].c<=tpp[j].c){
			if(!tpp[i].op)T.add(tpp[i].d,f[tpp[i].id]);
			++i;
		}
		if(tpp[j].op)f[tpp[j].id]=max(f[tpp[j].id],T.ask(tpp[j].d)+w[tpp[j].id]);
	}
	for(int i=l;i<=mid;++i)
		T.clear(tpp[i].d);
	man(mid+1,r);
	
}

inline void cdq(int l,int r){
	if(l==r)return;
	int mid=l+r>>1;
	cdq(l,mid);
	for(int i=l;i<=r;++i)
		tp[i]=q[i],tp[i].op=(i>mid);
	stable_sort(tp+l,tp+r+1,cmpb);
	
	man(l,r);
	cdq(mid+1,r);

}
 main(){
	#ifndef ONLINE_JUDGE
	freopen("in.in","r",stdin);
	freopen("out.out","w",stdout);
	#endif
	ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
	n=read();
	memset(T.c,0xcf,sizeof(T.c));
	for(int i=1,a,b,c,d,z;i<=n;++i){
		a=read()+B,b=read()+B,c=read()+B,d=read()+B,z=read();q[i]={a,b,c,d,0,i};f[i]=z;w[i]=z;
	}
	stable_sort(q+1,q+1+n,cmpa);
	cdq(1,n);
	for(int i=2;i<=n;++i)
		f[1]=max(f[1],f[i]);
	cout<<f[1];
}
posted @ 2026-07-15 08:50  lzrG23  阅读(67)  评论(0)    收藏  举报