cdq分治 基础篇

简介

前置芝士:归并排序。

\(cdq\) 分治是个离线算法,可以解决三维偏序或者优化 \(dp\)

陌上花开

维护三维偏序有个口诀:一维排序,二维归并,三维数据结构。

考虑第一维直接排序解决掉,然后还剩两维。

我们考虑第二维用归并排序解决掉。然后假设当前区间 \([l,r]\),区间中点 \(mid\)。那么我们通过递归可以解决 \([l,mid],[mid+1,r]\) 内部的贡献,现在要考虑解决两个点在两个区间中的贡献。

我们在递归时已经按照第二维排好序了。但是有个问题,第一维的限制呢?这样不是把第一维的限制忽略掉了吗?

再想一想我们求的东西,两个端点分别在两个区间内,而这时两个区间内的第一维的相对大小仍然是满足要求的,因为这时他们还没有完成第二维的排序。

然后如果我们发现这时第二维满足了要求,我们就把第三维扔进树状数组或者权值线段树内(推荐树状数组)。否则(重点),此时的 \([l,i-1]\) 是一个极大的 \(\forall u\in[l,i-1],u\) 满足前两维的偏序关系的区间。这时我们就要统计贡献了。

然后就是,我们把没跑完的部分跑完,但是这里的循环顺序非常重要,不能更改,具体原因写在代码注释里了。

最后记得堆原数组去个重,不然会出问题。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,m,k,c[N],siz[N],f[N],res[N];
struct node{
	int x,y,z,id;
	bool operator<(const node &t)const{
		if(x!=t.x)return x<t.x;
		if(y!=t.y)return y<t.y;
		return z<t.z;
	}
	bool operator!=(const node &t)const{
		return x!=t.x||y!=t.y||z!=t.z;
	}
}a[N],b[N];
int lowbit(int x){//树状数组三件套1
	return x&-x;
}
void add(int x,int v){//树状数组三件套2
	while(x<=k){
		c[x]+=v;
		x+=lowbit(x);
	}
}
int qry(int x){//树状数组三件套3
	int res=0;
	while(x){
		res+=c[x];
		x-=lowbit(x);
	}
	return res;//求有多少数比x小
}
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=0;
	while(i<=mid&&j<=r){
		if(a[i].y<=a[j].y){//如果满足,证明还不是极大区间
			add(a[i].z,siz[a[i].id]);//那么就把这个数存进去,注意有多个一起存
			b[now++]=a[i++];//归并排序
		}
		else{
			res[a[j].id]+=qry(a[j].z);//这时区间达到极大,可以统计
			b[now++]=a[j++];
		}
	}
	while(j<=r){//1,如果放在2后面会导致统计错误
		res[a[j].id]+=qry(a[j].z);
		b[now++]=a[j++];
	}
	for(int x=l;x<i;x++){//2,如果放到3后面会导致清空错误(多清空)
		add(a[x].z,-siz[a[x].id]);
	}
	while(i<=mid){//3,不能放到2前面,原因见2
		b[now++]=a[i++];
	}//上面这3个循环的顺序不能换
	for(i=l,j=0;i<=r;i++,j++){
		a[i]=b[j];//归并排序
	}
}
signed main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i].x>>a[i].y>>a[i].z;
	}
	sort(a+1,a+n+1);
	for(int i=1;i<=n;i++){
		if(a[i]!=a[i-1])b[++m]=a[i];//这里是去重
		siz[m]++;//记录相同的元素个数
	}
	for(int i=1;i<=m;i++){
		a[i]={b[i].x,b[i].y,b[i].z,i};//记录去重后的数组
	}
	cdq(1,m);
	for(int i=1;i<=m;i++){
		int id=a[i].id;
		//数量为去重后统计的答案再加上其他相同的元素的数量
		f[res[id]+siz[id]-1]+=siz[id];//这里减1是因为去掉自己
	}
	/*
	这里笔者写的时候有个疑惑,为什么把id改成i是对的,原因如下:
	首先,这两个写法从逻辑上来说不等价,但是结果上来说等价
	因为原来的id是按照第一维排序的,上面的i是按照第二维排序的,所以两者本质不同
	但是,由于id是不重复的,所以id恰好遍历1-m
	所以从结果上来说两者等价
	*/
	for(int i=0;i<n;i++){
		cout<<f[i]<<'\n';
	}
	return 0;
}

园丁的烦恼

