题目描述
小c同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。«天天爱跑步»是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。
这个游戏的地图可以看作一一棵包含 n n 个结点和 条边的树, 每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从 1 1 到的连续正整数。
现在有 m m 个玩家,第个玩家的起点为 Si S i ,终点为 Ti T i 。每天打卡任务开始时,所有玩家在第 0 0 秒同时从自己的起点出发, 以每秒跑一条边的速度, 不间断地沿着最短路径向着自己的终点跑去, 跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树, 所以每个人的路径是唯一的)
小C想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点jjj的观察员会选择在第秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第 Wj W j 秒也理到达了结点 j j 。 小C想知道每个观察员会观察到多少人?
注意: 我们认为一个玩家到达自己的终点后该玩家就会结束游戏, 他不能等待一 段时间后再被观察员观察到。 即对于把结点作为终点的玩家: 若他在第 Wj W j 秒前到达终点,则在结点 j j 的观察员不能观察到该玩家;若他正好在第秒到达终点,则在结点 j j 的观察员可以观察到这个玩家。
输入输出格式
输入格式:
第一行有两个整数和 m m 。其中代表树的结点数量, 同时也是观察员的数量, m m 代表玩家的数量。
接下来 行每行两个整数 u u 和 ,表示结点 u u 到结点 有一条边。
接下来一行 n n 个整数,其中第个整数为 Wj W j , 表示结点 j j 出现观察员的时间。
接下来 行,每行两个整数 Si S i ,和 Ti T i ,表示一个玩家的起点和终点。
对于所有的数据,保证 1≤Si,Ti≤n,0≤Wj≤n 1 ≤ S i , T i ≤ n , 0 ≤ W j ≤ n 。
输出格式:
输出1行 n 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
输出样例#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说明】
对于号点, Wi=0 W i = 0 ,故只有起点为 1 1 号点的玩家才会被观察到,所以玩家和玩家 2 2 被观察到,共有人被观察到。
对于 2 2 号点,没有玩家在第秒时在此结点,共 0 0 人被观察到。
对于号点,没有玩家在第 5 5 秒时在此结点,共0人被观察到。
对于号点,玩家 1 1 被观察到,共人被观察到。
对于 5 5 号点,玩家被观察到,共 1 1 人被观察到。
对于号点,玩家 3 3 被观察到,共人被观察到。
数据范围
n≤3e5 n ≤ 3 e 5 , m≤3e5 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
不要惊讶,就是这样,能感受到本蒟蒻考场上花俩小时打这题暴力(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 到, depth[x] d e p t h [ x ] 为 x x 在树中的深度,为 s s 到的路径长度,将 s s 到的路径转化为从 s s 到和 lca l c a 到 t t ,其次,因为题目要求求每一个点被影响的次数,所以我们采用桶来解决问题 //(桶的下标是当前结点的深度)
其中先考虑到 lca l c a 的情况,对于一个点 i i ,只有当时才会对点 i i 起效所以我们把装在桶内,到时候在访问到每个点时将 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 的情况,对于一个点,只有当 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 到 5 5 经过了 3 3 ,但取 tong[depth[t]−len] t o n g [ d e p t h [ t ] − l e n ] 时取到了 2 2 号点,然而这时若 1 1 号点,会将专门为 5 5 准备的号桶调用,但实际上这是非法调用
万能的神奇海螺坐不住了
我们可以用一个专为向上 (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;
}