NOIP2016——天天爱跑步(树上差分)

Description
小c同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。天天爱跑步是一个养成类游戏,需要
玩家每天按时上线,完成打卡任务。这个游戏的地图可以看作一一棵包含 N个结点和N-1 条边的树, 每条边连接两
个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从1到N的连续正整数。现在有个玩家,第个玩家的
起点为Si ,终点为Ti 。每天打卡任务开始时,所有玩家在第0秒同时从自己的起点出发, 以每秒跑一条边的速度,
不间断地沿着最短路径向着自己的终点跑去, 跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树, 所以
每个人的路径是唯一的)小C想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点的观察员会选
择在第Wj秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第Wj秒也理到达了结点J 。 小C想知道
每个观察员会观察到多少人?注意: 我们认为一个玩家到达自己的终点后该玩家就会结束游戏, 他不能等待一 段时
间后再被观察员观察到。 即对于把结点J作为终点的玩家: 若他在第Wj秒重到达终点,则在结点J的观察员不能观察
到该玩家;若他正好在第Wj秒到达终点,则在结点的观察员可以观察到这个玩家。
Input
第一行有两个整数N和M 。其中N代表树的结点数量, 同时也是观察员的数量, M代表玩家的数量。
接下来n-1 行每行两个整数U和V ,表示结点U 到结点V 有一条边。
接下来一行N 个整数,其中第个整数为Wj , 表示结点出现观察员的时间。
接下来 M行,每行两个整数Si和Ti,表示一个玩家的起点和终点。
对于所有的数据,保证 。
1<=Si,Ti<=N,0<=Wj<=N
Output
输出1行N 个整数,第个整数表示结点的观察员可以观察到多少人。
Sample Input
6 3
2 3
1 2
1 4
4 5
4 6
0 2 5 1 2 3
1 5
1 3
2
Sample Output
1 2 1 0 1
样例解释
对于1号点,Wi=0,故只有起点为1号点的玩家才会被观察到,所以玩家1和玩家2被观察到,共有2人被观察到。
对于2号点,没有玩家在第2秒时在此结点,共0人被观察到。
对于3号点,没有玩家在第5秒时在此结点,共0人被观察到。
对于4号点,玩家1被观察到,共1人被观察到。
对于5号点,玩家1被观察到,共1人被观察到。
对于6号点,玩家3被观察到,共1人被观察到。
在这里插入图片描述

这道题其实做了后会发现思想很简单,虽然代码十分难以实现

中心思想只需要一个差分就可以了

我们首先看前20分,随便瞎搞都可以做

然后第25分暴力枚举每个玩家的路径就是了

前40分用线段树或者树状数组维护区间就是了

然后我们考虑60分和80分

60分

每条路径都是从上往下的

每个点能观测到人当且仅当dep[i]=w[i]dep[i]=w[i]

那我们只需要记录每个观测点会被经过多少个人

而我们能发现一条路径对一个观察点有贡献时只有可能其终点在该观察点的子树中

所以我们对于每一条路径在其终点+1

则对于每个点满足dep[i]=w[i]dep[i]=w[i]的话(也就是能观察到人)

我们只需要统计其子树和

就是答案了

80分

每条路径都是从下往上的

那么对于一个观测点 i,它能观测到的点 j 必须要满足w[i]=dep[j]dep[i]w[i]=dep[j]-dep[i];

这个很好理解吧

变形一下 w[i]+dep[i]=dep[j]w[i]+dep[i]=dep[j]

也就是说对于每一个观察点ii

其子树中只有起点深度为dep[i]+w[i]dep[i]+w[i]的路径才会对其有贡献

类似60分的做法,给每个路径的终点ii打上一个值为dep[i]dep[i]标记

所以我们只需要维护每个点子树中所有起点的深度

对于每一个节点,找到其子树值为dep[i]+w[i]dep[i]+w[i]的标记的个数

这用桶就可以维护了

当然我们不可能维护1e5个桶

我们可以从根dfs

每dfs到一个节点

就先记录一下现在桶里面满足条件的点的个数

然后继续往儿子递归

每次递归完一个点就把在这个点的所有标记全部丢入桶中

然后回溯的时候就得到了包括其子树的所有标记

然后统计一下现在满足条件的标记数量

相减一下就是其子树中满足条件的标记的个数,也就是这个点的答案

正解

其实如果会80分做法基本上就会AC做法了

观察60分和80分的做法

其实里面给了我们许多提示

60分是从上往下走

80分是从下往上走

这在启示我们可以把一条路径拆开

对于每个路径u–>v

可以把它拆分成u–>lca(u,v)lca(u,v)lca(u,v)lca(u,v)–>v两条路径的

那么我们对每条路径单独维护

同时60分的做法暗示我们对于路径的两个节点打上标记

统计子树

而80分的暗示了我们要用桶来维护子树标记

下面切入正题

首先对于u–>lca(u,v)这条路径:

因为当一个观测点ii能观测到一个从jj出发的人当且仅当w[i]=dep[j]dep[i]w[i]=dep[j]-dep[i]

w[i]+dep[i]=dep[j]w[i]+dep[i]=dep[j]

运用差分的思想

那么对于每一个人jj来说,我们只需要在点jj打上一个dep[j]dep[j]的标记

