Luogu P1600 天天爱跑步

题面不好化简。

规定

%   为了方便,先钦定一个点为根节点,这里用1号点,并且规定一些符号。
   W u : W_u: Wu: u u u 的观测时间。
   l c a ( A , B ) : lca(A,B): lca(A,B): A A A 和点 B B B 的最近公共祖先。
   A → B : A\rightarrow B: AB: A A A 到点 B B B 的(树上唯一)路径。
   F u : F_u: Fu: u u u 的父亲。
   d e p u : dep_u: depu: u u u 的深度。
   a n s u : ans_u: ansu: u u u 最终的答案。
   d i s ( A , B ) : dis(A,B): dis(A,B): A → B A\rightarrow B AB 的路径长度。
  链:深度严格递增或递减的路径。

题解

%   可以发现,对于一个点,如果要考虑所有边对其贡献会非常困难。因此我们开始考虑对于每条边,求出对路径上的点的贡献。我们首先把一条 S → T S\rightarrow T ST 的路径拆开,拆成 S → l c a ( S , T ) → T S\rightarrow lca(S,T)\rightarrow T Slca(S,T)T ,进一步拆开变成 S → l c a ( S , T ) S\rightarrow lca(S,T) Slca(S,T) l c a ( S , T ) → T lca(S,T)\rightarrow T lca(S,T)T,然后分别考虑这两部分对答案的贡献,最后减去计算重复的 l c a ( S , T ) lca(S,T) lca(S,T)

向上的链

%   先来考虑 S → l c a ( S , T ) S\rightarrow lca(S,T) Slca(S,T),根据题意,点 u u u 能观测到这条路径当且仅当点 u u u S → l c a ( S , T ) S\rightarrow lca(S,T) Slca(S,T) 上,且 W u = d e p S − d e p u W_u=dep_S-dep_u Wu=depSdepu,即当花费 W u W_u Wu 的时间后恰好到达点 u u u,移项,得 d e p S = d e p u + W u dep_S=dep_u+W_u depS=depu+Wu

%   换言之,对于点 u u u,所有对 a n s u ans_u ansu 产生贡献的链 S → l c a ( S , T ) S\rightarrow lca(S,T) Slca(S,T) 必须同时满足以下三点。

  1. d e p S = d e p u + W u dep_S=dep_u+W_u depS=depu+Wu
  2. S S S 属于以 u u u 为根的子树
  3. S → l c a ( S , T ) S\rightarrow lca(S,T) Slca(S,T) 路径对应的 d e p l c a ( S , T ) < d e p u dep_{lca(S,T)}<dep_u deplca(S,T)<depu,即路径还没有结束。

%   因此,我们只需要统计以 u u u 为根的子树中,有多少个点的 d e p dep dep 值等于 d e p u + W u dep_u+W_u depu+Wu
  怎么求?我们考虑在深度优先搜索的时候,记录下 U p [ i ] Up[i] Up[i],表示已经遍历过的点中,有多少条没有结束的路径,满足路径开端的 d e p dep dep 值等于 i i i。那么如果我们要求子树内有多少个点满足 d e p dep dep 值等于 i i i,只需要计算一下遍历子树前遍历子树后 U p [ d e p u + W u ] Up[dep_u+W_u] Up[depu+Wu]差值即可。
  如此,在遇到一条路径的开端时,将其按照 d e p dep dep 值加入 U p Up Up 数组中,在遇到路径结束时,在 U p Up Up 数组中删除这条路径开端 d e p dep dep 值(注意不是删除路径末端的 d e p dep dep 值)。

//求向上的链的答案 
int Up[maxn],ans[maxn];
void dfst(int u){
	int c=Up[dep[u]+w[u]];//记录初始值 
	
	for(int i=head[u];i;i=edges[i].next){
		int v=edges[i].v;
		//Fa[u][k]表示点u的第2^j个父亲(倍增求lca用)
		if(v==Fa[u][0]) continue;
		dfst(v);
	}
	
	ans[u]+=Up[dep[u]+w[u]]-c;//增量为子树中满足dep值等于dep[u]+w[u]的数量
	
	//更新Up数组
	for(int i=qs[u];i;i=edges[i].next){//将开端和结束信息拉链成一个邻接表
		//此处edges[i].v==1表示这是一条路径开端
		//否则说明是一条路径的末尾
		//edges[i].qidian表示路径开端的点的编号
		if(edges[i].v==1) Up[dep[u]]++;//加上点u的贡献 
		else Up[dep[edges[i].qidian]]--;//删除当初的点u的贡献 
	}
}
向下的链