这里说一下 \(cdq\) 分治怎么写。考虑一个事情,求一个矩阵中的点的数量,我们可以转化为二维前缀和计算,就是计算一下每个点左下方有多少个点。我们假设当前点是 \(i\),左下方的点为 \(j\),则满足 \(x_j\le x_i,y_j\le y_i\)

就是比方说求的东西是 \(A\),于是需要求出 \(A\) 这个矩形的四个顶点左下方有多少个点,然后做一个二维前缀和就可以求答案了。

然后再考虑,把初始给定点的 \(z\) 设成 \(0\),自己查询的点的 \(z\) 设成 \(1\)。于是又有 \(z_j<z_i\)。发现是一个裸的三维偏序。

但是,可以发现 \(z\) 只有 \(0,1\) 两种取值,于是不需要树状数组,拿一个变量记录即可。

于是看一下代码:

#include<bits/stdc++.h> 
#define N 500005
using namespace std;
int n,m,res[N];
struct node{
	int x,y,z,p,id,sign,sum;
	//sign为求前缀和这个矩阵是被加还是减
	bool operator<(const node &t)const{
		if(x!=t.x)return x<t.x;
		if(y!=t.y)return y<t.y;
		return z<t.z;
	}
}q[N*5],tmp[N*5];
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=0,sum=0;
	while(i<=mid&&j<=r){
		if(q[i].y<=q[j].y){
			sum+=(q[i].z==0)*q[i].p;//如果i是初始点才计算贡献 
			tmp[k++]=q[i++];
		}
		else{
			q[j].sum+=sum;//还是在区间达到极大时进行统计 
			tmp[k++]=q[j++];
		}
	}
	while(i<=mid){
		sum+=(q[i].z==0)*q[i].p;//没啥用,但是美观 
		tmp[k++]=q[i++];
	}
	while(j<=r){
		q[j].sum+=sum;//区间已经极大,不会再扩展 
		tmp[k++]=q[j++];
	}
	for(i=l,j=0;i<=r;i++,j++){
		q[i]=tmp[j];
	}
}
signed main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		int x,y;
		cin>>x>>y;
		q[i]={x,y,0,1};//0代表是初始就有的 
	}
	int k=n;
	for(int i=1;i<=m;i++){
		int x_1,y_1,x_2,y_2;
		cin>>x_1>>y_1>>x_2>>y_2;
		q[k++]={x_2,y_2,1,0,i,1};
		q[k++]={x_1-1,y_2,1,0,i,-1};
		q[k++]={x_2,y_1-1,1,0,i,-1};
		q[k++]={x_1-1,y_1-1,1,0,i,1};//前缀和的四个部分 
	}
	sort(q,q+k);
	cdq(0,k-1);
	for(int i=0;i<k;i++){
		if(q[i].z==1){
			res[q[i].id]+=q[i].sum*q[i].sign;//这里在求前缀和 
		}
	}
	for(int i=1;i<=m;i++){
		cout<<res[i]<<'\n';
	}
	return 0;
}

Little Artem and Time Machine

板子题。考虑开桶维护元素个数。然后第一维就是操作顺序,第二维是操作时间,第三维是元素值。

可以发现第一维不用动,我们对第二维归并排序,相当于考虑 \([l,mid]\) 中的修改对 \([mid+1,r]\) 的询问的影响,其他东西我们递归解决。

所以就是在发现当前 \(i\) 位置的时间更小时,如果是个修改我们对桶进行修改;如果 \(j\) 位置是个询问,直接查询桶。

然后就是,我们为了开桶,需要把元素值离散化一下,于是就做完了,可以看一下代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,tot,b[N],res[N],cnt[N];
struct node{
	int op,t,x,id;
}q[N],tmp[N];
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=0;
	while(i<=mid&&j<=r){
		if(q[i].t<=q[j].t){
			if(q[i].op==1)cnt[q[i].x]++;
			else if(q[i].op==2)cnt[q[i].x]--;
			tmp[k++]=q[i++];
		}
		else{
			if(q[j].op==3)res[q[j].id]+=cnt[q[j].x];//这里必须是+=,因为贡献会多次累加
			tmp[k++]=q[j++];
		}
	}
	while(i<=mid){
		if(q[i].op==1)cnt[q[i].x]++;
		else if(q[i].op==2)cnt[q[i].x]--;
		tmp[k++]=q[i++];
	}
	while(j<=r){
		if(q[j].op==3)res[q[j].id]+=cnt[q[j].x];
		tmp[k++]=q[j++];
	}
	for(i=l;i<=mid;i++)cnt[q[i].x]=0;
	for(i=l,j=0;i<=r;i++,j++)q[i]=tmp[j];
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>q[i].op>>q[i].t>>q[i].x;
		b[i]=q[i].x;
		if(q[i].op==3)q[i].id=++tot;
	}
	sort(b+1,b+n+1);
	m=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;i++){
		q[i].x=lower_bound(b+1,b+m+1,q[i].x)-b;
	}
	cdq(1,n);
	for(int i=1;i<=tot;i++){
		cout<<res[i]<<'\n';
	}
	return 0;
}

