题目类型: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. Yii1.1框架关于日志的配置的简单使用

    最近开始接触新项目,新项目用的框架是Yii1.1版本的,通过看框架文档大致熟悉了解了Yii在日志方面的使用. 首先在protected/config/main.php配置文件中加入日志相关配置,如下图 ...

  2. Misha, Grisha and Underground CodeForces - 832D (倍增树上求LCA)

    Misha and Grisha are funny boys, so they like to use new underground. The underground has n stations ...

  3. Unique Snowflakes UVA - 11572 (离散化+尺取法)

    Emily the entrepreneur has a cool business idea: packaging and selling snowflakes. She has devised a ...

  4. CMD管道命令使用

    Windows netstat 查看端口.进程占用 开始--运行--cmd 进入命令提示符 输入netstat -ano 即可看到所有连接的PID 之后在任务管理器中找到这个PID所对应的程序如果任务 ...

  5. IdentityServer4【Topic】之授权类型

    Grant Types 授权类型 授权类型指出了一个客户端如何与IdentityServer进行交互.OpenID Conect和OAuth2.0定义了如下的授权类型: Implicit Author ...

  6. C#设计模式之2:单例模式

    在程序的设计过程中很多时候系统会要求对于某个类型在一个应用程序域中只出现一次,或者是因为性能的考虑,或者是由于逻辑的要求,总之是有这样的需求的存在,那在设计模式中正好有这么一种模式可以来满足这样的要求 ...

  7. java程序员一些初中级面试题(数据库部分)

    说出一些数据库优化方面的经验? 1.从JDBC编程的角度讲,用PreparedStatement一般来说比Statement性能高,因为在使用时,SQL语句被预编译并存储在PreparedStatem ...

  8. [转帖]FORFILES 的简单介绍。

    FORFILES https://blog.csdn.net/sandy9919/article/details/82932460 命令格式: forfiles.exe /p "D:\备份& ...

  9. Day 5-8 自定义元类控制类的实例化行为

    __call__方法: 对象后面加括号,触发执行. 注:构造方法的执行是由创建对象触发的,即:对象 = 类名() :而对于 __call__ 方法的执行是由对象后加括号触发的,即:对象() 或者 类( ...

  10. 手机端图像编辑上传-cropper

    编辑头像,实现相册,照像功能,并能缩放裁剪功能,可自定义UI,引用'cropper.js', 'exif.js' /*初始化裁剪插件*/ var screenWidth = $(window).wid ...