[NOIP2016提高组]天天爱跑步

题目:洛谷P1600、BZOJ4719、Vijos P2004、UOJ#261。

题目大意:一些人在一棵树上,每秒移动一个单位,从起点移动到终点。每个节点上有一个观测员,第i个能观测到w[i]秒刚好到这个节点的人。问每个观测员能观测到几个人。

解题思路:NOIP2016提高最难的一题啊!虽说80分很容易拿,但想AC却真的很难!w(゚Д゚)w

做法基本就这么两种:①树链剖分;②LCA+树上差分。

树剖我掌握得并不熟练(我仅用它写过LCA而且已经忘了),所以我只能用②了。

先讲思路。首先,对于每个人走的路径s->t,可拆分为s->lca(s,t)和lca(s,t)->t。我们对其分别考虑。

对于s->lca(s,t)之间任何一个点i,它能观测到当前这个人必须满足$deep[s]-w[i]=deep[i]$(其中deep[i]表示节点i到根节点的深度,下同),移项得$deep[s]=deep[i]+w[i]$,这样左边只和s有关,右边只和i有关。

再来考虑lca(s,t)->t之间任何一个点i,它能观测到当前这个人必须满足$deep[t]-deep[i]=len-w[i]$(len表示当前这个人的路径的总长度),移项得$deep[t]-len=deep[i]-w[i]$,这样左边只和t有关,右边只和i有关。

然后分别对两种路径进行差分。dfs遍历树,然后当前节点为i,我们用一个桶(tong)统计。tong的下标即为等式右边的表达式(用i即可算出),记录的即为等式左边的表达式(统计答案)。

由于$deep[t]-len=deep[i]-w[i]$中,等式左、右可能出现负数,只需把桶的下标统一往后移300000即可(Pascal请忽略)。

最后,如果一个人在lca上被统计到,那么它在s->lca(s,t)和lca(s,t)->t中都被统计了一遍,需要减去。

时间复杂度:Tarjan求LCA则$O(n+m)$,倍增等求LCA则$O(m\log_2 n)$,差分是$O(n)$的。我选用速度较快的Tarjan求LCA。

为方便大家(我)理解,我在代码核心部分增添了一些注释。

C++ Code:

#include<cstdio>
#include<cstring>
#include<vector>
#include<cctype>
using namespace std;
#define C c=getchar()
#define N 300010
int n,m,ne=0,nq=0,maxdeep=0;
bool vis[N];
int f[N],head[N],que[N],w[N],deep[N],mk[N],ans[N],tong[N<<1];
vector<int>js[N],js2[N],js3[N];//用于差分。由于直接开数组会炸,而实际要用的空间不多,所以用vector 
struct query{
    int same,nxt,to,num;
    bool flag;
}q[N<<1];
struct edge{
    int to,nxt;
}e[N<<1];
struct men{
	int x,y,len,lca;
}a[N];
inline int readint(){
    char C;
    for(;!isdigit(c);C);
    int d=0;
    for(;isdigit(c);C)
    d=(d<<3)+(d<<1)+(c^'0');
    return d;
}
inline void add_edge(int x,int y){
    e[++ne].to=y;
    e[ne].nxt=head[x];
    head[x]=ne;
    e[++ne].to=x;
    e[ne].nxt=head[y];
    head[y]=ne;
}
inline void add_que(int x,int y,int z){
    q[++nq].to=y;
    q[nq].same=nq+1;
    q[nq].num=z;
    q[nq].nxt=que[x];
    que[x]=nq;
    q[++nq].to=x;
    q[nq].same=nq-1;
    q[nq].num=z;
    q[nq].nxt=que[y];
    que[y]=nq;
}
int find(int x){
    if(f[x]==x)return x;
    return f[x]=find(f[x]);
}
void tarjan(int root){
    for(int i=head[root];i;i=e[i].nxt){
        int v=e[i].to;
        if(deep[v])continue;
        if(maxdeep<(deep[v]=deep[root]+1))maxdeep=deep[v];//求每个点到根节点的深度和树的最大深度 
        tarjan(v);
        f[v]=root;
        vis[v]=true;
    }
    for(int i=que[root];i;i=q[i].nxt)
    if(vis[q[i].to]&&!q[i].flag){
    	int p=q[i].num;
        a[p].len=deep[a[p].x]+deep[a[p].y]-(deep[a[p].lca=find(q[i].to)]<<1);//顺便算出每个人跑的总长度 
        q[i].flag=q[q[i].same].flag=true;
    }
}//Tarjan求LCA 
void dfs1(int rt){//deep[s]=deep[i]+w[i]
	int now=deep[rt]+w[rt],pre;//now记录等式右边,tong统计等式左边。pre记录开始时的tong[now] 
	if(now<=maxdeep)pre=tong[now];//当now超过最大深度则无需计算 
	for(int i=head[rt];i;i=e[i].nxt)
	if(deep[rt]<deep[e[i].to])dfs1(e[i].to);
	tong[deep[rt]]+=mk[rt];
	if(now<=maxdeep)ans[rt]+=tong[now]-pre;//当now超过最大深度则无需计算,否则更新答案
	//由于tong[now]在深搜过程中已经变化,那么变化的值就是可被观测到的人数。 
	for(int i=js[rt].size()-1;i>=0;--i)--tong[js[rt][i]];//把lca为当前节点的减去 
}
void dfs2(int rt){//deep[t]-len=deep[i]-w[i]
	int now=deep[rt]-w[rt]+300000,pre;//now、tong、pre同上 
	pre=tong[now];
	for(int i=head[rt];i;i=e[i].nxt)
	if(deep[rt]<deep[e[i].to])dfs2(e[i].to);
	for(int i=js2[rt].size()-1;i>=0;--i)++tong[js2[rt][i]+300000];//加上等式左边的东西 
	ans[rt]+=tong[now]-pre;//原理同上 
	for(int i=js3[rt].size()-1;i>=0;--i)--tong[js3[rt][i]+300000];//把无用节点减去 
}
int main(){
    n=readint(),m=readint();
    memset(vis,0,sizeof vis);
    memset(deep,0,sizeof deep);
    memset(ans,0,sizeof ans);
    deep[1]=1;
    for(int i=1;i<n;i++){
        int u=readint(),v=readint();
        add_edge(u,v);
    }
    for(int i=1;i<=n;++i)w[f[i]=i]=readint();
    for(int i=1;i<=m;i++){
    	a[i].x=readint();
    	a[i].y=readint();
    	if(a[i].x!=a[i].y)
		add_que(a[i].x,a[i].y,i);else{
			a[i].len=0;
			a[i].lca=a[i].x;
		}
	}//保存LCA询问,Tarjan 
    tarjan(1);
    memset(tong,0,sizeof tong);
    memset(mk,0,sizeof mk);//mk[i]表示第i个节点作为起点多少次 
    for(int i=1;i<=m;++i){
    	++mk[a[i].x];
    	js[a[i].lca].push_back(deep[a[i].x]);//js[i]存储lca为i的人的起点的深度 
    }
    dfs1(1);//处理s->lca(s,t)路径 
    memset(tong,0,sizeof tong);
    for(int i=1;i<=m;++i){
    	int f=deep[a[i].y]-a[i].len;
    	js2[a[i].y].push_back(f);//js2[i]存储终点为i的人的终点的深度减去这个人走的总长 
    	js3[a[i].lca].push_back(f);//js3[i]存储lca为i的人的终点的深度减去这个人走的总长
    	//这两个数组用来差分 
    }
    dfs2(1);
    for(int i=1;i<=m;++i)
    if(deep[a[i].x]-deep[a[i].lca]==w[a[i].lca])--ans[a[i].lca];
    //当lca(s,t)正好可以观测到这个人时,它在s->lca(s,t)和lca(s,t)->t都会被统计一次,所以去重。
	for(int i=1;i<n;++i)printf("%d ",ans[i]);
	printf("%d\n",ans[n]);
    return 0;
}

在BZOJ上PE是什么鬼!?

posted @ 2017-10-18 18:59  Mrsrz  阅读(534)  评论(0编辑  收藏  举报