CDQ分治

Part1:前置知识

归并排序,逆序对,二维偏序,树状数组

Part 2:CDQ分治

【模板题】三维偏序

(题目传送门)

题目大意

n 个元素,第 i 个元素有 aibici 三个属性,设 f(i) 表示满足 ajaibjbicjcijij 数量。

对于 d[0,n),求 f(i)=d 的数量。

解题思路

  • 同“二维偏序”,先按 a 数组从小到大排序

  • 现在考虑当 n=8 时,首先将数组一分为二,递归左边 [1,4] ,递归右边 [5,8], 再计算左边对右边的影响(即左边是否有元素能被右边的元素统计进它的 f )

  • 我们假设左边和右边内部的答案都已经计算得出,那么再来考虑左边对右边的贡献(影响)。此时问题就又变成了一个二维偏序,我们可以在左右两个区间内部按 b 的大小排序(因为内部答案已算出,内部排序不影响最终结果),再利用树状数组求出左边对右边的贡献.

  • 那么我们不断地对一个区间一分为二地递归,再计算左边对右边的影响,就可以计算出答案

注意事项

  • 因为题目的 f(i) 的判断条件可以取等号,所以我们排序完后需要将数组去重

  • 每层递归后,我们需要将树状数组清空。但用 memset 可能会超时,所以需要一个数组来记录修改的元素

代码

#include<bits/stdc++.h>
using namespace std;

const int N=100010,M=200010;

struct node
{
	int a,b,c,cnt,num; //cnt表示f(i)的大小(不取等号),num表示该元素的个数
}t[N],f[N];

int n,k,tot,c[M],ans[N];

bool cmp1(node x,node y)
{
	return x.a<y.a || (x.a==y.a && x.b<y.b) || (x.a==y.a && x.b==y.b && x.c<y.c);
}

bool cmp2(node x,node y)
{
	return x.b<y.b || (x.b==y.b && x.c<y.c);
}

void add(int x,int y)
{
	for(x; x<=k; x+=(x&-x))
		c[x]+=y;
}

int ask(int x)
{
	int s=0;
	for(; x; x-=(x&-x))
		s+=c[x];
	return s;
}

void solve(int l,int r)
{
	if(l==r)
		return;
		 
	int mid=(l+r)>>1;
	
	solve(l,mid); //一分为二地递归
	solve(mid+1,r);
	
	int len1=mid-l+1,len2=r-mid;
	
	sort(f+l,f+l+len1,cmp2);  //按b的大小排序
	sort(f+mid+1,f+mid+1+len2,cmp2);  
	
	vector <int> rec;  //统计树状数组修改了哪个元素
	for(int i=l,j=mid+1; j<=r; j++)
	{
		while(i<=mid && f[i].b<=f[j].b)
		{
			add(f[i].c,f[i].num);
			rec.push_back(i);
			i++;
		}
		
		f[j].cnt+=ask(f[j].c);  //更新答案
	}
	
	for(int i=0; i<rec.size(); i++) //清空树状数组
		add(f[rec[i]].c,-f[rec[i]].num);
}

int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1; i<=n; i++)
		scanf("%d%d%d",&t[i].a,&t[i].b,&t[i].c);
	
	sort(t+1,t+1+n,cmp1);  //按照a的大小排序
	
	int tt=0;
	for(int i=1; i<=n; i++) //去重
	{	
		tt++;
		if(t[i].a!=t[i+1].a || t[i].b!=t[i+1].b || t[i].c!=t[i+1].c)
		{
			f[++tot].a=t[i].a;
			f[tot].b=t[i].b;
			f[tot].c=t[i].c;
			f[tot].num=tt;
			tt=0;
		}
	}
	
	solve(1,tot); //CDQ分治
	
	for(int i=1; i<=tot; i++) //统计答案
		ans[f[i].cnt+f[i].num-1]+=f[i].num;
	
	for(int i=0; i<n; i++)
		printf("%d\n",ans[i]);

	return 0;
}

*总结:CDQ分治的模型

对于区间 [1,L]

1.设 mid=(l+r)>>1,递归计算 solve(l,r)

2.递归计算 solve(mid+1,r)

3.计算第 lmid 项操作对第 mid+1r 项操作的影响

时间复杂度: O(nlog2n)

关于上述模型的正确性,大家可自行证明

【练习题】Mokia

题目传送门

题目大意

维护一个 ww 的矩阵,初始值均为 0

每次操作可以增加某格子的权值,或询问某子矩阵的总权值

