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

时间:2022-09-28 09:49:05

题目:

 

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

这个游戏的地图可以看作一一棵包含 Noip    2016      天天爱跑步    【树上倍增+深搜】个结点和 Noip    2016      天天爱跑步    【树上倍增+深搜】条边的树, 每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从Noip    2016      天天爱跑步    【树上倍增+深搜】Noip    2016      天天爱跑步    【树上倍增+深搜】的连续正整数。

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

小C想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点Noip    2016      天天爱跑步    【树上倍增+深搜】的观察员会选择在第Noip    2016      天天爱跑步    【树上倍增+深搜】秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第Noip    2016      天天爱跑步    【树上倍增+深搜】秒也理到达了结点 Noip    2016      天天爱跑步    【树上倍增+深搜】 。 小C想知道每个观察员会观察到多少人?

注意: 我们认为一个玩家到达自己的终点后该玩家就会结束游戏, 他不能等待一 段时间后再被观察员观察到。 即对于把结点Noip    2016      天天爱跑步    【树上倍增+深搜】作为终点的玩家: 若他在第Noip    2016      天天爱跑步    【树上倍增+深搜】秒重到达终点,则在结点Noip    2016      天天爱跑步    【树上倍增+深搜】的观察员不能观察到该玩家;若他正好在第Noip    2016      天天爱跑步    【树上倍增+深搜】秒到达终点,则在结点Noip    2016      天天爱跑步    【树上倍增+深搜】的观察员可以观察到这个玩家。

分析:

此题题意明确,要求也不多,很容易想到许许多多暴力方法,也都能得到部分分。但如果想要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;
}