[Noip2016]天天爱跑步 LCA+树上差分

本文解析了一道NOIP2016竞赛题“天天爱跑步”。题目要求在给定的树结构中,计算每个节点的贡献值。通过树链剖分和双桶计数的方法,最终实现了O(n log n)的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接:Noip2016天天爱跑步 .

—————————————-

概述

题目大意如下:

给定一棵 n 个点的树,每一个点有一个点权w[i],再告诉你 m 条路径的起点u和终点 v 。对于一条路径,假如路径上某一点x到起点 u 的距离等于w[x],那么 x 的贡献加1。最后求每一个点的贡献。(1n,m300000, 0w[i]n.)

—————————————-

分析

我当时考试的时候,看到这题第一眼感觉就是十分不可做,所以打了60分暴力草草了事。即使到现在,我仍感觉这道题绝对不是Noip的难度,毕竟涉及的算法思想不是很容易就能想到,应该有Noi的级别。

言归正传,我们观察一下这题。

根据套路,我们设1号节点为根,预处理出每一条路径 u,v lca ,这里我用的是树链剖分。(因不会tarjon而瑟瑟发抖)

可以发现,假如我们的思路总是跟着“路径上的每一个点能获得的贡献”去想,复杂度怎么都是 O(nm) ,不会有优化的空间。所以我们可以避开考虑每一条路径能为路径上的点所提供的贡献,而是考虑每一个点如何能从路径上获得贡献。

我们对每个点 x 这么考虑:对x能产生贡献的起点 u 一定与x的距离为 w[x] ,我们设 dep[i] i 的深度,那么u的位置可以分为两种情况:

  1. u x深度大,即 u x下方。

    此时有:

    dep[x]+w[x]=dep[u].

    我们能够发现等式左边的值只跟 x 有关,确定了x就确定了整个等式。

    那么对 x “可能”有贡献的起点u即是在 x 的子树中,且深度为dep[x]+w[x]的起点。

    注意一下,上文我说“可能”有贡献,是因为以这个 u 为起点的路径可能不会经过x,也就是说: u v lca 可能在 x 的下方,即dep[lca]>dep[x]。这个情况我们在后面再考虑如何消除。

    我们建一个桶 cnt[i] 表示处理到当前时深度为 i 的起点的个数。那如何获得在x的子树中深度为 dep[x]+w[x] 的起点的个数呢?

    dep[x]+w[x]=t[x] ,我们可以发现,假如我们把初次搜索到 x 时的cnt[t[x]]记录下来,等搜索完 x 的子树后回溯到x时,此时新的 cnt[[x]] 与初次的 cnt[t[x]] 的差值即是 x 子树中深度为t[x]的起点个数。

    好了,统计的问题我们解决了,那么上面所提到的不应该有的贡献如何消除呢?

    我们思考一下,假如 u x的子树内, lca 若在 x 的子树外则能保证这条路径一定经过x,能对 x 产生贡献,相反地,lca若在 x 的子树内则没有贡献。

    所以,当我们处理完x这个点时,我们可以把所有 lca x 的路径的贡献删除,这样回溯到上面的时候就可以把本不应该有的贡献消除了。

    第一种情况讨论完毕。

  2. u的深度比 x 小,u x 上方。

    这个时候起点u不在 x 的子树里,没有办法统计,所以我们可以换过来统计终点v的贡献。

    此时有:

    dep[x]w[x]=dep[v]dis(u,v).

    我们观察到等式左边仍是只与 x 有关的量,确定了x就能确定哪些终点对它有贡献。

    类比第一种情况,我们另开一个桶 cnt2[i] 统计处理到当前点时, dep[v]dis(u,v) i 的终点的个数。统计方法和第一种情况一样,记dep[x]w[x] t[x] ,记录初次搜索到 x 时的cnt2[t[x]],处理完 x 的子树回溯到x时,用新的 cnt2[t[x]] 减去初次的值就是在 x 的子树内的终点个数。之后再把lca x 的终点的贡献删除掉就行了。

    有一个小细节,dep[v]dis(u,v) dep[x]w[x] 也许会是负数,我们只需要将数组整体向右平移就完事了。

    第二种情况讨论完毕。

值得注意的是,对于一条路径,假如一个观察点 x 它同时满足上面两种情况,那么这条路径的贡献就会被算2次,所以我们要减掉这多算的贡献。

观察一下,假如观察点x它同时满足上面两种情况时,我们可以得到:

dep[x]+w[x]=dep[u].
dep[x]w[x]=dep[v]dis(u,v).

这两个式子相加,我们得到:

dep[x]=dep[lca].

也就是说,只有当 x 是这条路径的lca时,它才可能被算2次贡献。

再把两个式子相减,进而得到:

w[x]=dis(u,lca).

这个式子的意思是,在 x 是路径的lca的前提下,当 w[x] x u的距离时,这条路径对 x 的贡献会被算2次。

