P1600 天天爱跑步(树上差分,桶思想,最近公共祖先)

2 篇文章 0 订阅
1 篇文章 0 订阅

P1600 天天爱跑步

题目描述

给定一棵树和一些路径,每个点有一个点权,求每个点点权恰好等于路径上的访问排位的数量.

举个例子: s − > t s->t s>t经过了点 k k k,点 k k k的权值为 w w w且点 k k k恰好是这条路径上第 w + 1 w+1 w+1个点(路径上第一个点 s s s的访问排位为0),则 a n s [ k ] + + ans[k]++ ans[k]++.

题目分析

25pts:

随便乱搞都可以. 我采用的方法是在线处理每条路径,用树剖的方法把路径剖下来,然后再与标准比较,如果 w w w和次序相同那么这个点的 a n s + + ans++ ans++,时间复杂度 O ( n m l o g 2 n ) O(nmlog^2n) O(nmlog2n). 能过 25 % 25\% 25%的数据.

评测记录

100pts:

我们需要转换思路. 每次的一条链不能对每一个元素都进行操作,否则时间快不上来.

那我们就想到了一种更加简单的对树上区间进行操作的方法

树上差分:

关于差分和树上差分的浅谈

差分通常是和前缀和结合起来的,比如说以上博文的例子,如果覆盖了,那么在区间前端加一,在区间末端减一.

这样,我们就相当于给区间打了标记,不用每次给出一段区间时都处理,而是最后再使用一个差分数组,统一处理标记,复杂度自然就下来了.

树上差分也是一样. 通过打标记的方式,来把对区间处理的复杂度由 O ( n ) O(n) O(n)降为 O ( 1 ) O(1) O(1).

比如说,我们有这样一条路径

在这里插入图片描述

对于这条路径,我们这样打上标记

在这里插入图片描述

我们在计算一个点被多少条路径经过时,只要统计在它的子树中的标记的和就可以了.

值得注意的是,两个-1的标记,一个打在 l c a lca lca上,另一个打在 f a [ l c a ] fa[lca] fa[lca]上,这样可以保证 l c a lca lca不会因为是两段的交集而重复计算路径数.

这样子,我们把原来每条路径上的点处理一次,总的处理复杂度为 O ( n 2 ) O(n^2) O(n2),变成了每条路径进行 O ( 1 ) O(1) O(1)处理,最后再用一次 d f s dfs dfs O ( n ) O(n) O(n)算出每个点的覆盖次数,从而大大降低了时间复杂度.

不难发现,我们同样可以把以上差分的处理方法运用到这道题上.


桶思想:

简而言之,桶就是不论里面的东西是什么,而只看里面的东西有多少.

关于桶这一思想,可以参见另外一篇博客:(留坑待填)

我们首先要对这一题的题目条件进行转换.

不难发现,对于任意一个在某一条路径上的点 x x x,如果这条路径对这个点的答案有贡献,那么必定满足以下条件之一:

1.如果这个点在 s − > l c a s->lca s>lca上,那么 d e p [ x ] − d e p [ u ] = w [ x ] dep[x]-dep[u]=w[x] dep[x]dep[u]=w[x]
2.如果这个点在 l c a − > t lca->t lca>t上,那么 d e p [ u ] − d e p [ l c a ] + d e p [ x ] − d e p [ l c a ] = w [ x ] dep[u]-dep[lca]+dep[x]-dep[lca]=w[x] dep[u]dep[lca]+dep[x]dep[lca]=w[x].

由以上两个条件,可以发现:

对于式1, d e p [ u ] = w [ x ] + d e p [ x ] dep[u]=w[x]+dep[x] dep[u]=w[x]+dep[x],等号右边是定值,由上面的差分思路可知,我们只要找到所有在 x x x的子树中且深度为 d e p [ u ] dep[u] dep[u]的点的数量即可.
对于式2, d e p [ u ] − 2 ∗ d e p [ l c a ] = w [ x ] − d e p [ x ] dep[u]-2*dep[lca]=w[x]-dep[x] dep[u]2dep[lca]=w[x]dep[x],等号右边是定值,这里不方便找点,我们改为找一个桶的大小,只需要找到在子树中值为 d e p [ u ] − 2 ∗ d e p [ l c a ] dep[u]-2*dep[lca] dep[u]2dep[lca]桶的大小即可.

那么思路就很明显了,我们开两组桶,每个桶里面储存的是恰好满足条件的路径的数量,比如说 b u c k e t 1 [ d e p ] bucket1[dep] bucket1[dep]就表示了起始点深度为 d e p dep dep的路径的多少, b u c k e t 2 [ d e p ] bucket2[dep] bucket2[dep]表示了满足 d e p [ u ] − 2 ∗ d e p [ l c a ] dep[u]-2*dep[lca] dep[u]2dep[lca]的路径的数量多少.

为什么要开两个桶数组呢?因为对于每个点 x x x,我们要查询的桶都有两个, w [ x ] + d e p [ x ] w[x]+dep[x] w[x]+dep[x] w [ x ] − d e p [ x ] w[x]-dep[x] w[x]dep[x].

此外,第二个数组可能发生 d e p [ u ] − 2 ∗ d e p [ l c a ] < 0 dep[u]-2*dep[lca]<0 dep[u]2dep[lca]<0的情况,为了防止这种情况出现,我们还需要给第二个桶的容量统一加上 m a x n maxn maxn.