%   然后我们来考虑向下的链 l c a ( S , T ) → T lca(S,T)\rightarrow T lca(S,T)T,根据题意,若点 u u u 能观测到这条路径当且仅当点 u u u l c a ( S , T ) → T lca(S,T)\rightarrow T lca(S,T)T 上,且 W u = d i s ( S , T ) − ( d e p T − d e p u ) W_u=dis(S,T)-(dep_T-dep_u) Wu=dis(S,T)(depTdepu),化简得 d e p u − W u = d e p T − d i s ( S , T ) dep_u-W_u=dep_T-dis(S,T) depuWu=depTdis(S,T)

%   因此,类似地,对于点 u u u,所有对 a n s u ans_u ansu 产生贡献的链 l c a ( S , T ) → T lca(S,T)\rightarrow T lca(S,T)T 必须同时满足以下三点。

  1. d e p u − W u = d e p T − d i s ( S , T ) dep_u-W_u=dep_T-dis(S,T) depuWu=depTdis(S,T)
  2. T T T 属于以 u u u 为根的子树
  3. d e p l c a ( S , T ) < d e p u dep_{lca(S,T)}<dep_u deplca(S,T)<depu,即 l c a ( S , T ) → T lca(S,T)\rightarrow T lca(S,T)T 已经开始。

%   类似地深度优先搜索的时,记录下 D n [ i ] Dn[i] Dn[i],表示已经遍历过的点中,有多少条已经开始的路径,满足路径末端的 d e p dep dep 值减去路径长度等于 i i i。求子树信息仍然计算差值即可。
   D n Dn Dn 数组的更新也类似。唯一需要注意的是 d e p u − W u dep_u-W_u depuWu 的值可能为负,因此需要对数组偏移一下,下面的代码没有采用访问时偏移的方法,而是采用了指针整体偏移的方式。

int _dn[maxn<<1];
int *Dn=&_dn[maxn];//指针放在中间位置
void dfed(int u){
	int c=Dn[dep[u]-w[u]];//记录初始值 
	
	for(int i=head[u];i;i=edges[i].next){
		int v=edges[i].v;
		if(v==Fa[u][0]) continue;
		dfed(v);
	}
	
	ans[u]+=Dn[dep[u]-w[u]]-c;//增量为子树中满足要求的总数 
	
	for(int i=qs[u];i;i=edges[i].next){
		if(edges[i].v==1) Dn[dep[u]-edges[i].w]++;//是路径开端则加上点u的贡献 
		else  Dn[dep[edges[i].qidian]-edges[i].w]--;//删除当初的点u的贡献 
	}
}
重复部分

%   上面考虑了 S → l c a ( S , T ) S\rightarrow lca(S,T) Slca(S,T) l c a ( S , T ) → T lca(S,T)\rightarrow T lca(S,T)T 两条链,因此 l c a ( S , T ) lca(S,T) lca(S,T) 被考虑了两次,因而需要减去一次。
  路径 S i → T j S_i\rightarrow T_j SiTj 能对 a n s l c a ( S , T ) ans_{lca(S,T)} anslca(S,T) 产生贡献当且仅当 W l c a ( S , T ) = d e p S − d e p l c a ( S , T ) W_{lca(S,T)}=dep_{S}-dep_{lca(S,T)} Wlca(S,T)=depSdeplca(S,T)  减去即可。

//LCA[i]=lca(S[i],T[i])
for(int i=1;i<=m;i++) if(dep[S[i]]-w[LCA[i]]==dep[LCA[i]]) --ans[LCA[i]];

完整代码

#include<bits/stdc++.h>
using namespace std;
#define maxn 1000010
struct edge{
	int v,next,w,qidian;
	//在图head意义下,v:端点	w,qidian: 无意义
	
