题目描述
小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),要在一条(ssim t)的路径上对每个点的权值加(k),我们可以对差分数组执行以下操作:在(s)加(k),在(t)加(k),在(lca(s,t))减(k),在(father(lca(s,t)))减(k),那么以(lca(s,t))为根的子树前缀和(从叶子节点向根节点做前缀和,根节点算在内)就是原来的子树和。(树剖或LCT可取而代之)
解释完概念之后,我们来着手这道题的解决。
分析题目,容易得出每个玩家其实就是对(ssim t)的路径上的点依次增加了(1sim len(s,t))的权值。故有暴力算法,对每个运动员,我们处理出他跑的路径对树的贡献,最后暴力dfs整棵树统计答案,复杂度(O(nm))。优化的好的话就能得到80分了。。。
似乎和树上差分没什么关系?
我们不妨转化一下思维,逆向看问题。
对于树上每个节点,它都有可能作为某一对起点和终点的LCA,假设某个在(ssim 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)的式子都在等号左边了。这就方便了我们来维护这些等式,但这也是难点所在。
根据以上分析,我们就得分(ssim lca(s,t)),(lca(s,t) sim t)两条路径讨论了。
我们先考察稍微简单一点的(ssim lca(s,t))这条路径。
不难想到,对于一个玩家,它的起点(s)会给(ssim 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])。
对于(ssim 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整颗树,计算树上前缀和。具体来说,对于(ssim 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;
}