Noip 2016 天天爱跑步 【树上倍增+深搜】

题目:

 

小c同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。«天天爱跑步»是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。

这个游戏的地图可以看作一一棵包含 个结点和 条边的树, 每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从的连续正整数。

现在有个玩家,第个玩家的起点为 ,终点为 。每天打卡任务开始时,所有玩家在第秒同时从自己的起点出发, 以每秒跑一条边的速度, 不间断地沿着最短路径向着自己的终点跑去, 跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树, 所以每个人的路径是唯一的)

小C想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点的观察员会选择在第秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第秒也理到达了结点 。 小C想知道每个观察员会观察到多少人?

注意: 我们认为一个玩家到达自己的终点后该玩家就会结束游戏, 他不能等待一 段时间后再被观察员观察到。 即对于把结点作为终点的玩家: 若他在第秒重到达终点,则在结点的观察员不能观察到该玩家;若他正好在第秒到达终点,则在结点的观察员可以观察到这个玩家。

分析:

此题题意明确,要求也不多,很容易想到许许多多暴力方法,也都能得到部分分。但如果想要AC,就必须使用一下几个突破口:

1、求两点的路径时,很容易想到lca,即两点互通时必经之地,方程(一定要用倍增):

lca=Lca(a,b);
len=dep[a]+dep[b]-2*dep[lca];//拆成两条路径

2、求两点间路径上的合法点时,用num[0][x]记录路径经过当前结点,且节点深度为x的合法起点节点数目;用num[1][x]记录路径经过当前结点,且节点深度为x的合法终点节点数目;那么如何能在根节点x仅仅利用w[x]和dep[x]求出合法的起点和终点呢?其实有这样一个技巧:

首先,一个路径(从u到v)能经过x的条件是:

从起点的角度上:dep[u]-w[x]=dep[x] ==> dep[x]+w[x]=dep[u]

从终点的角度上: len(u,v)-w[x]=dep[v]-dep[x] ==> w[x]-dep[x]=len(u,v)-dep[v]

我们可以看到,等式经整理后,左边全是关于x的,右边全是关于起点和终点的,于是就会惊喜地想到:在把每个起点通过建图联系起来,把每个起点通过建图联系起来,把它们的“深度”处理成红色字,在遍历每一棵树到x时都调用蓝色字就可以找到合法的起点和终点了。

还有以下的注意事项:

1、如果我们要求经过当前结点的路径上的合法节点的数目,显然不能直接用num[0][x]加上num[1][x],因为会有一些相同深度的节点不属于同一棵子树,会加多。那么,我们采取差量法,先记录num[0][x]+num[1][x],经过一番dfs子树后,再看num[0][x]+num[1][x]的变量,变化量就是子树的合法节点数量。

2、其实其中利用了差分的思想,每次在子节点的num[i][0],num[i][1]和num[j][0],num[j][1]分别加1,在退出时在根节点的num[x][0],num[x][1]分别减1。

下面是参考代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int lim=100000000;
const int maxn=600010;
struct point
{
    int to;
    int nxt;
}edge[maxn*2];
int n,Q,tot=0;
int dep[maxn]={0},cnt[2]={0},num[2][maxn*2],sum[maxn],f[20][maxn];
bool vis[maxn];
int w[maxn],vnxt[3][maxn*2],vlas[3][maxn*2],vfir[3][maxn*2],vv[3][maxn*2],head[maxn];

void add(int u,int v)
{
    tot++;
    edge[tot].to=v;
    edge[tot].nxt=head[u];
    head[u]=tot;
}
//        哪个点 起/终 所谓的深度 
void v_add(int x,int s,int v)//奇怪的联系起点(终点)们的方式 
{
    int k=++cnt[s];//k表示是第几个起点/终点 
    vv[s][k]=v;
    vnxt[s][k]=0;
    if(vfir[s][x])
        vnxt[s][vlas[s][x]]=k;
    else
        vfir[s][x]=k;
    vlas[s][x]=k;
}

