暑假集训__cdq分治

cdq分治

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

主要思想

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

大概是

  1. 找到区间l,r的mid

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

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

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

我们可以看一道题:

P1908 逆序对

大家肯定都学过了归并排序求逆序对

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

具体可看代码:

cpp 复制代码
// 贺的
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

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

再次进行比较,发现 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
点击查看代码

cpp 复制代码
#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
点击查看代码

cpp 复制代码
#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;});
	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)

cpp 复制代码
#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';

}