解题思路

  • 首先,如二维前缀和一般,对于左下角为 (x1,y1) ,右上角为 (x2,y2) 的询问,我们可以把它转化为四个询问: sum(x11,y11)sum(x11,y2)sum(x2,y11)sum(x2,y2)

  • 之后,我们发现,对于第 i 项查询,必定会受到前面修改操作的影响,因此,我们可以考虑CDQ分治。类似三维偏序,此问题的查询也有三维:时间 t ,行 x ,列 y 。所以,我们只需要寻找并计算 tj<tixjxiyjyi 的第 j 项修改对第 i 项查询的影响

  • 我们先对整个区间一分为二,对于两个独立的区间在里面进行CDQ分治,虽然右边的修改不会对左边的查询产生影响,但左边的修改会对右边的查询产生影响,所以我们还需计算左对右的影响

  • 计算左对右的影响时,因为左边的 t 始终小于右边的 t ,所以问题就变成了一个二维偏序:对 xjxiyjyi 的第 j 项修改进行计算

代码

#include<bits/stdc++.h>
using namespace std;

const int N=2000000,M=200010;

struct node
{
	int op,x,y; //op表示操作类型,x,y表示坐标
	int val,id; //val对于操作1来说就是增加量,对于操作2来说就是前缀和运算时是加还是减
				//id是对于操作2来说的,表示是第几个查询操作
}a[M];
int s,w,n,cnt,ans[M];  //n表示操作序列长度,cnt表示查询操作个数
int c[N]; //树状数组

bool cmp(node a,node b)
{
	return a.x<b.x || (a.x==b.x && a.y<b.y);
}

void add(int x,int y)
{
	for(x; x<=w; x+=(x&-x))
		c[x]+=y;
}

int ask(int x)
{
	int s=0;
	for(; x; x-=(x&-x))
		s+=c[x];
	return s;
} 

void solve(int l,int r)
{
	if(l==r)
		return;
	
	int mid=(l+r)>>1;
	solve(l,mid);
	solve(mid+1,r);
	
	int len1=mid-l+1,len2=r-mid;
	sort(a+l,a+l+len1,cmp); //对左半边和右半边排序
	sort(a+mid+1,a+mid+1+len2,cmp);
	
	vector <int> rec;
	for(int i=l,j=mid+1; j<=r; j++)
	{
		while(i<=mid && a[i].x<=a[j].x) //寻找xi<=xj
		{
			if(a[i].op==1) //如果是操作1
			{
				add(a[i].y,a[i].val); 
				rec.push_back(i);
			}
			i++;
		}
		
		ans[a[j].id]+=a[j].val*ask(a[j].y); //更新答案
	}
	
	for(int i=0; i<rec.size(); i++) //恢复树状数组
		add(a[rec[i]].y,-a[rec[i]].val);
}

int main()
{
	cin>>s>>w; //s无用
	
	int temp;
	while(cin>>temp && temp!=3)
	{
		if(temp==1)
		{
			int x,y,v;
			cin>>x>>y>>v;
			
			a[++n]=(node){1,x,y,v,0};
		}
		else
		{
			int x,y,xx,yy;
			cin>>x>>y>>xx>>yy;
			cnt++; //记录查询操作个数
			
			a[++n]=(node){2,x-1,y-1,1,cnt}; //拆分成4次查询操作
			a[++n]=(node){2,xx,y-1,-1,cnt};
			a[++n]=(node){2,x-1,yy,-1,cnt};
			a[++n]=(node){2,xx,yy,1,cnt};
		} 
	}
	
	solve(1,n);
	
	for(int i=1; i<=cnt; i++)
		cout<<ans[i]<<endl;
	
	return 0;
}

【练习题】天使玩偶

题目传送门

题目大意

  • 定义两个点之间的距离为 dist(A,B)=|AxBx|+|AyBy|
  • 在刚开始时,Ayu 已经知道有 n 个点可能埋着天使玩偶
  • 再接下来 m 行,每行三个非负整数 t,xi,yi
    • 如果 t=1,则表示 Ayu 又回忆起了一个可能埋着玩偶的点 (xi,yi)
    • 如果 t=2,则表示 Ayu 询问如果她在点 (xi,yi) 那么在已经回忆出来的点里,离她近的那个点有多远