void work(int x)
{
    int s1=num[0][dep[x]+w[x]+maxn];//没遍历子树之前的值 
    int s2=num[1][w[x]-dep[x]+maxn];
    for(int j=0;j<2;j++)//统计合法起点/终点 
        for(int i=vfir[j][x];i;i=vnxt[j][i])
        {
            int dd=vv[j][i];
            if(dd<=lim)
                num[j][dd]++;
        }
    for(int i=head[x];i;i=edge[i].nxt)//遍历子树 
    {
        int v=edge[i].to;
        if(vis[v]) continue;
        vis[v]=1;
        work(v);
    }
    for(int j=0;j<2;j++)//减去给lca的重复计数 
        for(int i=vfir[j][x];i;i=vnxt[j][i])
        {
            int dd=vv[j][i];
            if(dd>lim)
                num[j][dd-lim]--;
        }
    sum[x]+=num[0][dep[x]+w[x]+maxn]-s1;//差值才是真正的子树贡献值 
    sum[x]+=num[1][w[x]-dep[x]+maxn]-s2;
}

int Lca(int x,int y)//lca的老样子 
{
    int temp,co=0,lca;
    int tx=x,ty=y;
    if(dep[tx]>dep[ty]) swap(tx,ty);
    temp=dep[ty]-dep[tx];
    while(temp)
    {
        if(temp&1) ty=f[co][ty];
        temp>>=1;
        co++;
    }
    if(tx==ty) return tx;
    else
    {
        for(int j=19;j+1;j--)
        {
            if((1<<j)>dep[tx]) continue;
            if(f[j][tx]!=f[j][ty])
            {
                tx=f[j][tx];
                ty=f[j][ty];                
            }
        }
        lca=f[0][tx];
    }
    return lca;
}

int main()
{
    memset(head,0,sizeof(head));
    cin>>n>>Q;
    for(int i=1;i<=n-1;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);//双向建图 
        add(b,a);
    }
    for(int i=1;i<=n;i++)
        cin>>w[i];
    /*input over*/
    queue<int> q;
    vis[1]=1;
    dep[1]=0;
    q.push(1); 
    while(!q.empty())//宽搜处理dep和f[0][i] (父节点) 
    {
        int tt=q.front();
        q.pop();
        for(int i=head[tt];i;i=edge[i].nxt)
        {
            int v=edge[i].to;
            if(!vis[v])
            {
                vis[v]=1;
                q.push(v);
                dep[v]=dep[tt]+1;
                f[0][v]=tt; 
            }
        }
    }
    for(int j=1;j<20;j++)//倍增的老样子 
        for(int i=1;i<=n;i++)
        if(dep[i]>=(1<<j))
        f[j][i]=f[j-1][f[j-1][i]];
    while(Q--)
    {
        int a,b,lca,len;
        cin>>a>>b;
        if(a==b)//终点起点都一样 
        {
            if(w[a]==0)
                sum[a]++;
            continue;            
        }
        lca=Lca(a,b);
        len=dep[a]+dep[b]-2*dep[lca];//lca的老样子 
        if(dep[a]-dep[lca]==w[lca])//直接处理出根节点 
            sum[lca]++;
        if(a!=lca)
        {
            v_add(a,0,maxn+dep[a]);//给起点建图 
            v_add(lca,0,maxn+dep[a]+lim);//为后面减去lca的num准备 
        }
        if(b!=lca)
        {
            v_add(b,1,maxn+len-dep[b]);//给终点建图 
            v_add(lca,1,maxn+len-dep[b]+lim);//为后面减去lca的num准备 
        }
    }
    
    memset(vis,0,sizeof(vis));
    vis[1]=1;
    work(1);
    //通过遍历一个个子树,以深度为对象统计出每个节点作为根时的合法起点和终点 
    for(int i=1;i<=n;i++)
        cout<<sum[i]<<" ";
    return 0;
}
原文地址:https://www.cnblogs.com/linda-fcj/p/7223458.html