NOIP2016 Day2 T2 天天爱跑步(树上差分)

1.题目描述:
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
传送门:https://www.luogu.org/problem/show?pid=1600


超级大火题啊,简直比T3还要难(难很多)。
先总结一下做法:LCA+树上差分;


部分分做法:
为什么要先讲部分呢?因为满分算法就是由部分分算法推导来的。
这里要讲的部分分是第4档部分分:
这里写图片描述
如果是一条链,那么行走方向只有左右吧。
那么我们就把左右方向拆开来处理,这里以处理向右走(左走同理)为例:
当行走方向朝右时,一定有:S <= T 。 稍微想一想我们就可以发现:
若在 i 结点的观察员可以观察到第 j 个人,则一定有 W[ i ] == i - S[ j ],并且j走到i时还未到终点。
我们进行移项:S[ j ] = i - W[ i ],这是一个定值!!!!
这意味这,我们只用求解在 S[ j ] 到 T[ j ]中间,有多少个 结点k 满足:k - W[k] == S[ j ];
我们可以用差分来做这个东西:
记tot[ i ]表示当前以 i 结点为起点的且未到终点的玩家个数
我们预先在每一个终点处挂上其对应的起点位置。 然后从1结点开始处理,向后一直循环到N结点。
每次在处理一个结点 i 时,分为以下三步:

  A.将以此结点为起点的玩家加入答案。
  B.计算: Ans[i] = Ans[i] + tot[ i-W[i] ];
  C.将以此结点为终点的玩家退出答案(即将其对应起点的tot--) 

这样一路统计下去,然后反向处理朝朝左移动的情况。
根据定义可以得知,结点 i 的答案最终存在Ans[ i ]中


满分做法:
有了部分分的算法,我们就可以来尝试满分算法了。
其实,部分是给了一定的提示的:S = 1,T = 1—(@_@)
这其实就是暗示我们要吧路径拆成向上(S -> LCA)与向下(LCA -> T)两部分啦。


A.向上路径:
与之前部分分算分思考方式相同:
差分,但原先在S时加tot[S],在T时减tot[S],现在加减就要改变了。
显然根据树上差分思想,是要在LCA(S,T),T上做处理。
现在对于 i结点,若其观察员可以观察到某个人k,则要满足什么呢?
不难推出: deep[ i ] + W[ i ] == deep[ S[k] ];
由于方向一定是从S向LCA跑的,我们就要用差分使这一路径上的所有点都可以被贡献。
具体做法即在S时,tot[dep[S]]++;在LCA时,tot[dep[S]]–;
那么对于 i 结点,答案即更新为: Ans[ i ] = Ans[ i ] + tot[ dep[i]+W[i] ];
这里有一个要注意的地方:由于对于dep[k],可能统计了非LCA子树内结点,所以答案更新应该要作差:

int tardep = dep[u]+W[u],bef = tot[tardep];
BalaBalaBala......
Ans[u] = Ans[u] + tot[tardep] - bef;

B.向下路径:
向下路径的处理其实与向上处理的方法基本一样。
S[ k ] == dep[ i ] - W[ i ]时是可以观察到的。
但我们注意到dep[ i ] - W[ i ]有可能为一个负数,这就赋予其一定的特殊性。
对于这个问题,我的处理方法是建立一个”虚点”(深度)与之对应:
例如下面这棵树:
这里写图片描述
S = 3,T = 4 ,路径为3—->4,W[2] = 2
那么这时候2号结点可以观察的点满足:dep[S[k]] = dep[2]-W[2] = 0;
可是这里不存在第0层,那我们就人为搞一个第0层。
这里写图片描述
这样我们原先的tot[dep[S]]++,tot[dep[S]]–,也就变为tot[dep[虚点]]++/–;
所以在上面这个例子中,就为:tot[0]++ / tot[0]–
当然,此时所有的层数都要加一个很大的数(防止负数下标)。
int tardep = dep[u]-W[u]+BKS (BKS == 300000)
这里还有一个需要注意的地方:由于树的特点,我们统计答案必须从子节点向其父亲结点转移。
这也就要求我们将移动方向转动一下,即:

移动到T时,tot[ dep[对应S<虚点>] ]++;
移动到LCA时,tot[ dep[对应S<虚点>] ]--

然后剩下的做法与向上路径完全相同。


具体实现代码(内详细注释):

#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#define IL inline
#define maxn 300000
#define BKS 300000   //防止出现负下标所加的大数
using namespace std;

inline int gi(){
    int date = 0,m = 1; char ch = 0;
    while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();
    if(ch == '-'){m = -1 ; ch = getchar();}
    while('0'<=ch && ch<='9'){date = date * 10 + ch - '0';ch = getchar();}
    return date * m;
}
inline void write(int ac,int deep){
    if(ac>9)write(ac/10,deep+1);
    putchar(ac%10+'0');  if(!deep)putchar(' ');
}

