「NOIP2016 提高组」天天爱跑步题解

发布时间 2023-10-26 18:02:58作者: 逆行伐仙

题目背景
NOIP2016 提高组 Day1 T2

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

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

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

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

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

输入格式
第一行有两个整数 n 和 m 。其中 n 代表树的结点数量, 同时也是观察员的数量, m 代表玩家的数量。
接下来 n-1 行每行两个整数 u 和 v ,表示结点 u 到结点 v 有一条边。
接下来一行 n 个整数,其中第 j 个整数为 Wj, 表示结点 j 出现观察员的时间。
接下来 m 行,每行两个整数 Si 和 Ti ,表示一个玩家的起点和终点。
对于所有的数据,保证 1≤Si,Ti≤n,0≤Wj≤n 。

输出格式
输出 1 行 n 个整数,第 j 个整数表示结点 j 的观察员可以观察到多少人。

样例数据 1
输入

6 3
2 3
1 2
1 4
4 5
4 6
0 2 5 1 2 3
1 5
1 3
2 6

 

输出

2 0 0 1 1 1

 

样例数据 2
输入

5 3
1 2
2 3
2 4
1 5
0 1 0 3 0
3 1
1 4
5 5

 

输出

1 2 1 0 1

 

备注
【样例1说明】

对于 1 号点,W1 = 0 ,故只有起点为 1 号点的玩家才会被观察到,所以玩家 1 和玩家 2 被观察到,共有 2 人被观察到。
对于 2 号点,没有玩家在第 2 秒时在此结点,共 0 人被观察到。
对于 3 号点,没有玩家在第 5 秒时在此结点,共 0 人被观察到。
对于 4 号点,玩家 1 被观察到,共 1 人被观察到。
对于 5 号点,玩家 1 被观察到,共 1 人被观察到。
对于 6 号点,玩家 3 被观察到,共 1 人被观察到。
【数据规模与约定】

 

【提示】
如果你的程序需要用到较大的栈空间(这通常意味着需要较深层数的递归),请务必仔细阅读选手目录下的文档“running/stack.pdf”,以了解在最终评测时栈空间的限制与在当前工作环境下调整栈空间限制的方法。

附:running/stack.pdf 文件内容

在最终评测时,调用栈占用的空间大小不会有单独的限制,但在我们的工作环境中默认会有 8 MB 的限制。这可能会引起函数调用层数较多时,程序发生栈溢出崩溃。

我们可以使用一些方法修改调用栈的大小限制。例如,在终端中输入下列命令:

ulimit -s 1048576

此命令的意义是,将调用栈的大小限制修改为 1 GB。

例如,在选手目录建立如下sample.cpp 或 sample.pas

 

将上述源代码编译为可执行文件 sample 后,可以在终端中运行如下命令运行该程序

./sample

如果在没有使用命令“ulimit -s 1048576”的情况下运行该程序,sample 会因为栈溢出而崩溃;如果使用了上述命令后运行该程序,该程序则不会崩溃。

特别地,当你打开多个终端时,它们并不会共享该命令,你需要分别对它们运行该命令。

请注意,调用栈占用的空间会计入总空间占用中,和程序其他部分占用的内存共同受到内存限制。

 

解析:

终于把这个万年老坑给填上了。。。

首先考虑起点都在根节点的情况,则一个点的观察员能观察到选手的必要条件是:

w[i]=dep[i]

对于这种情况,一个点的答案即为以这个点为根的子树内的终点数量。

然后考虑终点都在根节点的情况,则一个点的观察员能观察到到选手的必要条件是:

dep[i]-dep[S]=w[i]

等价转化后:

dep[i]+w[i]=dep[S]

对于这种情况,一个点的答案即为以这个点为根的子树内的起点满足dep[i]+w[i]=dep[S]的数量。

最后考虑一般情况,步骤分已经再提示我们将路径拆分成两部分。

那么对于一个一般路径,拆成从 u 走到 lca(u,v) 和 lca(u,v) 走到 v 两部分。

对于第一部分,一个点的观察员能观察到的必要条件为:

dep[i]-dep[S]=w[i]

对于第二部分,一个点的观察员能观察到的必要条件为:

dep[S]+dep[T]-2*dep[lca(S,T)]-(dep[T]-dep[i])=w[i]

等价转化后:

dep[S]-2*dep[lca(S,T)]=w[i]-dep[i]

所以我们对于每个点分别维护两个桶,一个用于存起点,一个用于存终点,由于每个同要维护多个值的数量,所以从子树暴力更新到根是不行的。这里可以利用类似前缀和的思想,先记录下搜索这个点之前的对应信息,搜完这个点及子树后做差求和即可。

所以就是一道树上差分,时间复杂度近似于O(N+M+NlogN)

 

代码:

#include <bits/stdc++.h>
using namespace std;
 
const int Add=300000;
const int Max=300005;
int n,m,s,tot;
int first[Max],dep[Max],top[Max],fa[Max],son[Max],size[Max];
int sum1[Max],sum2[Max<<1],w[Max],ans[Max];
vector<int> sum[Max][2],add[Max][2];
struct shu{int to,next;}edge[Max<<1];
 
inline int get_int()
{
    int x=0,f=1;char c;
    for(c=getchar();(!isdigit(c))&&(c!='-');c=getchar());
    if(c=='-') f=-1,c=getchar();
    for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0';
    return x*f;
}
inline void print(int x)
{
    if(x>9) print(x/10);
    putchar('0'+x%10);
}
 
inline void build(int x,int y)
{
    edge[++s].next=first[x],first[x]=s,edge[s].to=y;
}
void dfs1(int p)
{
    size[p]=1;
    for(int u=first[p];u;u=edge[u].next)
    {
      int to=edge[u].to;
      if(to==fa[p]) continue;
      fa[to]=p,dep[to]=dep[p]+1,dfs1(to),size[p]+=size[to];
      if(size[to]>size[son[p]]) son[p]=to;
    }
}
void dfs2(int p,int tp)
{
    top[p]=tp;
    if(!son[p]) return;
    dfs2(son[p],tp);
    for(int u=first[p];u;u=edge[u].next)
    {
      int to=edge[u].to;
      if(to==fa[p]||to==son[p]) continue;
      dfs2(to,to);
    }
}
inline int LCA(int x,int y)
{
    while(top[x]^top[y])
    {
      if(dep[top[x]]<dep[top[y]]) swap(x,y);
      x=fa[top[x]];
    }
    return dep[x]<dep[y]?x:y;
}
void dfs(int p)
{
    int x1=w[p]+dep[p],x2=w[p]-dep[p],pre1=sum1[x1],pre2=sum2[x2+Add];
    for(int u=first[p];u;u=edge[u].next)
    {
      int to=edge[u].to;
      if(to==fa[p]) continue;
      dfs(to);
    }
    for(int i=0;i<sum[p][0].size();i++) sum1[sum[p][0][i]]+=add[p][0][i];
    for(int i=0;i<sum[p][1].size();i++) sum2[sum[p][1][i]+Add]+=add[p][1][i];
    ans[p]+=sum1[x1]-pre1,ans[p]+=sum2[x2+Add]-pre2;
}
 
int main()
{
    n=get_int(),m=get_int();
    for(int i=1;i<n;i++)
    {
      int x=get_int(),y=get_int();
      build(x,y),build(y,x);
    }
    for(int i=1;i<=n;i++) w[i]=get_int();
    dfs1(1),dfs2(1,1);
    while(m--)
    {
      int x=get_int(),y=get_int(),f=LCA(x,y);
      sum[x][0].push_back(dep[x]),add[x][0].push_back(1);
      sum[y][1].push_back(dep[x]-2*dep[f]),add[y][1].push_back(1);
      sum[f][0].push_back(dep[x]),add[f][0].push_back(-1);
      sum[fa[f]][1].push_back(dep[x]-2*dep[f]),add[fa[f]][1].push_back(-1);
    }
    dfs(1);
    for(int i=1;i<=n;i++) print(ans[i]),putchar(' ');
    return 0;
}