解题思路

  • 首先来看问题的简化版——假设没有 t=1 的操作,这时问题的答案很明显为

    min{|xxi|+|yyi|},1in

  • 为了去掉绝对值符号,我们不妨把原来的询问分为 4 个,分别询问在 (x,y) 的左下、左上、右上、右下方向上距离最近的点有多远,4 个结果取最小值即为答案。

    以左下方向为例,此时要求的式子变为:

    min{(xxi)+(yyi)},1in

    进一步化简为:

    (x+y)max{xi+yi},1in,xix,yiy

  • 所以,对于左下方向的点,我们可以先将所有点按横坐标从小到大排序,再利用树状数组去求出 max{xi+yi} ,那么就完成了对左下方向的求解

  • 对于其它三个方向,我们可以通过坐标的变换,把它们均转化为左下方向

  • 那么现在,我们就要来考虑带有 t=1 的操作,我们可以把输入变成一个长度为 n+m 的序列,进行 4CDQ分治,即可求出答案

注意事项

  • 如果在 4 次CDQ里的每一层都进行sort排序,那么复杂度会变得非常大,所以我们在操作的过程中顺便进行归并排序,这样便大大节省了时间

  • 由于此题是一个平面直角坐标系,坐标可能会取到0,但树状数组在进行 lowbit(0) 运算时会出错,所以要将输入的所有坐标+1

  • 再进行坐标变换时,坐标会变成负数,此时需要给坐标加上一个偏移量,这个偏移量 = max{x,y}+1,注意要+1,否则最大的那个坐标变换后会变为0

  • 有一种特殊情况:某一点非常靠近边界,导致某次变换时,没有点在它的左下。这样查询时默认返回了0,最终的距离就成了这个点到原点的距离,但原点是不存在的(经过刚刚的更改,已经没有横坐标或纵坐标为0的点)。为避免这种情况,在树状数组查询时需要特判,若为0则返回 INF

代码

#include<bits/stdc++.h>
using namespace std;

const int N=1000010,M=1000010,INF=1e9;

struct node
{
	int op,x,y;
	int ans,id;
}a[N],b[N],s[N]; //b用于CDQ里的操作,s用于归并排序
int n,m,mx,c[M];

bool cmp(node a,node b)
{
	return a.x<b.x;
}

void add(int x,int y)
{
	for(; x<=mx; x+=(x&-x))
		c[x]=max(c[x],y);
} //树状数组查询最大值

int ask(int x)
{
	int s=0;
	for(; x; x-=(x&-x))
		s=max(c[x],s);
	return s==0? -INF:s; //特殊情况
}

void clear_(int x)
{
	for(; x<=mx; x+=(x&-x))
		c[x]=0; //清空树状数组
}

void solve(int l,int r)
{
	if(l==r)
		return;
	
	int mid=(l+r)>>1;
	
	solve(l,mid);
	solve(mid+1,r);
	
	int i=l,k=l;
	
	for(int j=mid+1; j<=r; j++)
	{
		while(i<=mid && b[i].x<=b[j].x)
		{
			if(b[i].op==1)
				add(b[i].y,b[i].x+b[i].y);
			
			s[k++]=b[i++]; //顺便进行归并排序
		}
		
		if(b[j].op==2)
			a[b[j].id].ans=min(a[b[j].id].ans,b[j].x+b[j].y-ask(b[j].y)); //更新答案
		
		s[k++]=b[j];
	}
	
	for(int j=l; j<i; j++) //清空树状数组
		if(b[j].op==1)
			clear_(b[j].y);
		
	while(i<=mid) //归并排序的善后工作
		s[k++]=b[i++];
	
	for(int i=l; i<=r; i++) //将排好序的s数组赋值给b数组
		b[i]=s[i];
}

void cdq(int xx,int yy) //xx和yy控制x和y坐标是否变换
{
	for(int i=1; i<=n+m; i++)
	{
		b[i]=a[i];
		if(!xx)
			b[i].x=-b[i].x+mx;
		if(!yy)
			b[i].y=-b[i].y+mx;
	}
	
	solve(1,n+m);
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1; i<=n; i++)
	{
		int xx,yy;
		scanf("%d%d",&xx,&yy);
		
		a[i].x=++xx;  a[i].y=++yy;
		a[i].op=1;  a[i].id=i;
		mx=max(mx,max(xx,yy));
	}
	for(int i=n+1; i<=n+m; i++)
	{
		int t,xx,yy;
		scanf("%d%d%d",&t,&xx,&yy);
		
		a[i].x=++xx;  a[i].y=++yy;
		a[i].op=t;  a[i].id=i;
		a[i].ans=INF;
		mx=max(mx,max(xx,yy));	
	}
	
	mx++;
	
	cdq(1,1);  cdq(1,0);  cdq(0,1);  cdq(0,0); //4次cdq

	for(int i=n+1; i<=n+m; i++)
		if(a[i].op==2)
			printf("%d\n",a[i].ans);

	return 0;
}
posted @   xishanmeigao  阅读(13)  评论(0编辑  收藏  举报
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示