☆ [NOIp2016] 天天爱跑步 「树上差分」
题目类型:LCA+思维
传送门:>Here<
题意:给出一棵树,有\(M\)个人在这棵树上跑步。每个人都从自己的起点\(s[i]\)跑到终点\(t[i]\),跑过一条边的时间为1秒。现在每个节点都有一个观察员,节点\(i\)上的观察员会在第\(W[i]\)秒进行观察,如果有\(x\)个人此时到达节点\(i\),则这个观察员能够观察到\(x\)个人。问所有人跑步结束以后每个观察员可以观察到多少人
解题思路
这道题是公认所有\(NOIp\)中最难的一道题。但其实这道题的数据约定能够给我们很大的提示。
其实这道题的正解就是对各个部分分的方法的汇总和整合。因此我们先一一分析部分分如何拿
子任务一 暴力
由于\(N\)是\(1000\),可以暴力模拟每个人的行动轨迹。求一个\(LCA\)暴力走一下就好了,没有思维难度。这个部分分能够首先让你熟悉题目的意思
子任务二 树退化为链
乍一看很简单,其实由于\(N=99994\),也不是那么容易的。事实上,这一个部分分是整道题的精髓。
由于树已经退化为链,所以所有的路径要么从左往右,要么从右往左。我们可以记录一个桶\(b[x]\)。当我们从左往右扫到\(i\)时,\(b[x]\)表示以\(x\)为起点的所有路径中,能够一直延伸到\(i\)的路径数量。
记录这个有什么用呢?对于节点\(i\),由于路径从左往右,所以如果想要一条路径在走到\(i\)的时候被观察到,那么这条路径的起点\(S\)一定满足:$$S=i-W[i]$$因此我们在考虑节点\(i\)时,只需要考虑以\(i-W[i]\)为起点的能够延伸到\(i\)的路径条数,这些路径一定能够在\(i\)点被观察到。因此我们需要统计的就是\(b[i-W[i]\)。那么从右往左的路径也类似,我们只需要统计\(b[i+W[i]]\)就可以了
那么如何维护\(b\)数组呢?我们可以首先记录每个点的起点个数,然后过了终点以后减去终点对应的那些起点。例如\(2\)开头只有\(3\)条路径:\((2,4) \ (2,3) \ (2,7)\)。我们扫描到\(2\)的时候让\(b[2]+=3\),代表从\(2\)开始能够延伸到\(2\)的路径有\(3\)条。然而扫描到\(4\)的时候事实上只有\(2\)条路径满足了,因此在统计完\(3\)以后应当\(--b[2]\),因为\((2,3)\)这条路径不可能延伸到\(4\)了。但是如果还存在一条路径\((1,3)\)呢?一个终点可能对应多个起点,所以我们需要用一个\(vector\)来维护每个终点对应的所有起点,在扫描到这个终点的时候扫一遍\(vector\)全部减掉
事实上,这就是差分。只不过普通差分只需要统计次数,而不会规定起点的深度。对于这个规定起点深度的问题,自然需要排除深度不正确的起点。所以才会需要用\(vector\)来记录
子任务三 \(S=1\)
我们发现,当\(S=1\)时,所有路径都是从根节点往下。这样的路径有什么特点?对于任意一个点\(u\),由于它的路径一定是从根节点出发的,因此根节点到它的路径长度就是它的深度(根节点深度为0)。也就是说,这要满足\(dep[u]=W[u]\),这个点就是能够被观察到的
那么到底会被观察到几次呢?这就取决于根节点到它有几条路径。或者用更便于统计的方法:\(u\)及它的子树内有多少个终点
子任务四 \(T=1\)
和前者反了一下,但也更巧妙。所有路径都是从下往上到根节点的
我们发现一条路径上的一个点如果要被观察到,首先应该满足$$dep[S]-dep[u]=W[u]$$移项我们发现,也就是$$dep[u]+W[u]=dep[S]$$也就是说我们需要找的答案等价于,在节点\(u\)及其子树中,有多少起点的深度为\(dep[u]+W[u]\)。
联系链状那一部分的做法,其实就是把一维的拓展为了树。由于所有路径都是向上的,也就等价于链状中所有路径都是从右向左的。我们记录一个桶\(b[x]\)表示深度为\(x\)的起点有多少个。因此我们还是一样,每遇到一个起点就++。特别的幸运的是,我们还不需要开一个\(vector\),遇到终点就减去对应起点,因为终点一定是根节点。每一次统计的答案也就是\(b[dep[u]+W[u]]\)
但是有一个问题,我们这样的做法的正确性必须保证是在\(u\)的子树内。然而深度为\(x\)的起点有可能是之前在别的子树内统计的。怎么办?其实我们是从下往上走,也就是要在处理完\(u\)的全部子树以后才来处理\(u\),于是我们其实只需要考虑新增加的部分就可以了。也就是在\(DFS\)下去之前先记录一个\(b[dep[u]+W[u]]\)存为\(tmp\),在\(DFS\)结束以后的答案也就是\(b[dep[u]+W[u]]-tmp\)
附上如上子任务的代码 Code
正解
对于一般性的数据,所有路径一定是从下往上,经过\(LCA\),再从上往下。而数据提示了我们\(S=1\)和\(T=1\)。这就是在暗示我们将路径分开来考虑!
因此,就像链的情况,正解就是需要判断起点深度的树上差分!
首先考虑所有的\((S,LCA)\)的路径。这些路径的特性与\(T=1\)的路径是一样的。都需要满足\(dep[u]+W[u]=dep[S]\),唯一的不同就在于需要像链状一样,维护一个\(vector\)记录终点对应的起点,在过终点时减去即可。注意,这里的终点不是指\(T\),而是\(LCA\)
最最难理解的就是\((LCA,T)\)这一部分了。我们依旧分析路劲的特征。发现被观察到的点一定满足$$W[u]+dep[T]-dep[u]=dis(S,T)$$移项得到$$dep[u]-W[u]=dep[T]-dis(S,T)$$。但是不像之前的\(dep[S]\),这个\(dep[T]-dis(S,T)\)是在树上是没有实际意义的(一定要说有的话,那就是将向上的半截路径翻到上面去的深度)。
于是没有办法,我们需要类比\((S,LCA)\)部分的做法来处理这个部分。我们依然是从下往上做,与之前相反,每遇到一个\(T\)就代表着进入了一条新的路径,每遇到一个\(LCA\)就意味着离开了一条路径。并且我们每遇到一个终点,标记的不是对应起点,而是对应的\(dep[T]-dis(S,T)\);每遇到一个\(LCA\),减去的也是该路径对应的\(dep[T]-dis(S,T)\)。因此我们此时需要两个\(vector\)来存了
综上就是算法的基本思想。这里还有两个细节需要分析:
注意到\(dep[T]-dis(S,T)\)很有可能是个负数,这让\(c++\)开通特别麻烦。一个比较粗暴的方式就是全部一律右移\(MAXN\)
我们将一条路径剖为了\((S,LCA) 和 (LCA,T)\)。如果\(LCA\)恰好是能够被观察到的,不就重复统计了一遍?所以我们需要在最后结算的时候进行修改,如果当前路径的\(LCA\)会被看到,也就是满足$$dep[S]-dep[LCA]=W[LCA]$$那么就应当减去一个\(1\)了
Code
倍增的数组开太小了调试了近一个小时\(qwq\)。
/*By DennyQi 2018*/
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#define r read()
using namespace std;
typedef long long ll;
const int MAXN = 300010;
const int MAXM = 600010;
const int INF = 1061109567;
inline int Max(const int a, const int b){ return (a > b) ? a : b; }
inline int Min(const int a, const int b){ return (a < b) ? a : b; }
inline int read(){
int x = 0; int w = 1; register char c = getchar();
for(; c ^ '-' && (c < '0' || c > '9'); c = getchar());
if(c == '-') w = -1, c = getchar();
for(; c >= '0' && c <= '9'; c = getchar()) x = (x<<3) + (x<<1) + c - '0'; return x * w;
}
int N,M,x,y,len;
int first[MAXM],nxt[MAXM],to[MAXM],cnt;
int f[MAXN][25],dep[MAXN],W[MAXN],s[MAXN],t[MAXN],lca[MAXN],nums[MAXN],b[MAXN],bk[MAXN*2],ans[MAXN];
vector <int> S[MAXN];
vector <int> T1[MAXN], T2[MAXN];
inline void add(int u, int v){
to[++cnt]=v, nxt[cnt]=first[u], first[u]=cnt;
}
void BZ_INIT(int u, int _f, int d){
int v;
dep[u] = d; f[u][0] = _f;
for(int i = 1; (1 << i) <= d; ++i){
f[u][i] = f[f[u][i-1]][i-1];
}
for(int i = first[u]; i; i = nxt[i]){
if((v = to[i]) == _f) continue;
BZ_INIT(v, u, d+1);
}
}
inline int LCA(int a, int b){
if(dep[a] < dep[b]) swap(a, b);
for(int i = 20; i >= 0; --i){
if(dep[a] - (1 << i) >= dep[b]) a = f[a][i];
}
if(a == b) return a;
for(int i = 20; i >= 0; --i){
if(f[a][i] == f[b][i]) continue;
a = f[a][i], b = f[b][i];
}
return f[a][0];
}
void DFS1(int u, int _f){
int v, tmp = b[dep[u] + W[u]];
for(int i = first[u]; i; i = nxt[i]){
if((v = to[i]) == _f) continue;
DFS1(v, u);
}
b[dep[u]] += nums[u];
ans[u] += b[dep[u] + W[u]] - tmp;
for(int i = 0, sz = S[u].size(); i < sz; ++i){
--b[S[u][i]];
}
}
void DFS2(int u, int _f){
int v, tmp = bk[dep[u] - W[u] + MAXN];
for(int i = first[u]; i; i = nxt[i]){
if((v = to[i]) == _f) continue;
DFS2(v, u);
}
for(int i = 0, sz = T1[u].size(); i < sz; ++i){
++bk[T1[u][i] + MAXN];
}
ans[u] += bk[dep[u] - W[u] + MAXN] - tmp;
for(int i = 0, sz = T2[u].size(); i < sz; ++i){
--bk[T2[u][i] + MAXN];
}
}
int main(){
N = r, M = r;
for(int i = 1; i < N; ++i){
x = r, y = r;
add(x, y);
add(y, x);
}
BZ_INIT(1, 0, 0);
for(int i = 1; i <= N; ++i){
W[i] = r;
}
for(int i = 1; i <= M; ++i){
s[i] = r, t[i] = r;
lca[i] = LCA(s[i], t[i]);
len = dep[s[i]] + dep[t[i]] - 2 * dep[lca[i]];
++nums[s[i]];
S[lca[i]].push_back(dep[s[i]]);
T1[t[i]].push_back(dep[t[i]] - len);
T2[lca[i]].push_back(dep[t[i]] - len);
}
DFS1(1, 0);
DFS2(1, 0);
for(int i = 1; i <= M; ++i){
if(dep[s[i]] - dep[lca[i]] == W[lca[i]]){
--ans[lca[i]];
}
}
for(int i = 1; i <= N; ++i){
printf("%d ", ans[i]);
}
return 0;
}
☆ [NOIp2016] 天天爱跑步 「树上差分」的更多相关文章
- [luogu1600 noip2016] 天天爱跑步 (树上差分)
题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是一个养成类游戏,需要玩家每天按时上线,完成打卡任务. 这个游戏的地图可以看作一一棵 ...
- [NOIP2016]天天爱跑步 题解(树上差分) (码长短跑的快)
Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是一个养成类游戏,需要 玩家每天按时上线,完成打卡任务.这个游戏的地图 ...
- bzoj 4719: [Noip2016]天天爱跑步【树上差分+dfs】
长久以来的心理阴影?但是其实非常简单-- 预处理出deep和每组st的lca,在这里我简单粗暴的拿树剖爆算了 然后考虑对于一组s t lca来说,被这组贡献的观察员x当且仅当: x在s到lca的路径上 ...
- NOIP2016 天天爱跑步(树上差分)
题意 给定一棵树,从时刻 0 开始,有若干人从 S[i] 出发向 T[i] 移动,每单位时刻移动一条边 对于树上每个点 x,求 w[x] 时刻有多少人恰好路过 x N,M≤300000 题解 从上午 ...
- 【NOIP2016】天天爱跑步(树上差分)
题意: 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.?天天爱跑步?是一个养成类游戏,需要 玩家每天按时上线,完成打卡任务.这个游戏的地图可以看作一一棵包含 N个结点 ...
- LOJ2359. 「NOIP2016」天天爱跑步【树上差分】
LINK 思路 首先发现如果对于一个节点,假设一个节点需要统计从字数内来的贡献 需要满足\(dep_u - dep_s = w_u\) 这个条件其实可以转化成\(dep_u - w_u = dep_s ...
- P1600 [NOIP2016 提高组] 天天爱跑步 (树上差分)
对于一条路径,s-t,位于该路径上的观察员能观察到运动员当且仅当以下两种情况成立:(d[ ]表示节点深度) 1.观察员x在s-lca(s,t)上时,满足d[s]=d[x]+w[x]就能观察到,所以我们 ...
- [NOIp2016]天天爱跑步 线段树合并
[NOIp2016]天天爱跑步 LG传送门 作为一道被毒瘤出题人们玩坏了的NOIp经典题,我们先不看毒瘤的"动态爱跑步"和"天天爱仙人掌",回归一下本来的味道. ...
- [Noip2016]天天爱跑步 LCA+DFS
[Noip2016]天天爱跑步 Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.?天天爱跑步?是一个养成类游戏,需要玩家每天按时上线,完成打卡任 ...
随机推荐
- c++入门之命名空间存在的意义
看过鸡啄米的C++编程入门系列教程的朋友,应该能注意到,在其中的很多实例中,都有这么一条语句:using namespace std;,即使用命名空间std,其作用就是规定该文件中使用的标准库函数都是 ...
- 关于php,python,javascript文件或者模块导入引入的区别和联系
前言: 我们经常看到编程语言之间,文件或者模块的引来引去的,但是他们在各个编程语言之间有什么区别和联系呢? 1.javascript (1).全局引入方式: <script src='xxxxx ...
- java中流的简单小结
1.分类 按字节流分: InputStream(输出流) OutputStream(输入流) 按字符流分: Reader Writer 提示:输入.输出是站在程序的角度而言,所有输入流是“读 ...
- 什么是IaaS, PaaS和SaaS及其区别
IaaS, PaaS和SaaS是云计算的三种服务模式. . SaaS:Software-as-a-Service(软件即服务)提供给客户的服务是运营商运行在云计算基础设施上的应用程序,用户可以在各种设 ...
- 【问题解决方案】Dev C++ 无法调试的问题与解决
听翁恺老师课的时候用到一个叫DevC++的编辑器. 学到调试部分的时候,老师的没问题我的报错.我?? 试一试网上查到的方法: 工具 --> 编译选项 --> 代码生成/优化 --> ...
- [转][mysql]创建函数失败(1418错误)mysql双主模式导致的问题
https://blog.csdn.net/qq523786283/article/details/75102170
- Ubuntu 12.04 安装socks5代理服务器dante-server
dante-server是一个很好的socks4/5代理服务器软件. 使用apt-get安装 1 apt-getinstall dante-server 添加一个用户 1 2 useradd ...
- 基于redis实现的点赞功能设计思路详解
点赞其实是一个很有意思的功能.基本的设计思路有大致两种, 一种自然是用mysql等 数据库直接落地存储, 另外一种就是利用点赞的业务特征来扔到redis(或memcache)中, 然后离线刷回mysq ...
- IdentityServer4【QuickStart】之利用OpenID Connect添加用户认证
利用OpenID Connect添加用户认证 利用OpenID Connect添加用户认证 在这个示例中我们想要通过OpenID Connect协议将交互用户添加到我们的IdentityServer上 ...
- array_filter、array_walk、array_map的区别
<?php $arr=array( 1,2,3,4,5,6 ); function filter($var){ if($var%2==0) return true; } $data=array_ ...