P1600 天天爱跑步[桶+LCA+树上差分]

题目描述

小c同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。

这个游戏的地图可以看作一一棵包含 nn个结点和 n-1n−1条边的树, 每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从11到nn的连续正整数。

现在有mm个玩家,第ii个玩家的起点为 S_iS**i,终点为 T_iT**i 。每天打卡任务开始时,所有玩家在第00秒同时从自己的起点出发, 以每秒跑一条边的速度, 不间断地沿着最短路径向着自己的终点跑去, 跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树, 所以每个人的路径是唯一的)

小c想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点jj的观察员会选择在第W_jW**j秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第W_jW**j秒也理到达了结点 jj 。 小C想知道每个观察员会观察到多少人?

注意: 我们认为一个玩家到达自己的终点后该玩家就会结束游戏, 他不能等待一 段时间后再被观察员观察到。 即对于把结点jj作为终点的玩家: 若他在第W_jW**j秒前到达终点,则在结点jj的观察员不能观察到该玩家;若他正好在第W_jW**j秒到达终点,则在结点jj的观察员可以观察到这个玩家。

解析

可能是目前为止我做过的最难的题了。。。(话说这题为什么不是黑的)

再吐槽一句,考场上谁想得出这种神仙算法啊?!勉强拿个80暴力分都不容易啊!


在讲这题前,首先要引入一个船新的概念:全局桶

还有一些老东西:树上差分,LCA。

【全局桶】

全局桶,顾名思义,也是种桶,但是其维护的不是对象对应的下标,而是类似用桶来装权值(怎么说着那么奇怪)。精确的说,它维护的是对象产生的值出现的次数,而不是对象出现的次数。有点说不清楚,解释一下,比如这题,我们的全局桶将要维护的是每一个玩家的起点、终点对其跑步的路径所造成的影响,这个影响也就是起点、终点、路径上的观察员共同产生的一个值,下面会详细讲。我们会看到,它不维护树上某一个点的权值,而是作为总体的计数数组而存在。

【LCA】

即最近公共祖先,不必多言,看名字就知道啥意思。

【树上差分】

为了快速计算子树和,或维护子树的一些满足区间可减性(即满足该信息的前缀和可以相减,其可作为前缀和的逆运算)的信息时,树上差分可以很好胜任。类比序列上的差分,假设我们有一棵树\(T\),要在一条\(s\sim t\)的路径上对每个点的权值加\(k\),我们可以对差分数组执行以下操作:在\(s\)\(k\),在\(t\)\(k\),在\(lca(s,t)\)\(k\),在\(father(lca(s,t))\)\(k\),那么以\(lca(s,t)\)为根的子树前缀和(从叶子节点向根节点做前缀和,根节点算在内)就是原来的子树和。(树剖或LCT可取而代之)


解释完概念之后,我们来着手这道题的解决。

分析题目,容易得出每个玩家其实就是对\(s\sim t\)的路径上的点依次增加了\(1\sim len(s,t)\)的权值。故有暴力算法,对每个运动员,我们处理出他跑的路径对树的贡献,最后暴力dfs整棵树统计答案,复杂度\(O(nm)\)。优化的好的话就能得到80分了。。。

似乎和树上差分没什么关系?

我们不妨转化一下思维,逆向看问题。

对于树上每个节点,它都有可能作为某一对起点和终点的LCA,假设某个在\(s\sim t\)上的观察员为\(x\)。而\(x\)对最终答案产生贡献仅当\(deep[x]+w[x]=deep[s]\)\(deep[x]+deep[s]-deep[lca(s,t)]*2=w[x]\)时。这是比较显而易见的表达方式,然后我们将其移项,使得其呈现规律性:\(deep[s]=deep[x]+w[x]\)\(deep[s]-deep[lca(s,t)]*2=w[x]-deep[x]\)。好的!现在关于\(x\)的式子都在等号左边了。这就方便了我们来维护这些等式,但这也是难点所在。

根据以上分析,我们就得分\(s\sim lca(s,t)\)\(lca(s,t) \sim t\)两条路径讨论了。


我们先考察稍微简单一点的\(s\sim lca(s,t)\)这条路径。

不难想到,对于一个玩家,它的起点\(s\)会给\(s\sim lca(s,t)\)上的所有点\(x\)造成\(deep[s]\)的贡献,只有当\(x\)的贡献\(deep[x]+w[x]\)与其相等时,\(x\)才能观察到该玩家,该点答案\(+1\)。换句话说,就是对该路径上所有点加上\(deep[x]\)。你想到了什么?没错,树上差分,这是很自然的一个推导过程。假设这个差分数组为数组\(c\),此时我们对\(c[s]\)\(deep[s]\),对\(c[lca(s,t)]\)\(deep[x]\)

对于\(s\sim lca(s,t)\)这条路径呢,我们就给\(c[lca(s,t)]\)减去\(deep[s]-deep[lca(s,t)]*2\),给\(c[t]\)加上\(deep[s]-deep[lca(s,t)]*2\)。等等,LCA怎么算重了???没事,你只减一条路上LCA就行,你会发现在LCA这里,两个等式是等价的。复杂度因人而异,但肯定不会超过\(O(nm)\)啦。

这样做,你需要线段树合并,或者别的数据结构来维护这个差分数组。你愉快的发现,你T了。


我们还没拿出来全局桶呢,不得不说,这个办法太秀了,也比较抽象。我们再逆向一下思维,考虑所有点作为LCA时的情况