所以,我们只需要枚举每一条路径的lca,判断它是否满足 w[lca]=dis(u,lca) ,满足的话就把它的贡献-1即可。

综上,这道题的解法就讲完了,时间复杂度 O(n log n).

—————————————-

代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#define ll long long
#define For(i, j, k) for(int i = j; i <= (int)k; ++ i)
#define Forr(i, j, k) for(int i = j; i >= (int)k; -- i)
#define INF 0x3f3f3f3f
using namespace std;

const int maxn = 300000 + 5;

int n, m, tot, maxdep;
int w[maxn], s[maxn], cnt1[maxn], cnt2[maxn<<2], Ans[maxn];
int size[maxn], dep[maxn], son[maxn], fa[maxn], top[maxn];
int Cnt, Begin[maxn], Next[maxn<<1], To[maxn<<1];
vector<int> q1[maxn], q2[maxn], q3[maxn];
struct player{
    int u, v, lca, dis;
}a[maxn];

inline int Read(){
    int x = 0;    char c = getchar();
    for(; !isdigit(c); c = getchar());
    for(; isdigit(c); c = getchar())    x = (x << 3) + (x << 1) + (c ^ 48);
    return x;
}

inline void Swap(int &x, int &y) {int temp = x;  x = y;  y = temp;}

inline void Add(int x, int y){
    To[++Cnt] = y;
    Next[Cnt] = Begin[x];
    Begin[x] = Cnt;
}

void dfs1(int x){
    size[x] = 1;
    for(int i = Begin[x],v; i; i = Next[i]){
        v = To[i];
        if(v != fa[x]){
            fa[v] = x;      dep[v] = dep[x] + 1;
            dfs1(v);
            size[x] += size[v];
            if(size[v] > size[son[x]])    son[x] = v;
        }
    }
}

void dfs2(int x, int tp){
    top[x] = tp;
    if(son[x])    dfs2(son[x], tp);
    for(int i = Begin[x],v; i; i = Next[i]){
        v = To[i];
        if(v != fa[x] && v != son[x])
            dfs2(v, v);
    }
}

inline int Lca(int x, int y){
    int tx, ty;
    while((tx = top[x]) != (ty = top[y])){
        if(dep[tx] < dep[ty])    Swap(x, y),  Swap(tx, ty);
        x = fa[tx];
    }
    if(dep[x] > dep[y])     Swap(x, y);
    return x;
}

void dfs(int x){
    int now = w[x] + dep[x], cun;    if(now <= maxdep)     cun = cnt1[now];
    for(int i = Begin[x],v; i; i = Next[i]){
        v = To[i];
        if(v != fa[x])    dfs(v);
    }
    cnt1[dep[x]] += s[x];    if(now <= maxdep)    Ans[x] = cnt1[now] - cun;
    For(i, 0, q1[x].size()-1)    -- cnt1[dep[q1[x][i]]];
}

void DFS(int x){
    int now = dep[x] - w[x] + maxn,  cun = cnt2[now];
    for(int i = Begin[x],v; i; i = Next[i]){
        v = To[i];
        if(v != fa[x])    DFS(v);
    }
    For(i, 0, q2[x].size()-1)    ++ cnt2[q2[x][i]+maxn];
    Ans[x] += cnt2[now] - cun;
    For(i, 0, q3[x].size()-1)    -- cnt2[q3[x][i]+maxn];
}

int main(){    
    int x, y;
    n = Read(),  m = Read();
    For(i, 1, n - 1){
        x = Read(),  y = Read();
        Add(x, y),    Add(y, x);
    }
    For(i, 1, n)    w[i] = Read();
    For(i, 1, m)    a[i].u = Read(),  a[i].v = Read();


    dep[1] = 1,  dfs1(1);    dfs2(1, 1);
    For(i, 1, n)    maxdep = max(maxdep, dep[i]);
    For(i, 1, m){
        a[i].lca = Lca(a[i].u, a[i].v);    ++ s[a[i].u];
        a[i].dis = dep[a[i].u] + dep[a[i].v] - (dep[a[i].lca] << 1);
        q1[a[i].lca].push_back(a[i].u);
    }
    dfs(1);
    For(i, 1, m){
        q2[a[i].v].push_back(dep[a[i].v] - a[i].dis);
        q3[a[i].lca].push_back(dep[a[i].v] - a[i].dis);
    }
    DFS(1);

    For(i, 1, m)    if(dep[a[i].u] - dep[a[i].lca] == w[a[i].lca])    -- Ans[a[i].lca];
    For(i, 1, n)    printf("%d ", Ans[i]);
    return 0;
}

—————————————-

小结

经过2天的时间, Noip2016 最难的一道题终于被攻克下来了。题目用到的算法虽然很基础,但是分析思考的过程十分的耐人寻味,或许我就是欠缺这种思维的练习。总的来说,这是道难得的好题!

—————————————-
wrote by miraclejzd.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值