LGP1600_1 天天爱跑步(高妙统计贡献) 笔记

原题链接:传送门

题意简述

给定一棵 N 个结点的树。有 M 个玩家从第 0 时刻开始从 si 出发,以每秒一条边的速度沿着树上的简单路径向 ti 跑去。对于每个结点 j 都有一个观察员,会选择在 wj 时刻观察其结点上所有玩家。问每个观察员分别能观察到多少玩家。

N,M3×105

解决方案

最暴力的做法是模拟每个玩家的跑步过程,这么做在数据随机的情况下是 O(MlogN) 的,但是会被链数据卡到 O(NM)。考虑这样做为什么不优:这种做法是让每个玩家沿路给路上的结点加 01 个贡献,而一条链压缩到极致的信息量也是 logN 级别的。但如果我们转而从每个观察员的视角考虑问题,让玩家的贡献在合适的时间段放在一定的桶里,然后顺着树的深搜过程让每个结点“取”贡献,这么做的话每个结点统计答案就是 O(1) 级别了。

那么这么做有没有可行性呢?我们来分析一下一个玩家给一个观察员造成贡献的条件。

首先,我们令 ai=lca(si,ti)di=dis(si,ti)。把 (si,ti) 的路径拆成 (si,ai)(ai,ti) 这两条深度单调连续变化的链来考虑(这个处理是很经典显然的)。

对于观察员 u 来说,若 (si,ai) 为其造成贡献:

  • si 必须在 u 的子树内部,且 ai 必须在 u 的子树外部或与 u 重合,否则整条路径压根不经过 u
  • depsi=depu+wu,这样此玩家才能刚好在第 wu 秒走到 u

同理,对于观察员 u 来说,若 (si,ai) 为其造成贡献:

  • ti 必须在 u 的子树内部,且 ai 必须在 u 的子树外部或与 u 重合,否则整条路径压根不经过 u
  • deptidi=depuwi。这个式子看起来不太好懂?可以这么理解:如果把路径抻直,那么第 0 时刻时玩家就在 deptidi 的深度。

既然我们要让观察员“自取”贡献,那么我们就要开桶把这些贡献存进去,然后观察员顺着下标就取出来了。另外还有两点要注意:

  1. 如果刚好有玩家在 ai 处做出了贡献,那么会算重。我们要提前减掉多算的一次(见代码)
  2. deptididepuwi 都有可能是负的,因此代码实现上要给其加一个偏移量 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;
}

反思与总结

这道题的此做法中,我们在算贡献时从考虑每条玩家的路径转而考虑每个结点的贡献。这启示我们要考虑从不同的角度和对象计算贡献,找到最优的那个。

posted @   矞龙OrinLoong  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示