「NOIP2016」天天爱跑步 题解
(声明:图片来源于网络)
「NOIP2016」天天爱跑步 题解
题目
题目描述
小c同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。
这个游戏的地图可以看作一一棵包含\(n\)个结点和\(n-1\)条边的树,每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从\(1\)到\(n\)的连续正整数。
现在有\(m\)个玩家,第\(i\)个玩家的起点为\(t_i\),终点为\(t_i\) 。每天打卡任务开始时,所有玩家在第\(0\)秒同时从自己的起点出发,以每秒跑一条边的速度,不间断地沿着最短路径向着自己的终点跑去,跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树,所以每个人的路径是唯一的)
小c想知道游戏的活跃度,所以在每个结点上都放置了一个观察员。在结点\(j\)的观察员会选择在第\(w_j\)秒观察玩家,一个玩家能被这个观察员观察到当且仅当该玩家在第\(w_j\)秒也正好到达了结点\(j\)。小c想知道每个观察员会观察到多少人?
注意:我们认为一个玩家到达自己的终点后该玩家就会结束游戏,他不能等待一 段时间后再被观察员观察到。 即对于把结点\(j\)作为终点的玩家:若他在第\(w_j\)秒前到达终点,则在结点\(j\)的观察员不能观察到该玩家;若他正好在第\(w_j\)秒到达终点,则在结点\(j\)的观察员可以观察到这个玩家。
输入格式
第一行有两个整数\(n\)和\(m\)。其中\(n\)代表树的结点数量,同时也是观察员的数量,\(m\)代表玩家的数量。
接下来\(n−1\)行每行两个整数\(u\)和\(v\),表示结点\(u\)到结点\(v\)有一条边。
接下来一行\(n\)个整数,其中第\(j\)个整数为\(w_j\),表示结点\(j\)出现观察员的时间。
接下来\(m\)行,每行两个整数\(s_i\),和\(t_i\),表示一个玩家的起点和终点。
对于所有的数据,保证\(1\leq s_i\),\(t_i\leq n\), \(0\leq w_j\leq n\)。
输出格式
输出\(1\)行\(n\)个整数,第\(j\)个整数表示结点\(j\)的观察员可以观察到多少人。
输入输出样例
输入 #1
6 3
2 3
1 2
1 4
4 5
4 6
0 2 5 1 2 3
1 5
1 3
2 6
输出 #1
2 0 0 1 1 1
输入 #2
5 3
1 2
2 3
2 4
1 5
0 1 0 3 0
3 1
1 4
5 5
输出 #2
1 2 1 0 1
First
首先看这道题,因为题目说了输入的会是一棵数,而在树中,两点间的最短路径为:起点到达他们的lca,再由lca到达终点。
所以先求出这两点之间的lca,这个很明显。(可以用Tarjan,亦可用倍增,本题解使用倍增求解)
C++代码:
#include <cstdio>
#include <vector>
using namespace std;
const int MAXN = 3e5;
vector<int> v[MAXN];
int W[MAXN], de[MAXN], dp[MAXN][32];
bool vis[MAXN];
int n, m;
void Read();
void Write();
void Init();
void dfs(int, int);
int LCA(int, int);
int main() {
Read();
Init();
Write();
return 0;
}
void dfs(int now, int step) {
de[now] = step;
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(!vis[next]) {
vis[next] = true;
dp[next][0] = now;
dfs(next, step + 1);
}
}
}
int LCA(int x, int y) {
if(de[x] < de[y])
swap(x, y);
for(int i = 30; i >= 0; i--)
if(de[x] - (1 << i) >= de[y])
x = dp[x][i];
if(x == y)
return x;
for(int i = 30; i >= 0; i--) {
if(dp[x][i] != dp[y][i]) {
x = dp[x][i];
y = dp[y][i];
}
}
return dp[x][0];
}
void Init() {
vis[1] = true;
dfs(1, 0);
for(int j = 1; j < 31; j++)
for(int i = 1; i <= n; i++)
dp[i][j] = dp[dp[i][j - 1]][j - 1];
}
void Read() {
scanf("%d %d", &n, &m);
for(int i = 1; i < n; i++) {
int A, B;
scanf("%d %d", &A, &B);
v[A].push_back(B);
v[B].push_back(A);
}
for(int i = 1; i <= n; i++)
scanf("%d", &W[i]);
}
void Write() {
for(int i = 1; i <= m; i++) {
int A, B;
scanf("%d %d", &A, &B);
int lca = LCA(A, B);
}
}
在这里不难想到一种暴力跑\(O(nm)\)的算法:
对于\(m\)个玩家\(i\),可以对于每一个观察员来判断在指定时刻到达该点,若到达,则为该观察员做出贡献。
还是可以骗到一些分的。
继续深入的思考一下:有哪些路径是重合的呢?
对于这种做法,当然没有,因为对于每一个节点都有不同的路径,而对于观察员来说两两关联并不是很大,所以这种对于每个玩家来进行贡献统计,不能优化什么。
Second
既然对于每个玩家跑一遍是行不通的,那么可以先转换思路,对于每一个观察员进行统计,看看那些节点对自己做了贡献。
初步地来想,好像也是\(O(nm)\)的暴力做法,求出每个玩家的起点与终点的lca,看是否与自己的时间要求相匹配。若可以匹配,则玩家为自己做出了贡献。
对于这棵树进行dfs,但是如何简化求出对自己做出贡献的节点呢?
情况一:
观察员在起点到lca的路上