int head[maxn],cnt;                     //建图链式前向星
struct Road{int to,next;}t[2*maxn+2];   //建图粮食前向星
vector<int>Insert[maxn],Delete[maxn];   //每个结点所挂的对应起点位置(添加、删除)
int fa[maxn][30],dis[maxn],dep[maxn];   //倍增找父亲,距离根节点的距离,所在深度
int tot[2*maxn+5];                      //第i层为起点的玩家个数
int W[maxn],Ans[maxn],N,M;              //题中所述w,第i个结点的答案,题中所述N、M

struct Player
{
    int S,T,lca,len;
    IL void Read(){S = gi(); T = gi();}
    IL void LCA()                         //求解S、T的LCA
    {
        int f1 = S,f2 = T;
        if(dep[f1]<dep[f2])swap(f1,f2);
        for(int i = log2(dep[f1]+1); i>=0 ; i--)
            if( (1<<i)&(dep[f1]-dep[f2]) )f1 = fa[f1][i];
        if(f1 == f2){lca = f1; return;}
        for(int i = log2(f1)+1; i >= 0 ; i --){
            if(fa[f1][i] == fa[f2][i])continue;
            f1 = fa[f1][i]; f2 = fa[f2][i];
        }lca = fa[f1][0];
    }
    IL void Getlen(){len = dis[S]+dis[T]-2*dis[lca];} //求解S——>T的距离
}p[maxn];

//倍增顺便求解dis,dep,fa;
void Dfs(int u,int fth,int deep,int Dist)
{
    dis[u] = Dist;
    dep[u] = deep; fa[u][0] = fth;
    for(int i = 1; i <= log2(deep); i ++)
        fa[u][i] = fa[ fa[u][i-1] ][i-1];
    for(int i = head[u];i;i = t[i].next)
    {
        int v = t[i].to;
        if(v == fth)continue;
        Dfs(v,u,deep+1,Dist+1);
    }return;
}

//求解向上路径部分
void DfsUp(int u,int fth)
{
    int tardep = dep[u]+W[u],bef = tot[tardep];    //bef用于去除非当前子树的贡献
    for(int i = head[u];i;i = t[i].next)
    {
        int v = t[i].to;
        if(v == fth)continue;
        DfsUp(v,u);
    }
    for(int i = 0; i < Insert[u].size(); i ++)tot[Insert[u][i]]++;  //以u为起点的加入贡献中
    Ans[u] = Ans[u] + tot[tardep] - bef;                            //统计贡献
    for(int i = 0; i < Delete[u].size(); i ++)tot[Delete[u][i]]--;  //去除以u为终点的贡献
}

//求解向下路径部分( T时++,LCA时--,注意从下往上统计答案 )
void DfsDown(int u,int fth)
{
    int tardep = dep[u]-W[u]+BKS,bef = tot[tardep];   //+BKS:建立一个虚点
    for(int i = head[u];i;i = t[i].next)
    {
        int v = t[i].to;
        if(v == fth)continue;
        DfsDown(v,u);
    }
    for(int i = 0; i < Insert[u].size(); i ++)tot[Insert[u][i]+BKS]++;
    Ans[u] = Ans[u] + tot[tardep] - bef;
    for(int i = 0; i < Delete[u].size(); i ++)tot[Delete[u][i]+BKS]--;
}


int main()
{
    N = gi(); M = gi();
    for(int i = 1; i <= N-1; i ++)
    {
        int u = gi(),v = gi();
        t[++cnt] = (Road){v,head[u]}; head[u] = cnt;
        t[++cnt] = (Road){u,head[v]}; head[v] = cnt;
    }
    for(int i = 1; i <= N ; i ++)W[i] = gi();
    Dfs(1,0,1,0);
    for(int i = 1; i <= M ; i ++){
        p[i].Read();
        p[i].LCA();
        p[i].Getlen();
    }
    //Step1: 处理向上的路径:
    for(int i = 1; i <= N ; i ++){
        Insert[i].clear();Delete[i].clear();}
    memset(tot,0,sizeof(tot));
    for(int i = 1; i <= M ; i ++)
    {
        Insert[p[i].S].push_back( dep[p[i].S] );           //计算每个点所挂的加入内容
        Delete[p[i].lca].push_back( dep[p[i].S] );         //计算每个点所挂的删除内容
    } 
    DfsUp(1,0);
    //Step2: 处理向下的路径:
    for(int i = 1; i <= N ; i ++){
        Insert[i].clear();Delete[i].clear();}
    memset(tot,0,sizeof(tot));
    for(int i = 1; i <= M ; i ++)
    {
        Insert[p[i].T].push_back( dep[p[i].T]-p[i].len );    //计算每个点所挂的加入内容
        Delete[p[i].lca].push_back( dep[p[i].T]-p[i].len );  //计算每个点所挂的删除内容
    }
    DfsDown(1,0);
    //Step3:LCA在分解为上下时 都算了一遍,去重:
    for(int i = 1;i <= M ; i ++)
        if(dep[p[i].lca]+W[p[i].lca] == dep[p[i].S])
            Ans[p[i].lca]--;
    //Step4:输出答案啦......
    for(int i = 1;i <= N ; i ++)write(Ans[i],0);
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值