动态逆序对

我们先设每个位置 \(i\) 的数被删除的时间为 \(t_i\),如果这个数没有被删除,那么就随便找一个大数(注意不要重复)。

但是这样会要求我们用树状数组维护后缀和,这个东西比前缀和难维护,所以我们把删除时间翻转一下(即最早删除的 \(t\) 数组最大)。

然后我们考虑把逆序对的贡献记录到删除时间更晚的上面(但是实际的 \(t\) 小,因为反着记录的)。

所以现在如果我们对于 \((i,j)\) 能把贡献记录到 \(j\) 上,那么有两种可能:

  • \(i<j,t_i<t_j,val_i>val_j\)

  • \(i>j,t_i<t_j,val_i<val_j\)

然后就做两遍统计即可,代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,tr[N],ans[N],pos[N];
struct node{
    int a,t,res;
}q[N],w[N];
int lowbit(int x){
    return x&-x;
}
void add(int x,int v){
    for(int i=x;i<N;i+=lowbit(i)){
        tr[i]+=v;
    }
}
int qry(int x){
    int res=0;
    for(int i=x;i;i-=lowbit(i)){
        res+=tr[i];
    }
    return res;
}
void merge_sort(int l,int r){
    if(l>=r)return;
    int mid=l+r>>1;
    merge_sort(l,mid);
    merge_sort(mid+1,r);
    int i=mid,j=r;
    while(i>=l&&j>=mid+1){//这里倒着统计是因为保证[i+1,l]的val都比j的val大 
        if(q[i].a>q[j].a){//i<j,t_i<t_j,val_i>val_j
            add(q[i].t,1);
            i--;
        }
        else{
            q[j].res+=qry(q[j].t-1);
            j--;
        }
    }
    while(j>=mid+1)q[j].res+=qry(q[j].t-1),j--;//i的循环没必要做,因为不计入答案 
    for(int k=i+1;k<=mid;k++){//清空 
        add(q[k].t,-1);
    }
    j=l;i=mid+1;
    while(j<=mid&&i<=r){//这里正着统计的原因同上 
        if(q[i].a<q[j].a){//i>j,t_i<t_j,val_i<val_j
            add(q[i].t,1);
            i++;
        }
        else{
            q[j].res+=qry(q[j].t-1);
            j++;
        }
    }
    while(j<=mid)q[j].res+=qry(q[j].t-1),j++;
    for(int k=mid+1;k<i;k++){
        add(q[k].t,-1);
    }
    i=l;j=mid+1;//归并排序 
    int k=0;
    while(i<=mid&&j<=r){
        if(q[i].a<=q[j].a){
            w[k++]=q[i++];
        }
        else{
            w[k++]=q[j++];
        }
    }
    while(i<=mid)w[k++]=q[i++];
    while(j<=r)w[k++]=q[j++];
    for(i=l,j=0;j<k;i++,j++)q[i]=w[j];
}
signed main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        cin>>q[i].a;
        pos[q[i].a]=i;
    }
    for(int i=0,j=n;i<m;i++){
        int a;
        cin>>a;
        q[pos[a]].t=j--;//这里倒着记录,方便统计 
        pos[a]=-1;
    }
    for(int i=1,j=n-m;i<=n;i++){
        if(pos[i]!=-1){
            q[pos[i]].t=j--;//对于没有被删除的这样就可以 
        }
    }
    merge_sort(0,n-1);
    for(int i=0;i<n;i++)ans[q[i].t]=q[i].res;//这个贡献是在这个时间上的,需要前缀和得到答案 
    for(int i=1;i<=n;i++)ans[i]+=ans[i-1];//对每个时间做前缀和,得到要求的答案 
    for(int i=0,j=n;i<m;i++,j--){
        cout<<ans[j]<<'\n';//因为删除时间是倒着的,所以输出也要 
    }
    return 0;
}
posted @ 2024-07-23 22:55  zxh923  阅读(31)  评论(0编辑  收藏  举报