题目类型: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] 天天爱跑步 「树上差分」的更多相关文章

  1. [luogu1600 noip2016] 天天爱跑步 (树上差分)

    题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是一个养成类游戏,需要玩家每天按时上线,完成打卡任务. 这个游戏的地图可以看作一一棵 ...

  2. [NOIP2016]天天爱跑步 题解(树上差分) (码长短跑的快)

    Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是一个养成类游戏,需要 玩家每天按时上线,完成打卡任务.这个游戏的地图 ...

  3. bzoj 4719: [Noip2016]天天爱跑步【树上差分+dfs】

    长久以来的心理阴影?但是其实非常简单-- 预处理出deep和每组st的lca,在这里我简单粗暴的拿树剖爆算了 然后考虑对于一组s t lca来说,被这组贡献的观察员x当且仅当: x在s到lca的路径上 ...

  4. NOIP2016 天天爱跑步(树上差分)

    题意 给定一棵树,从时刻 0 开始,有若干人从 S[i] 出发向 T[i] 移动,每单位时刻移动一条边 对于树上每个点 x,求 w[x]  时刻有多少人恰好路过 x N,M≤300000 题解 从上午 ...

  5. 【NOIP2016】天天爱跑步(树上差分)

    题意: 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.?天天爱跑步?是一个养成类游戏,需要 玩家每天按时上线,完成打卡任务.这个游戏的地图可以看作一一棵包含 N个结点 ...

  6. LOJ2359. 「NOIP2016」天天爱跑步【树上差分】

    LINK 思路 首先发现如果对于一个节点,假设一个节点需要统计从字数内来的贡献 需要满足\(dep_u - dep_s = w_u\) 这个条件其实可以转化成\(dep_u - w_u = dep_s ...

  7. P1600 [NOIP2016 提高组] 天天爱跑步 (树上差分)

    对于一条路径,s-t,位于该路径上的观察员能观察到运动员当且仅当以下两种情况成立:(d[ ]表示节点深度) 1.观察员x在s-lca(s,t)上时,满足d[s]=d[x]+w[x]就能观察到,所以我们 ...

  8. [NOIp2016]天天爱跑步 线段树合并

    [NOIp2016]天天爱跑步 LG传送门 作为一道被毒瘤出题人们玩坏了的NOIp经典题,我们先不看毒瘤的"动态爱跑步"和"天天爱仙人掌",回归一下本来的味道. ...

  9. [Noip2016]天天爱跑步 LCA+DFS

    [Noip2016]天天爱跑步 Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.?天天爱跑步?是一个养成类游戏,需要玩家每天按时上线,完成打卡任 ...

随机推荐

  1. ASP.Net Core 运行错误 Http Error 502.5 解决办法

    Http Error 502.5 - Process Failure 如果你看到上面这张图片了的话,说明你在本地运行的时候报错了. 尤其好多都是我的群友,说下情况. 这个一般是本地的.NET Core ...

  2. en

    发音,这个应该算是学习英语的头等大事,如果没有机会和条件练好发音,也可以先将就着,不过后面你就会感觉到你说的人家可能会听不懂,我自己也曾经深受其害. 基本常用单词积累(大概2000~4000左右的词汇 ...

  3. Leetcode 143. Reorder List(Medium)

    Given a singly linked list L: L0→L1→…→Ln-1→Ln,reorder it to: L0→Ln→L1→Ln-1→L2→Ln-2→… You must do thi ...

  4. [LeetCode] Rank Scores -- 数据库知识(mysql)

    Write a SQL query to rank scores. If there is a tie between two scores, both should have the same ra ...

  5. Randomized Online PCA Algorithms with Regret Bounds that are Logarithmic in the Dimension

    目录 Setup of Batch PCA and Online PCA Hedge Algorithm 改进算法 用于矩阵 \(rounding()\) 前俩次,都用到了\(rounding()\) ...

  6. Python迭代器与格式化

    三元运算 对if-else判断的简写 >>> age = 18 >>> res = "You are so young!" if age < ...

  7. 不能再忽视了!宝宝不肯吃粥的N个原因,你避免了几个?

    辅食不懂怎么添加? 宝宝吃饭爱挑食? 营养均衡和多样化的辅食 在这里你都能找到 宝宝辅食微课堂 不能再忽视了!宝宝不肯吃粥的N个原因,你避免了几个? 2017-10-09 09:35 辅食不懂怎么添加 ...

  8. python的UnboundLocalError: local variable 'xxx' referenced b

    一.意思: 本地变量xxx引用前没定义. 二.错误原因     在于python没有变量的声明 , 所以它通过一个简单的规则找出变量的范围 :如果有一个函数内部的变量赋值 ,该变量被认为是本地的,所以 ...

  9. 多线程系列之六:Producer-Consumer模式

    一,Producer-Consumer模式 Producer:生产者的意思,指的是生成数据的线程.Consumer:消费者的意思,指的是使用数据的线程当生产者和消费者以不同的线程运行时,两者之间的处理 ...

  10. PHP中友好的处理方式

    在使用PHP进行开发的时候,由于PHP是弱类型语言的特性,所以,偶尔会遇到一些意想不到的错误.规范我们的编程就变得尤为重要了.下面总结一下,我日常开发中的一些经验,可能有些地方不妥,还请多多斧正,指教 ...