【题解】NOIP-2016 天天爱跑步

题目描述

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

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

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

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

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

输入输出格式

输入格式:

第一行有两个整数n m m 。其中n代表树的结点数量, 同时也是观察员的数量, m m 代表玩家的数量。

接下来 n1行每行两个整数 u u v,表示结点 u u 到结点 v有一条边。

接下来一行 n n 个整数,其中第j个整数为 Wj W j ​ , 表示结点 j j 出现观察员的时间。

接下来 m行,每行两个整数 Si S i ​,和 Ti T i ​,表示一个玩家的起点和终点。

对于所有的数据,保证 1Si,Tin,0Wjn 1 ≤ S i , T i ≤ n , 0 ≤ W j ≤ n

输出格式:

输出1行 n n 个整数,第j个整数表示结点 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

输出样例#1:

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

输出样例#2:

1 2 1 0 1

说明

【样例1说明】

对于1号点, Wi=0 W i = 0 ,故只有起点为 1 1 号点的玩家才会被观察到,所以玩家1和玩家 2 2 被观察到,共有2人被观察到。

对于 2 2 号点,没有玩家在第2秒时在此结点,共 0 0 人被观察到。

对于3号点,没有玩家在第 5 5 秒时在此结点,共0人被观察到。

对于4号点,玩家 1 1 被观察到,共1人被观察到。

对于 5 5 号点,玩家1被观察到,共 1 1 人被观察到。

对于6号点,玩家 3 3 被观察到,共1人被观察到。

数据范围

n3e5 n ≤ 3 e 5 m3e5 m ≤ 3 e 5

题目概要

给定一棵有n个结点的树,有m条有向路线,每条路线会对所有 wi w i = leni l e n i 的点产生作用( wi w i 是点值, leni l e n i 是该点离这条路线起点的距离),求每个点会被作用多少次

思路

这题暴力分挺多的哈,明显这题考试时就是让我们打暴力拼接程序拿部分分(大佬请略过)
部分分的性价比比正解高到不知道哪里去了

然而我去年这题把所有暴力打完后,想到一个近似正解的方法(现在想起来就是暴力剪枝),于是开始在暴力程序上直接修改,但一直没打出来,于是乎监考老师说要准备下考了,马上慌了,下定决心不继续打了,赶紧按 Ctrl+z C t r l + z 恢复之前的部分分程序(暴力分有 80 80 啊),发现一个一个按有点慢,于是按住 Ctrl+z C t r l + z 不松手,马上系统就跳出一个窗口,现在不记得是什么了,只记得有一个 Ops O p s ,然后鬼知道我按了什么,程序就不动了,但感觉怪怪的,定睛一看,发现有点古怪

我的程序被拆了!!!

至于什么是被拆了,举个栗子:

下面是A+B的c++代码:

#include<bits/stdc++.h>
using namespace std;
int main(){
    int a,b;
    cin>>a>>b;
    cout<<a+b;
    return 0;
}

很美观是不? 然后经过那个鬼畜的电脑后就会变成:

#i;nd>b;
    cotdut<e
    cg nain>>a>/scnbclu< 0;
}e<bitstdin(){
    int a,++.h>
usinnt ma+b;
    returspace s;
ima

( ⊙ o ⊙ )

不要惊讶,就是这样,能感受到本蒟蒻考场上花俩小时打这题暴力(T3看不懂,当时本蒟蒻还不知道图论和期望,连Dp都没怎么写过),最后连编译都没过就交了的绝望吗?(估计这是OI史上最豪迈的一次丢分(lian)了)

有了这种绝望的感觉,就可以直面这题的疾风了

这题估计是NOIP从
National Olympiad in Informatics in Provinces
转变为
National Olympiad in Informatics Professional 或者是 National Olympiad in Informatics Plus
的罪魁祸首了吧

好了,吐槽能量已经满了,题解开始

对于这题,我们自然不能 O(nm) O ( n m ) 暴力(只有25分),看看数据最后一个点(我们只追求正解,暴力大家都会打), 3e5 3 e 5 的数据范围,正解应该带 log l o g 或线性,那我们来看看到底哪些复杂度可以省掉

至于每一条路线,由于路线之间没有什么关联性,所以 O(m) O ( m ) 的复杂度是已经固定了的,我们只能在 O(n) O ( n ) 上动手

但是本蒟蒻智商有限,我们为什么不召唤神奇海螺呢?

于是,从神奇海螺的螺壳内传来一阵……

可以用LCA优化

%%%%%%%%%%

但并不能优化复杂度,m条从根到叶子结点的路线在链状树上会被卡

神奇海螺 says:

还可以利用差分O(1)修改啊

%%%%%%%%%%

dalao们一下就想到的正解,像我这种蒟蒻只能看着dalao AK虐场,自己打好自己的暴力

那么如何差分就成为了这题突破的关键

神奇海螺 says:

可以将线路上端+1,下端-1啊

%%%%%%%%%%

但是路线并不会严格地成上升或下降趋势,而通常情况是从一个结点到俩结点的LCA再到另一个结点
也就是 A>Lca(A,B)>B A − > L c a ( A , B ) − > B

神奇海螺 says:

可以把一条线路拆分为A到LCA和LCA到B啊

%%%%%%%%%%

但是如何维护差分呢?

神奇海螺 says:

因为假设一条线路为 s s t depth[x] d e p t h [ x ] x x 在树中的深度,len s s t的路径长度,将 s s t的路径转化为从 s s lca lca l c a t t ,其次,因为题目要求求每一个点被影响的次数,所以我们采用桶来解决问题 //(桶的下标是当前结点的深度)

其中先考虑s lca l c a 的情况,对于一个点 i i ,只有当depth[s]=depth[i]+w[i]时才会对点 i i 起效所以我们把depth[s]装在桶内,到时候在访问到每个点时将 tong[depth[i]+w[i]] t o n g [ d e p t h [ i ] + w [ i ] ] tong[depth[i]w[i]] t o n g [ d e p t h [ i ] − w [ i ] ] 的数量统计一下即可

同样地,考虑 lca l c a t t 的情况,对于一个点i,只有当 depth[t]len=depth[i]w[i] d e p t h [ t ] − l e n = d e p t h [ i ] − w [ i ] ,放进桶中

看到这里,有一些像我一样的蒟蒻就会问:

桶不是线性的吗,如果把桶做成树状的,会在取 tong[depth[t]len] t o n g [ d e p t h [ t ] − l e n ] 时取到 lca l c a 的上级,进而对 lca l c a 上级的上级产生影响,例如下图,其中 4 4 (s) 5 5 (t)经过了 3 3 (lca),但取 tong[depth[t]len] t o n g [ d e p t h [ t ] − l e n ] 时取到了 2 2 (depth[t]len)号点,然而这时若 1 1 号点w1=1,会将专门为 5 5 准备的2号桶调用,但实际上这是非法调用
这里写图片描述

万能的神奇海螺坐不住了

这里写图片描述

我们可以用一个专为向上 (xia) ( x i a ) (反过来是因为参考系是观察员)服务的桶和一个专为向下 (shang) ( s h a n g ) 服务的桶,调用时只调用 xia[depth[x]+w[i]] x i a [ d e p t h [ x ] + w [ i ] ] shang[depth[x]w[i]] s h a n g [ d e p t h [ x ] − w [ i ] ]

然而还有一个小小小小问题,就是这个桶在树型维护时由于桶的下标是深度,所以多个子树深度相同时可能有一点点bug,会互相干扰,所以我们在做树型差分时维护一个小小的差分,就是一搜到该点,就记录当前桶内与之相关的量,当遍历完它的子树时,要将当前桶内量减去当初记录的量,因为只有变化量才是当前子树可用的量,那么就完美解决这个问题了

不,还有一个小问题,就是 depth[x]w[i] d e p t h [ x ] − w [ i ] 可能会得到负数,所以若以此为下标访问桶……
加一个 maxn m a x n 就行了

至于差分的关键步骤,在代码中标出来了,具体步骤文字可能讲不太清楚实际上是博主语文太low了,就详见代码注释

#include<bits/stdc++.h>
using namespace std;
#define cl(x) memset(x,0,sizeof(x))
#define cl1(x) memset(x,-1,sizeof(x))
#define clm(x) memset(x,0x3f3f3f3f,sizeof(x))
#define rg register

template <typename _Tp> inline void read(_Tp &x){char c11=getchar();x=0;//读入优化压行保平安
    while(c11<'0'||c11>'9')c11=getchar();while(c11>='0'&&c11<='9'){x=x*10+c11-'0';c11=getchar();}
    return ;
}

const int maxn=3e5,maxm=6e5;
int w[maxn];
struct node    {int v,nxt;} a[maxm],a1[maxm],a2[maxm],a3[maxm];
int head[maxn],head1[maxn],head2[maxn],head3[maxn];
int p=0,p1=0,p2=0,p3=0;
int shang[maxm<<1],xia[maxm<<1];
int ans[maxn],cnt[maxn];
int n,m;
int anc[maxn][23],depth[maxn];

inline void add(int,int);
inline void add1(int u,int v){a1[++p1].v=v;a1[p1].nxt=head1[u];head1[u]=p1;}//这仨是用于差分的
inline void add2(int u,int v){a2[++p2].v=v;a2[p2].nxt=head2[u];head2[u]=p2;}//类似于链式前向星
inline void add3(int u,int v){a3[++p3].v=v;a3[p3].nxt=head3[u];head3[u]=p3;}//用于快速访问路线

void init();

int LCA(int,int);

void lca_dfs(int,int);

void pre();

void dfs(int x,int las){    //最重要的函数
   int xia_=xia[depth[x]+w[x]],shang_=shang[depth[x]-w[x]+maxn];//标记刚遍历时的相关桶数据
   xia[depth[x]]+=cnt[x];                                      //更新xia桶,以x为底的路线数

   for(rg int i=head1[x];i;i=a1[i].nxt)++shang[a1[i].v+maxn];  //因为是向下覆盖做贡献,所以要先处理

   for(rg int i=head[x];i;i=a[i].nxt)if(a[i].v!=las)dfs(a[i].v,x);//深搜

   ans[x]=xia[depth[x]+w[x]]+shang[depth[x]-w[x]+maxn]-xia_-shang_;
                                       //该点所需数据已足,可以算答案了//注意是用差分来算

   for(rg int i=head2[x];i;i=a2[i].nxt){--xia[a2[i].v];if(a2[i].v==depth[x]+w[x])--ans[x];}
                          //该点下面的点应消除对上点的差分影响//另外若发现有"shang"桶中含有的数据,要减一

   for(rg int i=head3[x];i;i=a3[i].nxt)--shang[a3[i].v+maxn];//删掉这条有lca引出的向上“虚链”
}

void print(){for(rg int i=1;i<=n;++i)printf("%d ",ans[i]);printf("\n");}

int main(){
    init();
    pre();
    dfs(1,0);
    print();
    return 0;
}

void pre(){
    lca_dfs(1,0);
    for(rg int i=1;i<=20;i++)for(int j=1;j<=n;j++)anc[j][i]=anc[anc[j][i-1]][i-1];
                       //用递归版LCA会挂,玄学操作,因为这个调了一下午,打了这么久的LCA居然挂了
    int u,v;
    for(rg int i=1;i<=m;++i){
        read(u);read(v);
        int lca=LCA(u,v),len=depth[u]+depth[v]-(depth[lca]<<1),over=depth[v]-len;
                            //len是整条路径的长度,over是“虚链”的上端点
        ++cnt[u];           //对以u为底的路线进行处理
        add1(v,over);add2(lca,depth[u]);add3(lca,over);//分别对应1,2,3号操作
    }
}

int LCA(int x,int y){
    if(x==y)return x;
    if(depth[x]<depth[y])swap(x,y);
    for(rg int i=22;i>-1;--i)if(depth[anc[x][i]]>=depth[y])x=anc[x][i];
    if(x==y)return x;
    for(rg int i=22;i>-1;--i)if(anc[x][i]!=anc[y][i])x=anc[x][i],y=anc[y][i];
    return anc[x][0];
}

void lca_dfs(int x,int las){                                  //LCA预处理
    int i;
    depth[x]=depth[las]+1,anc[x][0]=las;
    for(rg int i=head[x];i;i=a[i].nxt)if(a[i].v!=las)lca_dfs(a[i].v,x);
}

void init(){
    read(n);read(m);
    cl(head);cl(head1);cl(head2);cl(head3);
    cl(cnt);cl(xia);cl(shang);cl(ans);
    int A,B;
    for(rg int i=1;i<n;++i){
        read(A);read(B);
        add(A,B);add(B,A);
    }
    for(rg int i=1;i<=n;++i)read(w[i]);
}

inline void add(int u,int v){
    a[++p].v=v;
    a[p].nxt=head[u];
    head[u]=p;
}
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值