来一种$O(nlogn)$的毒瘤做法。
首先介绍一下主角:$dsu\ on\ tree$ 。其原理是树上启发式合并。它支持$O(nlogn)$无修改子树信息统计。
实现:树剖划分出轻重儿子。对于某个节点,先递归处理轻儿子,并清除轻子树的影响。然后处理重儿子,保留重子树的影响。再次统计轻子树的影响,就能得到该点整个子树的信息。
伪代码:
void dfs(int x,bool remain){
//remain表示是否保留该点子树的信息
int f=fa[x],s=son[x];//s为重儿子
for(int i=h[x];i;i=e[i].pre){
int y=e[i].to;
if(x!=f&&x!=s)dfs(y,0);//递归处理轻儿子,删除信息
}
if(s)dfs(s,1);//递归处理重儿子并保留信息
count(x);//这里的count是统计x的轻子树的信息
ans[x]=...;//记录答案
if(!remain)clear(x);//清除x整个子树的影响
}
复杂度:每个点到根节点的路径被剖成了最多$logn$条重链,而其中每个轻边都会使该点被用至多$O(n)$的时间统计一次,只要单点的信息统计与删除是$O(1)$的,总复杂度就为$O(nlogn)$
参考:
http://www.cnblogs.com/zzqsblog/p/6146916.html
https://www.cnblogs.com/zcysky/p/6822395.html
回到这个题上。对于每一个玩家$(x->y)$,我们可以把它分成两半:$(x->lca(x,y))+(lca(x,y)->y)$,因为前者是往上跑,后者是往下跑,分开好处理。
放个图:
红色的数字是跑到该点的时间。
(以下用$deep$代表点的深度)
先处理$(x->lca(x,y))$
对于往上跑的路径来说,只要满足$deep[x]-deep[i]=w[i]$,且该路径能覆盖到$i$,$i$就能观测到该名玩家。
即$deep[i]+w[i]=deep[x]$
再处理$(lca(x,y)->y)$
如图容易发现:从点$y$往上跳,每跳一步,时间就会$-1$,而一开始时间为$dis(x,y)$。
则一个点$i$能观测到$y$的条件是:
$deep[y]-deep[i]=dis(x,y)-w[i]$且该路径也要覆盖$i$。
其中$dis(x,y)=deep[x]+deep[y]-2*deep[lca(x,y)]$
移项整理得:
$deep[x]-2*deep[lca(x,y)]=w[i]-deep[i]$
综上,运用树上差分的思想,对于每个玩家$(x->y)$,用两个数组$up$和$down$:
- 将点$x$的$up$插入一个$deep[x]$。
- 将点$y$的$down$插入一个$deep[x]-2*deep[lca(x,y)]$。
- $lca(x,y)$会统计两遍该路径,在$up$中打上删除$deep[x]$的标记
- 为了保证路径不覆盖的点不会统计,将$fa[lca(x,y)$]打上删除$deep[x]-2*deep[lca(x,y)]$的标记。
对于一个点$i$,统计其子树的$up$、$down$所有插入过的数,答案即为$up$中$(deep[i]+w[i])$的数量加上$down$中$(w[i]-deep[i])$的数量。
怎么统计呢?$dsu\ on\ tree$就出场了!直接统计就好了。
时间复杂度:每位玩家最多影响四个点,均摊下来单点统计还是$O(1)$的,总复杂度$O(nlogn)$。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
#define maxn 300005
#define inf 0x3f3f3f
#define pn putchar('\n')
#define px(x) putchar(x)
#define ps putchar(' ')
#define pd puts("======================")
#define pj puts("++++++++++++++++++++++")
using namespace std;
inline int read(){
int x=0,y=0;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')y=1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return y?-x:x;
}
template<typename T>
inline T read(){
T x=0;
int y=0;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')y=1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return y?-x:x;
}
int h[maxn],w[maxn],top[maxn],deep[maxn],fa[maxn],siz[maxn],son[maxn],num,n,m,ans[maxn];
bool vis[maxn];
int upi[maxn];
vector<int>doi[maxn],dod[maxn],upd[maxn];
//upi:up insert,向上路径的插入。因为该插入只会插入某个点的deep,所以压成了普通的数组
//upd:up delete,向上路径的删除
//doi、dod同理
struct edge{
int pre,to;
}e[maxn<<1];
inline void add(int from,int to){
e[++num].pre=h[from],h[from]=num,e[num].to=to;
}
struct Tree_Chain_Split{
void dfs1(int node=1){
siz[node]=1;
for(register int i=h[node];i;i=e[i].pre){
int x=e[i].to;
if(!siz[x]){
fa[x]=node,deep[x]=deep[node]+1;
dfs1(x);
if(siz[x]>siz[son[node]])son[node]=x;
siz[node]+=siz[x];
}
}
}
void dfs2(int node=1){
vis[node]=1;
if(son[node]){
top[son[node]]=top[node];
dfs2(son[node]);
for(register int i=h[node];i;i=e[i].pre){
int x=e[i].to;
if(!vis[x]){
top[x]=x;
dfs2(x);
}
}
}
}
int lca(int x,int y){
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]])swap(x,y);
x=fa[top[x]];
}
return deep[x]<deep[y]?x:y;
}
}tcs;
//树链剖分,顺便用的树剖的lca
int upcnt[maxn],CNT[maxn<<1],*downcnt=&CNT[maxn];
//upcnt是向上路径统计数组,downcnt反之。两种路径统计依据不同,所以分开
//因为w[i]-deep[i]可能为负数,所以用了指针小技巧使downcnt能使用负数下标
bool in[maxn];
//用于dsu on tree的数组。标记上的点在统计子树信息时不会访问该点的子树(见count函数)
void count(int node){
int f=fa[node];
for(register int i=h[node];i;i=e[i].pre){
int x=e[i].to;
if(x!=f&&!in[x])count(x);
}
upcnt[deep[node]]+=upi[node];
for(register int i=0;i<upd[node].size();++i)
--upcnt[upd[node][i]];
for(register int i=0;i<doi[node].size();++i)
++downcnt[doi[node][i]];
for(register int i=0;i<dod[node].size();++i)
--downcnt[dod[node][i]];
}//统计信息
void clear(int node){
int f=fa[node];
for(register int i=h[node];i;i=e[i].pre){
int x=e[i].to;
if(x!=f)clear(x);
}
upcnt[deep[node]]-=upi[node];
for(register int i=0;i<upd[node].size();++i)
++upcnt[upd[node][i]];
for(register int i=0;i<doi[node].size();++i)
--downcnt[doi[node][i]];
for(register int i=0;i<dod[node].size();++i)
++downcnt[dod[node][i]];
}
void dfs(int node=1,bool remain=0){
int s=son[node],f=fa[node];
if(s){
for(register int i=h[node];i;i=e[i].pre){
int x=e[i].to;
if(x!=s&&x!=f)dfs(x,0);
}
dfs(s,1);
in[s]=1;//count时不会统计上它的重子树
}
count(node);//统计轻儿子的信息
if(deep[node]+w[node]<=n)ans[node]=upcnt[deep[node]+w[node]];
//判一下不能越界
ans[node]+=downcnt[w[node]-deep[node]];
//获取答案
in[s]=0;//一定要清掉标记
if(!remain)clear(node);//清除整个子树的信息
}
void init(){
while(m--){
int a=read(),b=read(),l=tcs.lca(a,b);
++upi[a];
doi[b].push_back(deep[a]-(deep[l]<<1));
upd[l].push_back(deep[a]);
dod[fa[l]].push_back(deep[a]-(deep[l]<<1));
}
}//读入每名玩家并处理
int main(){
n=read(),m=read();
for(register int i=1;i<n;++i){
int a=read(),b=read();
add(a,b),add(b,a);
}
for(register int i=1;i<=n;++i)
w[i]=read();
tcs.dfs1(),tcs.dfs2();
init(),dfs();
for(register int i=1;i<=n;++i)
printf("%d ",ans[i]);
}
码量确实有点大。不过$dsu\ on\ tree$在某些题中还是很高效的$QwQ$。