LGP1600_1 天天爱跑步(高妙统计贡献) 笔记
原题链接:传送门
题意简述
给定一棵 \(N\) 个结点的树。有 \(M\) 个玩家从第 \(0\) 时刻开始从 \(s_i\) 出发,以每秒一条边的速度沿着树上的简单路径向 \(t_i\) 跑去。对于每个结点 \(j\) 都有一个观察员,会选择在 \(w_j\) 时刻观察其结点上所有玩家。问每个观察员分别能观察到多少玩家。
\(N,M\le 3\times 10^5\)。
解决方案
最暴力的做法是模拟每个玩家的跑步过程,这么做在数据随机的情况下是 \(O(M\log N)\) 的,但是会被链数据卡到 \(O(NM)\)。考虑这样做为什么不优:这种做法是让每个玩家沿路给路上的结点加 \(0\) 或 \(1\) 个贡献,而一条链压缩到极致的信息量也是 \(\log N\) 级别的。但如果我们转而从每个观察员的视角考虑问题,让玩家的贡献在合适的时间段放在一定的桶里,然后顺着树的深搜过程让每个结点“取”贡献,这么做的话每个结点统计答案就是 \(O(1)\) 级别了。
那么这么做有没有可行性呢?我们来分析一下一个玩家给一个观察员造成贡献的条件。
首先,我们令 \(a_i=\text{lca}(s_i,t_i)\),\(d_i=dis(s_i,t_i)\)。把 \((s_i,t_i)\) 的路径拆成 \((s_i,a_i)\) 和 \((a_i,t_i)\) 这两条深度单调连续变化的链来考虑(这个处理是很经典显然的)。
对于观察员 \(u\) 来说,若 \((s_i,a_i)\) 为其造成贡献:
- \(s_i\) 必须在 \(u\) 的子树内部,且 \(a_i\) 必须在 \(u\) 的子树外部或与 \(u\) 重合,否则整条路径压根不经过 \(u\)。
- \(dep_{s_i}=dep_u+w_u\),这样此玩家才能刚好在第 \(w_u\) 秒走到 \(u\)。
同理,对于观察员 \(u\) 来说,若 \((s_i,a_i)\) 为其造成贡献:
- \(t_i\) 必须在 \(u\) 的子树内部,且 \(a_i\) 必须在 \(u\) 的子树外部或与 \(u\) 重合,否则整条路径压根不经过 \(u\)。
- \(dep_{t_i}-d_i=dep_{u}-w_i\)。这个式子看起来不太好懂?可以这么理解:如果把路径抻直,那么第 \(0\) 时刻时玩家就在 \(dep_{t_i}-d_i\) 的深度。
既然我们要让观察员“自取”贡献,那么我们就要开桶把这些贡献存进去,然后观察员顺着下标就取出来了。另外还有两点要注意:
- 如果刚好有玩家在 \(a_i\) 处做出了贡献,那么会算重。我们要提前减掉多算的一次(见代码)
- \(dep_{t_i}-d_i\) 和 \(dep_{u}-w_i\) 都有可能是负的,因此代码实现上要给其加一个偏移量 \(N\),相应地桶数组也要开两倍。
代码
#include <bits/stdc++.h>
using namespace std;
const int MaxN=6e5+5;
int frdint(){
int n=0,k=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')k=-1;ch=getchar();}
while(isdigit(ch))n=(n<<3)+(n<<1)+ch-'0',ch=getchar();
return n*k;
}
void fwrint(int x){
if(x<0)putchar('-'),x=-x;
if(x>9)fwrint(x/10);
putchar(x%10+'0');
}
int N,M,X,Y,W[MaxN];
vector<int> Ti[MaxN];
void addudge(int u,int v){
Ti[u].push_back(v);
Ti[v].push_back(u);
}
int dep[MaxN],tfa[MaxN],siz[MaxN],hvs[MaxN];
void dfs1(int u,int f){
dep[u]=dep[f]+1,tfa[u]=f,siz[u]=1;
for(int v : Ti[u]){
if(v==f)continue;
dfs1(v,u);siz[u]+=siz[v];
if(siz[v]>siz[hvs[u]])hvs[u]=v;
}
}
int top[MaxN];
void dfs2(int u,int t){
top[u]=t;if(!hvs[u])return;
dfs2(hvs[u],t);for(int v : Ti[u]){
if(v!=tfa[u]&&v!=hvs[u])dfs2(v,v);
}
}
int getlca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
x=tfa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
struct anob{int s,t,d;}P[MaxN];
int buc[2][MaxN<<1],ans[MaxN];
vector<int> avec[MaxN],svec[MaxN],tvec[MaxN];
void dfs3(int u){
int tmp1=buc[0][dep[u]+W[u]],tmp2=buc[1][W[u]-dep[u]+N];
for(int v : Ti[u])if(v!=tfa[u])dfs3(v);
buc[0][dep[u]]+=svec[u].size();
for(int i : tvec[u])buc[1][P[i].d-dep[P[i].t]+N]++;
int cres=buc[0][dep[u]+W[u]]-tmp1+buc[1][W[u]-dep[u]+N]-tmp2;
if(cres>=0)ans[u]+=cres;
for(int i : avec[u])buc[0][dep[P[i].s]]--;
for(int i : avec[u])buc[1][P[i].d-dep[P[i].t]+N]--;
}
int main(){
N=frdint(),M=frdint();
for(int i = 1;i < N;i++){
X=frdint(),Y=frdint();
addudge(X,Y);
}
for(int i = 1;i <= N;i++)W[i]=frdint();
dfs1(1,0),dfs2(1,1);
for(int i = 1;i <= M;i++){
auto &[cs,ct,cd]=P[i];
cs=frdint(),ct=frdint();
svec[cs].push_back(i);
tvec[ct].push_back(i);
int anc=getlca(cs,ct);
avec[anc].push_back(i);
cd=dep[cs]+dep[ct]-dep[anc]*2;
if(dep[anc]+W[anc]==dep[cs])ans[anc]--;
}
dfs3(1);
for(int i = 1;i <= N;i++){
fwrint(ans[i]),putchar(' ');
}
return 0;
}
反思与总结
这道题的此做法中,我们在算贡献时从考虑每条玩家的路径转而考虑每个结点的贡献。这启示我们要考虑从不同的角度和对象计算贡献,找到最优的那个。
浙公网安备 33010602011771号