在其路径起点终点的lca处(下文简称lca)减去一个dep[j]dep[j]的标记、

那么我们对于每个观测点只需要统计其子树内每个值的标记的个数,用桶来维护

那么这个观测点的ans就是值为w[i]+dep[i]w[i]+dep[i]的标记的个数

这样既可以保证对于路径上的所有点,这条路径的影响会被记录

而且对于lca以上的所有点没有影响

那我们对于每个点,都记录一下它被打上的两种标记

暴力加减就是了

对于lca–>v其实也是一样的

只不过这里是需要满足len(u,v)(dep[v]dep[j])=w[j]len(u,v)-(dep[v]-dep[j])=w[j]

变形之后就是dep[j]+w[j]=len(u,v)dep[v]dep[j]+w[j]=len(u,v)-dep[v]

那么我们一样的打一个值为(len(u,v)dep[v])(len(u,v)-dep[v])的标记

又可以得到len(u,v)=dep[u]+dep[v]2lca(u,v)len(u,v)=dep[u]+dep[v]-2*lca(u,v)

化简一下就是dep[v]2lca(u,v)dep[v]-2*lca(u,v)的标记

然后同样暴力统计就是了

还有注意两条路径一个在lca打标记,一个要在fa[lca]打标记

这样lca处也是有一个标记

不会因为加减标记而漏算

还有,每个点的标记最好用vector存,否则容易爆内存

dfs的复杂度是O(m+n)O(m+n)

因为每个标记只会加、减一次

主要在求lca的时候复杂度是O(mlogn)O(mlogn)

当然也可以tarjan离线O(m+n)O(m+n)

那总复杂度就是O(m+n)的

#include<bits/stdc++.h>
using namespace std;
inline int read(){
 char ch=getchar();
 int res=0;
 while(!isdigit(ch)) ch=getchar();
 while(isdigit(ch)) res=(res<<1)+(res<<3)+(ch^48),ch=getchar();
 return res;
}
const int mor=1926817;
const int N=300005;
const int M=600005;
int cnt,adj[N],nxt[M],to[M],n,m,maxn,dep[N],w[N],f[N][20],ans[N],g1[M*10],g2[M*10];//g1是维护u到lca的标记的桶,g2是维护lca到v的标记的桶 
vector<int> a1[N],a2[N],a3[N],a4[N];//a1是u到lca的+1标记,a2是-1标记,a3是lca到v的+1标记,a4是减1标记 
inline void addedge(int u,int v){
 nxt[++cnt]=adj[u],adj[u]=cnt,to[cnt]=v;
}
inline void Dfs(int u,int fa){
 for(int e=adj[u];e;e=nxt[e]){
  int v=to[e];
  if(v==fa)continue;
  dep[v]=dep[u]+1;
  f[v][0]=u;
  Dfs(v,u);
 }
}
inline void init(){
 for(int j=1;j<=19;j++){
  for(int i=1;i<=n;++i){
   if(f[i][j-1]) f[i][j]=f[f[i][j-1]][j-1];
  }
 }
}
inline int lca(int x,int y){//倍增求lca 
 if(dep[x]<dep[y]) swap(x,y);
 int len=dep[x]-dep[y];
 for(int i=19;~i;i--) if(len>=(1<<i)) len-=1<<i,x=f[x][i];
 if(x==y) return x;
 for(int i=19;~i;i--) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
 return f[x][0];
}
inline void dfs(int u,int fa){
 int d1=dep[u]+w[u],d2=w[u]-dep[u],lg=g1[d1],lf=g2[d2+mor];//mor:给下标加一个较大的值,防止dep[u]-2*dep[lca]为负爆下标的情况 
 for(int e=adj[u];e;e=nxt[e]){
  int v=to[e];
  if(v==fa)continue;
  dfs(v,u);
 }
 for(int i=0;i<a1[u].size();++i)++g1[a1[u][i]];
 for(int i=0;i<a2[u].size();++i)--g1[a2[u][i]];
 ans[u]+=g1[d1]-lg;//统计u到lca的答案数 
 for(int i=0;i<a3[u].size();++i)++g2[mor+a3[u][i]];
 for(int i=0;i<a4[u].size();++i)--g2[mor+a4[u][i]];
 ans[u]+=g2[d2+mor]-lf;//统计lca到v的答案数 
}
int main(){
 n=read(),m=read();
 for(int i=1;i<n;++i){
  int u=read(),v=read();
  addedge(u,v),addedge(v,u);
 }
 for(int i=1;i<=n;++i)w[i]=read();
 Dfs(1,0);
 init();
 for(int i=1;i<=m;++i){
  int u=read(),v=read();
  int x=lca(u,v);
  a1[u].push_back(dep[u]);
  a2[x].push_back(dep[u]);
  a3[v].push_back((dep[u]-2*dep[x]));
  a4[f[x][0]].push_back(dep[u]-2*dep[x]);//打四种标记 
 }
 dfs(1,0);
 for(int i=1;i<=n;++i){
  cout<<ans[i]<<" ";
 }
 return 0;
}

当然其实这道题做法很多

可以对每个点维护一颗权值线段树

然后做一下线段树合并

或者树套树也是可以的

树剖也可以的

复杂度都O(nlongn)O(nlongn)级别的

原文地址:https://www.cnblogs.com/stargazer-cyk/p/10366486.html