说白了其实就是上面那种方法逆过来搞,可以实现一种先处理所有玩家最后统计答案的算法,复杂度可以达到\(O((n+m)log(n+m))\)

这个全局桶,它的好处就是去掉了许多无用的条件,只保留最少的我们所需要的条件。

建立全局桶\(bucket\),对每种类型的贡献(比如\(deep[x]+w[x]\))进行计数。

对树上每一个节点开4个多重集(比如vector),将所有经过该节点的玩家的起点放进一个集合,终点放进一个集合,将该节点作为一条路径的终点放进一个集合,将该节点作为一条路径的起点放进一个集合。首先起点和终点直接的路径时一定的,且一定会经过它们的LCA。那么对于某个节点\(x\),第一个集合也就对应着所有经过\(x\)的路径,或者说以\(x\)为LCA的起点的贡献,第二个集合对应着所有以\(x\)为LCA的终点的贡献。

最后,dfs整颗树,计算树上前缀和。具体来说,对于\(s\sim lca(s,t)\),就是递归进入在这条路径上的点\(x\)时,对于第一个集合中的点集\(i_1\),给\(bucket[deep[i_1]]-1\),对于第二个集合,也给\(bucket[deep[i_2]]-1\),对于第三个集合,给\(bucket[deep[i_3]]+1\),对第四个集合,给\(bucket[deep[i_4]]+1\)。等我们又回溯到\(x\)时,它的子树已经对桶完成更新,意即所有能被\(x\)观察到的玩家都被算进了\(bucket[deep[x]+w[x]]\),因此此时\(bucket[deep[x]+w[x]]\)前后的差值就是\(x\)点对答案的贡献。

发现又加重了,我们最后减掉就行。

注意,对于\(lca(s,t)\sim t\)这条路径,统计的时候桶下标会溢出,我们要离散化或者平移坐标处理。

回顾树上差分方法,你会发现二者其实在做同一件事情,而后者真是惊艳到我了。

这里给出一种全局桶的做法,简化了第四个集合(其实是“借鉴”题解的):

参考代码

#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<ctime>
#include<cstdlib>
#include<algorithm>
#include<queue>
#include<set>
#include<map>
#define N 600010
#define M 300010
using namespace std;
inline int read()
{
	int f=1,x=0;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
	return x*f;
}
int f[31][N],n,m,a[N+M],c[N],dep[N],t,sum[N],ans[N];
vector<int> v1[M],v2[M],v3[M];
struct rec{
	int next,ver;
}g[N];
int head[N],tot;
struct node{
	int s,t,dis,lca;
}s[M];
inline void add(int x,int y)
{
	g[++tot].ver=y;
	g[tot].next=head[x],head[x]=tot;
}
inline void init()
{
	queue<int> q;
	q.push(1);dep[1]=1;
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=head[x];i;i=g[i].next){
			int y=g[i].ver;
			if(dep[y]) continue;
			dep[y]=dep[x]+1;
			f[0][y]=x;
			for(int j=1;j<=t;++j)
				f[j][y]=f[j-1][f[j-1][y]];
			q.push(y);
		}	
	}	
}
inline int lca(int x,int y)
{
	if(dep[y]>dep[x]) swap(x,y);
	for(int j=t;j>=0;--j)
		if(dep[y]<=dep[f[j][x]]) x=f[j][x];
	if(x==y) return x;
	for(int j=t;j>=0;--j)
		if(f[j][x]!=f[j][y]) x=f[j][x],y=f[j][y];
	return f[0][x];
}
inline void dfs1(int x,int fa)
{
	int cnt=c[dep[x]+a[x]+M];
	for(int i=head[x];i;i=g[i].next){
		int y=g[i].ver;
		if(y==fa) continue;
		dfs1(y,x);
	}
	c[dep[x]+M]+=sum[x];
	ans[x]+=c[dep[x]+a[x]+M]-cnt;
	for(int i=0;i<v1[x].size();++i)
		c[v1[x][i]+M]--;
}
inline void dfs2(int x,int fa)
{
	int cnt=c[a[x]-dep[x]+M];
	for(int i=head[x];i;i=g[i].next){
		int y=g[i].ver;
		if(y==fa) continue;
		dfs2(y,x);
	}	
	for(int i=0;i<v3[x].size();++i)
		c[v3[x][i]+M]++;
	ans[x]+=c[a[x]-dep[x]+M]-cnt;
	for(int i=0;i<v2[x].size();++i)
		c[v2[x][i]+M]--;
}
int main()
{
	n=read(),m=read();
	t=log2(n)+1;
	for(int i=1;i<n;++i){
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	init();
	for(int i=1;i<=n;++i) a[i]=read();
	for(int i=1;i<=m;++i){
		int f=read(),t=read();
		s[i].s=f,s[i].t=t;
		s[i].lca=lca(f,t);
		s[i].dis=dep[f]+dep[t]-dep[s[i].lca]*2;
		sum[f]++;
		v1[s[i].lca].push_back(dep[f]);
		v2[s[i].lca].push_back(dep[f]-dep[s[i].lca]*2);
		v3[t].push_back(dep[f]-dep[s[i].lca]*2);
	}
	dfs1(1,-1);
	dfs2(1,-1);
	for(int i=1;i<=m;++i)
		if(dep[s[i].s]==dep[s[i].lca]+a[s[i].lca]) ans[s[i].lca]--;
	for(int i=1;i<=n;++i)
		printf("%d ",ans[i]);
	return 0;
}
posted @ 2019-10-04 20:30  DarkValkyrie  阅读(227)  评论(0编辑  收藏  举报