【WC2013】糖果公园(详解)

文章讲述了在解决洛谷P4074糖果公园问题时,如何利用欧拉序和进出序获取区间边界,以及通过异或进行区间边界的转移。作者还揭示了在处理树上莫队时,LCA的特殊处理方法,包括贡献的删除和特殊情况的判断。
摘要由CSDN通过智能技术生成

前言

真不知道是哪个傻子调了一个小时代码,试图找到解决区间左边界转移问题的解决方案,冥思苦想却无计可施,最后却发现一定会被重置的lca在某些情况下没有参与统计答案……

前置知识

oiwiki真的好用!(来自luckyxun的感慨)

题目描述

洛谷P4074 [WC2013] 糖果公园,这里就不再赘述。

分析

作为一个刚学完莫队没多久的蒟蒻,看到这会因两个点不同而改变的答案贡献(不易合并),这树上两点的简单路径(易想到欧拉序,也就有了区间),这对点的修改操作,想到带修莫队这不是理所当然嘛!

但是! 第一个难点也来了:光靠欧拉序没法做啊!

一.区间的获取

欧拉序只是在对树进行dfs时,记录了经过的点,光靠它无法进行区间边界的转移。不过,仔细观察欧拉序,你会发现一个事实:通过欧拉序,你可以得到每个点的出入情况。

根据这一点,我们可以很轻易地想到:只要在对树进行dfs时,记录下每个点的进出情况,不就可以进行区间边界的转移了吗!暂且叫这个新的序列为进出序。

相关代码如下:

  • pel[i]表示进出序为i的点为x。
  • per[i]表示点x第一次被遍历时的进出序。
//在树上跑dfs,得到每个点的经过情况 
void dfs(int x,int f){
	pel[++tp]=x;// 入 
	per[x]=tp;
	st[tp][0]=x;
	for(int i=h[x];i;i=p[i].ne){
		int v=p[i].to;
		if(v!=f){
			dfs(v,x);
			pel[++tp]=x;//出 
			st[tp][0]=x;
			pel[++tp]=x;//入 
			st[tp][0]=x;
		}
	}
	pel[++tp]=x;//出 
	st[tp][0]=x;
}

二.区间边界的转移

现在我们已经得到了进出序,那就该想想如何进行莫队中区间边界的转移。

根据进出序中,每个点都有进、出两种状态,且奇数次经过为进、偶数次经过为出,可以轻易地联想到异或,也就联想到了区间边界的转移:

  • 用ins[i]表示在区间(l,r)中,点 i 的状态,1为进入状态,0为未进状态。
  • 在区间边界变化时,根据边界点的状态,进行相应的带修莫队常规转移操作。

相关代码如下:

//删除贡献
void del(int x){
	int ty=c[x];
	sum-=(long long) v[ty]*w[cnt[ty]--];
	ins[x]=0;
} 
//加上贡献
void addd(int x){
	int ty=c[x];
	sum+=(long long) v[ty]*w[++cnt[ty]];
	ins[x]=1;
}
	l=1,r=0,t=0;
	for(int i=1;i<=ta;i++){
		//修改操作 
		while(t<ak[i].t){
			t++;
			int x=cg[t].x;
			if(ins[x]){//需强行修改 
				del(x);
				swap(cg[t].y,c[x]);
				addd(x);
			}
			else{
				swap(cg[t].y,c[x]);
			}
		}
		while(t>ak[i].t){
			int x=cg[t].x;
			if(ins[x]){
				del(x);
				swap(cg[t].y,c[x]);
				addd(x);
			}
			else{
				swap(cg[t].y,c[x]);
			}
			t--;
		}
		//不带修改的莫队操作 
		int x=per[ak[i].x],y=per[ak[i].y];
		if(x>y) swap(x,y);
		while(r>y) cga(pel[r--]);
		while(r<y) cga(pel[++r]);
		while(l>x) cga(pel[--l]);
		while(l<x) cga(pel[l++]);

但是! 只是这样转移是不对的!如题目所给样例:
样例图示
所得到的进出序为:1 3 4 4 3 3 2 2 3 3 3 1 1 1

这时候你就会发现,样例中,询问1和3的答案是正确的,询问2和4却是错的!

可是为什么会错呢?

以第二次询问为例:

xins[x]
10
21
30
40

根据表格和图示,可以很明显地发现问题:三号点和四号点并没有对答案做出贡献!

手动模拟区间边界的转移过程,就可以发现作为lca的三号点被经过了四次,作为左边界的四号点被经过了两次。

这个时候,多造数据模拟一下,观察一下,就可以大胆地猜测一下:

  • 若两点的lca并不是两点中的一个,则lca的贡献一定会被删除。
  • 左边界的点被经过的次数少了一。

这也就是树上莫队不同于普通莫队的地方,而要处理这两个问题,只需要先把左边界的点的经过次数加一,这时就会发现lca一定不会贡献答案,接着再特判lca即可。

记得在统计完答案后,lca的贡献一定要删除!!!

完整代码

#include <bits/stdc++.h>
using namespace std;
/*
	1.树上的点存在两个状态:入和出
	2.需要记录dfs遍历树时,经过的点的顺序(状态不同的相同点当作不同点以同一序号记录)
	3.记录每个点第一次进入时的下标,作为边界进行计算和求lca(类似于欧拉序) 
	4.两点的公共祖先会被二次经过,故需特判加上,答案统计完后再删除 
	5.按照经过点的顺序的统计数分块后,跑莫队,之前 ins[x]状态为0则加上数据,否则减去
	6.在跑莫队时,左边界往右扩展时,需注意是<=而非<,因为左边界的那个点也得参加统计 
*/ 
const int N=1e6+5; 
int n,m,q,cnt[N],l,r,pel[N<<3],per[N],tp,c[N],h[N],tot,blocksize,x,y,ty,tq,ta,t,st[N<<3][35],lg[N<<3];
long long v[N],w[N],sum,ans[N];
bool ins[N];
struct P{
	int ne,to;
}p[N<<1];
struct Q{
	int x,y,t,id;
}ak[N],cg[N];
//加边 
void add(int f,int to){
	p[++tot].ne=h[f];
	p[tot].to=to;
	h[f]=tot;
}
//在树上跑dfs,得到每个点的经过情况 
void dfs(int x,int f){
	pel[++tp]=x;// 入 
	per[x]=tp;
	st[tp][0]=x;
	for(int i=h[x];i;i=p[i].ne){
		int v=p[i].to;
		if(v!=f){
			dfs(v,x);
			pel[++tp]=x;//出 
			st[tp][0]=x;
			pel[++tp]=x;//入 
			st[tp][0]=x;
		}
	}
	pel[++tp]=x;//出 
	st[tp][0]=x;
}
//分块 
int get(int x){
	return (x-1)/blocksize+1;
}
//询问排序 
bool cmp(Q a,Q b){
	int xa=get(per[a.x]),xb=get(per[b.x]);
	int ya=get(per[a.y]),yb=get(per[b.y]);
	return xa==xb?(ya==yb?a.t<b.t:ya<yb):xa<xb;
}
//加上贡献
void addd(int x){
	int ty=c[x];
	sum+=(long long) v[ty]*w[++cnt[ty]];
	ins[x]=1;
}
//删除贡献
void del(int x){
	int ty=c[x];
	sum-=(long long) v[ty]*w[cnt[ty]--];
	ins[x]=0;
} 
//更新答案 
void cga(int x){
	if(ins[x]){//已经计算过了,就删除 
		del(x);
	}
	else{//未计算过,则加入
		addd(x);
	}
} 
//用于st表中的比较 
int minn(int x,int y){
	return per[x]<per[y]?x:y;
}
//求两个点的lca
int fd(int x,int y){
	int k=lg[x-y+1];
	return minn(st[y][k],st[x-(1<<k)+1][k]);
} 
int main(){
	cin>>n>>m>>q;
	for(int i=1;i<=m;i++){
		scanf("%lld",&v[i]);
	}
	for(int j=1;j<=n;j++){
		scanf("%lld",&w[j]);
	}
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	for(int i=1;i<=n;i++){
		scanf("%d",&c[i]);
	}
	dfs(1,0);//对树进行dfs 
	//预处理出log2的值
	lg[1]=0;//
	for(int i=2;i<=tp;i++){
		lg[i]=lg[i>>1]+1;
	} 
	//求st表 
	for(int j=1;j<=25;j++){
		for(int i=1;i+(1<<j)<=tp;i++){
			st[i][j]=minn(st[i][j-1],st[i+(1<<(j-1))][j-1]);
		}
	}
	blocksize=pow(tp,2.0/3);//分块大小 
	for(int i=1;i<=q;i++){
		scanf("%d%d%d",&ty,&x,&y);
		if(ty){
			ta++;
			ak[ta].id=ta;
			ak[ta].t=tq;
			ak[ta].x=x;
			ak[ta].y=y;
		}
		else{
			cg[++tq].x=x;
			cg[tq].y=y;
		}
	}
	sort(ak+1,ak+1+ta,cmp);
	l=1,r=0,t=0;
	for(int i=1;i<=ta;i++){
		//修改操作 
		while(t<ak[i].t){
			t++;
			int x=cg[t].x;
			if(ins[x]){//需强行修改 
				del(x);
				swap(cg[t].y,c[x]);
				addd(x);
			}
			else{
				swap(cg[t].y,c[x]);
			}
		}
		while(t>ak[i].t){
			int x=cg[t].x;
			if(ins[x]){
				del(x);
				swap(cg[t].y,c[x]);
				addd(x);
			}
			else{
				swap(cg[t].y,c[x]);
			}
			t--;
		}
		//不带修改的莫队操作 
		int x=per[ak[i].x],y=per[ak[i].y];
		if(x>y) swap(x,y);
		while(r>y) cga(pel[r--]);
		while(r<y) cga(pel[++r]);
		while(l>x) cga(pel[--l]);
		while(l<=x) cga(pel[l++]);//重点!!!是<=不是<!!! 
		//求lca并特判 
		int lca=fd(y,x);
		addd(lca);
		ans[ak[i].id]=sum;//统计答案 
		del(lca);
	}
	for(int i=1;i<=ta;i++){
		printf("%lld\n",ans[i]);//输出 
	}
	return 0;
}

题外话

第一次写博客,表达可能不是很好,只能尽力把我做题时的想法和踩的坑写出来了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值