P1600 [NOIP 2016 提高组] 天天爱跑步 题解

一道好题,如果直接根据玩家去找有多少点能够记录到它,比较难处理。我们可以反过来想,转成统计树上每一个点可以记录多少运动员,那么记录一个运动员有两种情况。

我们不妨设以\(1\)为根,遍历到\(x\)时的深度为\(dep_x\)

第一种情况:点\(x\)位于\(s\)\(lca(s,t)\)的路径上,如图所示:
image

那么显然如果从\(s\)走到\(x\)时候恰好是\(w_x\),那么一定满足\(dep_x-dep_s=w_x\),进行转化可以得到\(dep_x-w_x=dep_s\),这也就意味着对于一个点\(x\),其能捕捉到的点\(s\)需要满足\(dep_x-w_x=dep_s\)

第二种情况:点\(x\)位于\(s\)\(lca(s,t)\)的路径上,如图所示:
image

那么我们仿照第一种情况去列出方程,需要满足从\(s\)走到\(x\)的时候恰好是\(w_x\),那么需要满足\(dep_x-dep_{lca}+dep_s-dep_{lca}=w_x\),即把\(s\)走到\(x\)的路径拆为\(s->lca\)\(lca->x\),移项得到\(2*dep_{lca}-dep_s=dep_x-w_x\),也就是说对于一个点\(x\),它能捕获到的点\(s\)需要满足\(2*dep_{lca}-dep_s=dep_x-w_x\)

对于上面的第一种情况而言,对于一个节点为\(s\)的玩家,它能够通过第一种情况影响到\(s-lca\)路径上的所有点;对于第二种情况而言,对于一个节点为\(s\)的玩家,它能够通过第二种情况影响到\(lca-t\)路径上的所有点,这种情况应该不包含\(lca\),因为\(lca\)我们已经在第一种情况被算进去了。

那么我们应该怎么维护这个信息呢?我们可以用树上差分快速的为两种情况的区间分别进行标记。同时我们现在需要对每个点维护一个可以查询区间中单点值的信息,来查询满足两种情况等式的点有多少,可以每个点用两个值域线段树分别维护第一种情况和第二种情况的信息。

具体做法是,
我们以\(1\)为根开始遍历,算出每个点的深度\(dep_x\)。然后对每个玩家而言,第一种情况在\(s\)的线段树的\(dep[s]\)位置\(+1\)\(fa[lca]\)的线段树的\(dep[fa[lca]]\)位置\(-1\),第二种情况在\(t\)的线段树上进行\(2*dep_{lca}-dep_s\)位置\(+1\),在\(t\)的右节点的线段树上进行\(2*dep_{lca}-dep_s\)位置\(-1\)
最后再次以\(1\)为根进行遍历,合并自己的左右节点的线段树,因为上面的差分操作,我们可以保证第一种情况和第二种情况一定被限制在了\(s->lca->t\)的这条路径上,而不会因为这个标记影响到其他不在路径上的点。

#include <bits/stdc++.h>
using namespace std;

const int T=1e7+2,N=3e5+2,P=20;

vector<int> e[N];
int n,m;
int sum[T],dep[N],fa[N],ls[T],rs[T];
int w[N],cnt=0;
int rootup[N],rootdown[N];
int st[N][P],ans[N];

void up(int i){
    sum[i]=sum[ls[i]]+sum[rs[i]];
}

void dfs1(int u,int last){
    fa[u]=last;
    dep[u]=dep[last]+1;
    st[u][0]=last;
    for(int p=1;p<P;p++){
        st[u][p]=st[st[u][p-1]][p-1];
    }
    for(int v:e[u]){
        if(v^last){
            dfs1(v,u);
        }
    }
}

int LCA(int a,int b){
    if(dep[a]<dep[b]) swap(a,b);
    for(int i=P-1;i>=0;i--){
        if(dep[st[a][i]]>=dep[b]) a=st[a][i];
    }
    if(a==b) return a;
    for(int i=P-1;i>=0;i--){
        if(st[a][i]!=st[b][i]){
            a=st[a][i],b=st[b][i];
        }
    }
    return st[a][0];
}

int add(int ji,int jv,int l,int r,int i){
    int idx=i;
    if(!idx) idx=++cnt;
    if(l==r){
        sum[idx]+=jv;
        return idx;
    }
    int mid=(l+r)>>1;
    if(ji<=mid) ls[idx]=add(ji,jv,l,mid,ls[idx]);
    else rs[idx]=add(ji,jv,mid+1,r,rs[idx]);
    up(idx);
    return idx;
}

int merge(int l,int r,int t1,int t2){
    if(t1==0||t2==0) return t1+t2;
    if(l==r){
        sum[t1]+=sum[t2];
    }else{
        int mid=(l+r)>>1;
        ls[t1]=merge(l,mid,ls[t1],ls[t2]);
        rs[t1]=merge(mid+1,r,rs[t1],rs[t2]);
        up(t1);
    }
    return t1;
}

int query(int ji,int l,int r,int i){
    if(ji<l||ji>r||i==0) return 0;
    if(l==r){
        return sum[i];
    }
    int mid=(l+r)>>1;
    if(ji<=mid) return query(ji,l,mid,ls[i]);
    else return query(ji,mid+1,r,rs[i]);
}

void dfs2(int u,int last){
    for(int v:e[u]){
        if(v^last){
            dfs2(v,u);
            rootup[u]=merge(1,n,rootup[u],rootup[v]);
            rootdown[u]=merge(-n,n,rootdown[u],rootdown[v]);
        }
    }
    ans[u]=ans[u]+query(dep[u]+w[u],1,n,rootup[u])+query(dep[u]-w[u],-n,n,rootdown[u]);
}


int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);

    cin>>n>>m;
    for(int i=1;i<=n-1;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v),e[v].push_back(u);
    }
    for(int i=1;i<=n;i++) cin>>w[i];
    dfs1(1,0);
    for(int i=1;i<=m;i++){
        int s,t;
        cin>>s>>t;
        int lca=LCA(s,t);
        rootup[s]=add(dep[s],1,1,n,rootup[s]);
        rootup[fa[lca]]=add(dep[s],-1,1,n,rootup[fa[lca]]);
        rootdown[t]=add(2*dep[lca]-dep[s],1,-n,n,rootdown[t]);
        rootdown[lca]=add(2*dep[lca]-dep[s],-1,-n,n,rootdown[lca]);
    }
    dfs2(1,0);
    for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
    return 0;
}

总结:直接计算答案比较困难的时候,可以用拆贡献的思想,从另外一个角度出发统计每种情况的贡献。
维护树上区间信息可以采用树上差分,需要维护每个点的区间信息并需要对子树节点的信息进行合并的时候可以采用线段树合并的方法。

posted @ 2026-03-05 23:06  1445141  阅读(21)  评论(0)    收藏  举报