【强基】CDQ分治

Part1:前置知识

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

Part 2:CDQ分治

【模板题】三维偏序

(题目传送门)

题目大意

n n n 个元素,第 i i i 个元素有 a i a_i ai b i b_i bi c i c_i ci 三个属性,设 f ( i ) f(i) f(i) 表示满足 a j ≤ a i a_j \leq a_i ajai b j ≤ b i b_j \leq b_i bjbi c j ≤ c i c_j \leq c_i cjci j ≠ i j \ne i j=i j j j 数量。

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

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

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

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

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

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

  • 每层递归后,我们需要将树状数组清空。但用 m e m s e t memset 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,L] [1,L]
1.设 m i d = ( l + r ) > > 1 mid=(l+r)>>1 mid=(l+r)>>1,递归计算 s o l v e ( l , r ) solve(l,r) solve(l,r)
2.递归计算 s o l v e ( m i d + 1 , r ) solve(mid+1,r) solve(mid+1,r)
3.计算第 l − m i d l-mid lmid 项操作对第 m i d + 1 − r mid+1-r mid+1r 项操作的影响

时间复杂度: O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

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

【练习题】Mokia

题目传送门

题目大意

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

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

解题思路
  • 首先,如二维前缀和一般,对于左下角为 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ,右上角为 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 的询问,我们可以把它转化为四个询问: s u m ( x 1 − 1 , y 1 − 1 ) sum(x_1-1,y_1-1) sum(x11,y11) s u m ( x 1 − 1 , y 2 ) sum(x_1-1,y_2) sum(x11,y2) s u m ( x 2 , y 1 − 1 ) sum(x_2,y_1-1) sum(x2,y11) s u m ( x 2 , y 2 ) sum(x_2,y_2) sum(x2,y2)

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

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

  • 计算左对右的影响时,因为左边的 t t t 始终小于右边的 t t t ,所以问题就变成了一个二维偏序:对 x j ≤ x i x_j\le x_i xjxi y j ≤ y i y_j\le y_i yjyi 的第 j j 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 ) = ∣ A x − B x ∣ + ∣ A y − B y ∣ \operatorname{dist}(A,B)=|A_x-B_x|+|A_y-B_y| dist(A,B)=AxBx+AyBy
  • 在刚开始时,Ayu 已经知道有 n n n 个点可能埋着天使玩偶
  • 再接下来 m m m 行,每行三个非负整数 t , x i , y i t,x_i,y_i t,xi,yi
    • 如果 t = 1 t=1 t=1,则表示 Ayu 又回忆起了一个可能埋着玩偶的点 ( x i , y i ) (x_i,y_i) (xi,yi)
    • 如果 t = 2 t=2 t=2,则表示 Ayu 询问如果她在点 ( x i , y i ) (x_i,y_i) (xi,yi) 那么在已经回忆出来的点里,离她近的那个点有多远
解题思路
  • 首先来看问题的简化版——假设没有 t = 1 t=1 t=1 的操作,这时问题的答案很明显为
    m i n { ∣ x − x i ∣ + ∣ y − y i ∣ } , 1 ≤ i ≤ n min\{|x-x_i|+|y-y_i|\},1\le i\le n min{xxi+yyi}1in

  • 为了去掉绝对值符号,我们不妨把原来的询问分为 4 4 4 个,分别询问在 ( x , y ) (x,y) (x,y) 的左下、左上、右上、右下方向上距离最近的点有多远, 4 4 4 个结果取最小值即为答案。
    以左下方向为例,此时要求的式子变为:
    m i n { ( x − x i ) + ( y − y i ) } , 1 ≤ i ≤ n min\{(x-x_i)+(y-y_i)\},1\le i\le n min{(xxi)+(yyi)}1in
    进一步化简为:
    ( x + y ) − m a x { x i + y i } , 1 ≤ i ≤ n , x i ≤ x , y i ≤ y (x+y)-max\{x_i+y_i\},1\le i\le n,x_i\le x,y_i\le y (x+y)max{xi+yi}1inxixyiy

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

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

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

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

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

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

  • 有一种特殊情况:某一点非常靠近边界,导致某次变换时,没有点在它的左下。这样查询时默认返回了0,最终的距离就成了这个点到原点的距离,但原点是不存在的(经过刚刚的更改,已经没有横坐标或纵坐标为0的点)。为避免这种情况,在树状数组查询时需要特判,若为0则返回 − I N F -INF 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值