到这里这道题就基本做完了. 倍增求lca是树上差分的套路就不讲了 但是还需要考虑一些其他的东西:

1.当一个点同时满足以上两个式子的时候, a n s ans ans会重复计算,多算一次路径的贡献. (这里和普通树上差分不太一样)

上面的两个式子
d e p [ u ] − 2 d e p [ l c a ] = w [ x ] − d e p [ x ] dep[u]-2dep[lca]=w[x]-dep[x] dep[u]2dep[lca]=w[x]dep[x]
d e p [ u ] = w [ x ] + d e p [ x ] dep[u]=w[x]+dep[x] dep[u]=w[x]+dep[x]
因为 d e p [ u ] dep[u] dep[u]相同,所以联立得
w [ x ] − d e p [ x ] + 2 d e p [ l c a ] = w [ x ] + d e p [ x ] w[x]-dep[x]+2dep[lca]=w[x]+dep[x] w[x]dep[x]+2dep[lca]=w[x]+dep[x]
d e p [ x ] = d e p [ l c a ] dep[x]=dep[lca] dep[x]=dep[lca],也就是说,如果 l c a lca lca满足其中一个式子,它必定满足另一个,所以如果 l c a lca lca满足一个式子, a n s [ l c a ] ans[lca] ans[lca]
要-1,以抵消多算一次的影响.

2.一个点的 a n s ans ans应该取未访问它的子树之前和访问它的子树之后的差值,以防止非同一子树的桶的元素造成的影响.

3.桶的大小要开够.

参考资料

程序实现

#include<bits/stdc++.h>
#define maxn 300010
using namespace std;
struct edge{
	int v,next;
}e[maxn<<1];
int head[maxn],tot;
void add(int u,int v){
	e[++tot].v =v;
	e[tot].next =head[u];
	head[u]=tot;
}
int dep[maxn],fa[maxn][30];
void dfs1(int u,int pre){
	dep[u]=dep[pre]+1;
	fa[u][0]=pre;
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs1(v,u);
	}
}
int LCA(int u,int v){
	if(dep[u]<dep[v])swap(u,v);
	for(int i=20;i>=0;i--){
		if(dep[fa[u][i]]>=dep[v])u=fa[u][i];
	}
	if(u==v)return u;
	for(int i=20;i>=0;i--){
		if(fa[u][i]==fa[v][i])continue;
		u=fa[u][i],v=fa[v][i];
	}
	return fa[u][0];
}
int ans[maxn],wt[maxn];
map<int ,int >add_start[maxn],add_to[maxn],lca_start[maxn],lca_to[maxn];
int bucket1[maxn<<1],bucket2[maxn<<1];
int sums[maxn],sumt[maxn],minus1[maxn],minus2[maxn];
void dfs2(int u,int pre){
	int t1=bucket1[wt[u]+dep[u]],t2=bucket2[wt[u]-dep[u]+maxn];
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs2(v,u);
	}
	for(int i=1;i<=sums[u];i++)bucket1[add_start[u][i]]++;//差分为了保证准确性,通常都是先加后减
	for(int i=1;i<=sumt[u];i++)bucket2[add_to[u][i]]++;//相应的桶++
	ans[u]+=bucket1[wt[u]+dep[u]]-t1+bucket2[wt[u]-dep[u]+maxn]-t2;//计算增量
	for(int i=1;i<=minus1[u];i++)bucket1[lca_start[u][i]]--;
	for(int i=1;i<=minus2[u];i++)bucket2[lca_to[u][i]]--;//相应的桶--
}
int n,m;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	for(int i=1;i<=n;i++)scanf("%d",&wt[i]);
	dfs1(1,1);
	for(int j=1;j<=20;j++)
		for(int i=1;i<=n;i++)
		fa[i][j]=fa[fa[i][j-1]][j-1];
	for(int i=1,s,t,lca;i<=m;i++){
		scanf("%d%d",&s,&t);
		lca=LCA(s,t);
		add_start[s][++sums[s]]=dep[s];//用map存:在这个点的位置桶dep[s]++
		add_to[t][++sumt[t]]=dep[s]-2*dep[lca]+maxn;//同上
		lca_start[lca][++minus1[lca]]=dep[s];//这个桶在lca的位置--
		lca_to[lca][++minus2[lca]]=dep[s]-2*dep[lca]+maxn;
		if(dep[lca]+wt[lca]==dep[s])ans[lca]--;//如果一个点是lca又满足条件,则要防止重复计算路径
	}
	dfs2(1,1);
	for(int i=1;i<=n;i++)printf("%d ",ans[i]);
	return 0; 
} 

题后总结

1.桶是非常重要的一种思想,桶可以用来存满足某个条件的数的数量,比较方便.

但是使用桶,就要求数据范围不是很大,否则占用空间就太多了.

对于全局桶的应用:记录它对某次计算的贡献可以统计他的增量.

2.对于树上的区间操作:

区间是否便于用线段树维护?是则使用树链剖分,否则使用差分或者差分思想.

使用差分思想:题意给出的(可能隐藏的)式子能否转换成便于差分维护/便于用差分思想维护(这种情况下通常是桶)的形式?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值