如上图,满足上述条件,设e为起点,P为终点,若e为P做了贡献,不难想到需要满足以下条件:
deep[e]=w[P]+deep[P]
(deep为该节点的深度,可在求lca是进行处理)
由于P为e的祖先,所以e,P之间的距离就为deep[e]-deep[P],等于时间×速度,时间为w[P],速度又为1(题目已经给出),所以路程为w[P]。移项就转换为上述条件。
情况二:
观察员在lca到终点的路上

如上图,同理可以求出需要满足该条件:
deep[c]+deep[f]-2*deep[lca]−w[P]=deep[f]−deep[P]
(由于该图是一颗树,所以deep[c]+deep[f]-2*deep[lca] 为c到f的距离,下文使用dist来表示)
Third
应该如何统计那些节点对自己做出了贡献呢?
如果使用枚举的方法,那时间复杂度还是不变。
所以使用一个桶来存储当前访问的贡献值,回溯时就直接调用即可。
方法:
情况一:

很明显,c对于b与a都做出了贡献,满足上述情况。但是需要注意的地方是:
c点不应该为e点做出贡献!
怎么办呢?如何统计无法生效的多做了的贡献。
继续观察上图,可以发现只有桶内原来的值与现在桶内的差值才是所处了的真正贡献。(差分思想)
情况二:

因为对于该访问节点now,若是以now为根的子树,却不经过经过now节点的值,是必不会为该节点做出贡献的。
所以及时统计该子树做出的贡献,再删除该贡献的值,就不会被计入不该计入的树的贡献之中(离开这颗树就什么都不是)。
C++实现:
#include <cstdio>
#include <vector>
using namespace std;
int Quick_Read() {
int res = 0, op = 1;
char x = getchar();
while(!(x >= '0' && x <= '9')) {
if(x == '-')
op = -1;
x = getchar();
}
while(x >= '0' && x <= '9') {
res = (res << 3) + (res << 1) + x - '0';
x = getchar();
}
return res * op;
}
const int MAXN = 3e5;
vector<int> v[MAXN], Vend[MAXN], Vlca[MAXN];
int dist[MAXN], s[MAXN], t[MAXN], From[MAXN];
int ans[MAXN];
int bucket1[MAXN], bucket2[MAXN * 2];
int W[MAXN], deep[MAXN], dp[MAXN][32];
bool vis[MAXN];
int n, m;
void Read();
void Init();
void Player();
void DP(int);
void dfs(int, int);
int LCA(int, int);
signed main() {
Read();
Init();
Player();
return 0;
}
void DP(int now) {
int Num1 = bucket1[W[now] + deep[now]];
int Num2 = bucket2[W[now] - deep[now] + MAXN];
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(dp[now][0] != next)
DP(next);
}
bucket1[deep[now]] += From[now];
SIZ = Vend[now].size();
for(int i = 0; i < SIZ; i++) {
int next = Vend[now][i];
bucket2[dist[next] - deep[t[next]] + MAXN]++;
}
ans[now] += bucket1[W[now] + deep[now]] + bucket2[W[now] - deep[now] + MAXN] - Num1 - Num2;
SIZ = Vlca[now].size();
for(int i = 0; i < SIZ; i++) {
int next = Vlca[now][i];
bucket1[deep[s[next]]]--;
bucket2[dist[next] - deep[t[next]] + MAXN]--;
}
}
void dfs(int now, int step) {
deep[now] = step;
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(!vis[next]) {
vis[next] = true;
dp[next][0] = now;
dfs(next, step + 1);
}
}
}
int LCA(int x, int y) {
if(deep[x] < deep[y])
swap(x, y);
for(int i = 30; i >= 0; i--)
if(deep[x] - (1 << i) >= deep[y])
x = dp[x][i];
if(x == y)
return x;
for(int i = 30; i >= 0; i--) {
if(dp[x][i] != dp[y][i]) {
x = dp[x][i];
y = dp[y][i];
}
}
return dp[x][0];
}
void Init() {
deep[1] = 1; vis[1] = true; dp[1][0] = 1;
dfs(1, 1);
for(int j = 1; j < 31; j++)
for(int i = 1; i <= n; i++)
dp[i][j] = dp[dp[i][j - 1]][j - 1];
}
void Read() {
n = Quick_Read(); m = Quick_Read();
for(int i = 1; i < n; i++) {
int A, B;
A = Quick_Read(); B = Quick_Read();
v[A].push_back(B);
v[B].push_back(A);
}
for(int i = 1; i <= n; i++)
W[i] = Quick_Read();
}
void Player() {
for(int i = 1; i <= m; i++) {
s[i] = Quick_Read();
t[i] = Quick_Read();
int lca = LCA(s[i], t[i]);
dist[i] = deep[s[i]] + deep[t[i]] - 2 * deep[lca];
From[s[i]]++;
Vend[t[i]].push_back(i);
Vlca[lca].push_back(i);
if(deep[lca] + W[lca] == deep[s[i]])//若起点或终点与lca重合,则会重复统计
ans[lca]--;
}
DP(1);
for(int i = 1; i <= n; i++) {
printf("%d", ans[i]);
if(i != n)
printf(" ");
}
}
因为该做法只需要便利每一个玩家与观察员,所以时间复杂度为\(O(n+m)\)。(如果使用了链式向前星)
(注意:在统计向下的贡献时,有可能数组下标为负数,加上一个MAXN就可以了。但是即使为负数,也是有意义的,因为这个式子是通过移项所得到的)
「NOIP2016」天天爱跑步 题解的更多相关文章
- LOJ #2359. 「NOIP2016」天天爱跑步(倍增+线段树合并)
题意 LOJ #2359. 「NOIP2016」天天爱跑步 题解 考虑把一个玩家的路径 \((x, y)\) 拆成两条,一条是 \(x\) 到 \(lca\) ( \(x, y\) 最近公共祖先) 的 ...
- LOJ2359. 「NOIP2016」天天爱跑步【树上差分】
LINK 思路 首先发现如果对于一个节点,假设一个节点需要统计从字数内来的贡献 需要满足\(dep_u - dep_s = w_u\) 这个条件其实可以转化成\(dep_u - w_u = dep_s ...
- 「NOIP2016」天天爱跑步
传送门 Luogu 解题思路 树上差分+桶计数. 我们发现在一条路径上的点 \(i\) ,它可以观测到玩家的条件是: \(i \in (u \to LCA),dep_u=w_i+dep_i\) \(i ...
- NOIP2016(D1T2)天天爱跑步题解
首先声明这不是一篇算法独特的题解,仍然是"LCA+桶+树上差分",但这篇题解是为了让很多很多看了很多题解仍然看不懂的朋友们看懂的,其中就包括我,我也在努力地把解题的"思维 ...
- 【NOIP2016】天天爱跑步 题解(LCA+桶+树上差分)
题目链接 题目大意:给定一颗含有$n$个结点的树,每个结点有一个权值$w$.给定$m$条路径,如果一个点与路径的起点的距离恰好为$w$,那么$ans[i]++$.求所有结点的ans. 题目分析 暴力的 ...
- UOJ261 【NOIP2016】天天爱跑步 LCA+动态开点线段树
UOJ261 [NOIP2016]天天爱跑步 Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.天天爱跑步是一个养成类游戏,需要玩家每天按时上线, ...
- 「NOIP2009」最优贸易 题解
「NOIP2009」最优贸易 题解 题目TP门 题目描述 \(C\)国有\(n\)个大城市和\(m\)条道路,每条道路连接这\(n\)个城市中的某两个城市.任意两个城市之间最多只有一条道路直接相连.这 ...
- NOIP2016天天爱跑步 题解报告【lca+树上统计(桶)】
题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.«天天爱跑步»是一个养成类游戏,需要玩家每天按时上线,完成打卡任务. 这个游戏的地图可以看作一一棵包含 nn个 ...
- [NOIP2016]天天爱跑步 题解(树上差分) (码长短跑的快)
Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是一个养成类游戏,需要 玩家每天按时上线,完成打卡任务.这个游戏的地图 ...
随机推荐
- GA001-181-21
Composite State with History The Composite State with History Pattern describes an entity (e.g. Cl ...
- CentOS8平台nginx日志的定时切分
一,编写bash脚本: [root@yjweb crontab]# vi split_nginx_logs.sh 代码: #!/bin/bash # 备份nginx的日志 # 昨天的日期 file_d ...
- python并发编程之多进程(实践篇) 转
一 multiprocessing模块介绍 python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程.Python提供了multiproce ...
- shell脚本在后台运行以及日志重定向输出
后台运行命令 在命令行后加上 &,表示进程到后台中执行,如:cmd & 日志输出重定向 如:cmd > out.log & Linux默认定义两个变量:1和2: 1 表示 ...
- 《Kafka笔记》4、Kafka架构,与其他组件集成
目录 1 kafka架构进阶 1.1 Kafka底层数据的同步机制(面试常问) 1.1.1 高水位截断的同步方式可能带来数据丢失(Kafka 0.11版本前的问题) 1.1.2 解决高水位截断数据丢失 ...
- CSS中-moz、-ms、-webkit、-o的意思
-moz代表firefox浏览器私有属性 -ms代表ie浏览器私有属性 -webkit代表safari.chrome浏览器私有属性 -o代表opera浏览器私有属性 上述这些是为了兼容老版本的写法:
- Python包安装及使用指南
这里长期更新一些Python第三方包的安装教程,以及使用教程... Pygame 安装教程: Windows: 首先,查看已安装的Python版本:访问https://www.lfd.uci.edu/ ...
- 小程序商城Mall,打造最佳SpringCloudAlibaba最佳实践
背景 由于一路一来看过很多的技术体系,也见证一些技术体系停止维护,想用自己觉得比较好的一套技术体系来做一个分布式微服务系统,包括开发层面,中间件层面和运维层面的技术,作为自己希望的一个技术团队里的技术 ...
- CVE-2019-15107漏洞复现
特别说明 漏洞复现参考了teeom sec的panda潘森的文章,是根据他的思路进行复现. 搭建dockers环境, 然后安装vulhub(此处省略,自行百度) 进入vulhub/webmin/CVE ...
- 如何安装一个高可用K3s集群?
作者介绍 Janakiram MSV是Janakiram & Associates的首席分析师,也是国际信息技术学院的兼职教师.他也是Google Qualified Developer.亚马 ...