	//在询问qs意义下,v:链的开始为1,链的结束为-1
	//w:点对距离 	qidian:询问的另一个端点 
}edges[maxn<<1];
int n,head[maxn],cnt=0;
//加入图中的边 
void ins(int u,int v){
	edges[++cnt]=(edge){v,head[u]};
	head[u]=cnt;
}
//加入点对信息 
int qs[maxn];
//op:链的开始(1)或者结束(-1)
void qns(int u,int op,int w=0,int qidian=0){
	edges[++cnt]=(edge){op,qs[u],w,qidian};
	qs[u]=cnt;
}

//求出深度,父亲 
int w[maxn],dep[maxn],Fa[maxn][20];
void predfs(int u,int fa=0){
	dep[u]=dep[fa]+1; Fa[u][0]=fa;
	for(int i=head[u];i;i=edges[i].next){
		int v=edges[i].v;
		if(v==fa) continue;
		predfs(v,u);
	}
}

//倍增求最近公共祖先 
int lca(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=19;i>=0&&dep[x]!=dep[y];i--)
		if(dep[Fa[x][i]]>=dep[y]) x=Fa[x][i];
	if(x==y) return x;
	for(int i=19;i>=0;i--)
		if(Fa[x][i]!=Fa[y][i]){
			x=Fa[x][i];
			y=Fa[y][i];
		}
	return Fa[x][0];
}

//求距离 
int dis(int u,int v){
	int lcas=lca(u,v);
	return dep[u]+dep[v]-dep[lcas]*2;
}

//求向上的链的答案 
int Up[maxn],ans[maxn];
void dfst(int u){
	int c=Up[dep[u]+w[u]];//记录初始值 
	
	for(int i=head[u];i;i=edges[i].next){
		int v=edges[i].v;
		if(v==Fa[u][0]) continue;
		dfst(v);
	}
	
	ans[u]+=Up[dep[u]+w[u]]-c;//增量为子树中满足要求的总数
	
	for(int i=qs[u];i;i=edges[i].next){
		if(edges[i].v==1) Up[dep[u]]++;//加上点u的贡献 
		else Up[dep[edges[i].qidian]]--;//删除当初的点u的贡献 
	}
}

//求向下的链的答案 
int _dn[maxn<<1];
int *Dn;
void dfed(int u){
	int c=Dn[dep[u]-w[u]];//记录初始值 
	
	for(int i=head[u];i;i=edges[i].next){
		int v=edges[i].v;
		if(v==Fa[u][0]) continue;
		dfed(v);
	}
	
	ans[u]+=Dn[dep[u]-w[u]]-c;//增量为子树中满足要求的总数 
	
	for(int i=qs[u];i;i=edges[i].next){
		if(edges[i].v==1) Dn[dep[u]-edges[i].w]++;//加上点u的贡献 
		else  Dn[dep[edges[i].qidian]-edges[i].w]--;//删除当初的点u的贡献 
	}
}

int S[maxn],T[maxn],LCA[maxn];
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1,a,b;i<n;i++){
		scanf("%d%d",&a,&b);
		ins(a,b);ins(b,a);
	}
	for(int i=1;i<=n;i++)
		scanf("%d",&w[i]);
	
	//预处理信息 
	predfs(1);
	for(int j=1;j<=19;j++)
		for(int i=1;i<=n;i++)
			Fa[i][j]=Fa[Fa[i][j-1]][j-1];

	//处理向上的链 
	for(int i=1;i<=m;i++){
		scanf("%d%d",&S[i],&T[i]);
		LCA[i]=lca(S[i],T[i]);//求lca 
		ans[S[i]]+=(w[S[i]]==0);//当链长度为0时特判
		qns(S[i],1);//加入链信息 
		qns(LCA[i],-1,0,S[i]);//在lca处删除链信息 
	}dfst(1);
	
	//重新处理向下的链 
	memset(qs,0,sizeof qs);
	for(int i=1;i<=m;i++){
		int dist=dis(S[i],T[i]);
		ans[T[i]]+=(w[T[i]]==dist);//当链长度为0时特判
		qns(T[i],1,dist);//加入链信息 
		qns(LCA[i],-1,dist,T[i]);//在lca处删除链信息
	}
	
	Dn=&_dn[maxn];//为了适应负数的情况 
	dfed(1);
	
	//去除lca被重复计算的部分 
	for(int i=1;i<=m;i++)
    	if(dep[S[i]]-w[LCA[i]]==dep[LCA[i]])
			--ans[LCA[i]];
	
	for(int i=1;i<=n;i++)
		printf("%d ",ans[i]);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值