(声明:图片来源于网络)

「NOIP2016」天天爱跑步 题解

题目TP门

题目

题目描述

小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」天天爱跑步 题解的更多相关文章

  1. LOJ #2359. 「NOIP2016」天天爱跑步(倍增+线段树合并)

    题意 LOJ #2359. 「NOIP2016」天天爱跑步 题解 考虑把一个玩家的路径 \((x, y)\) 拆成两条,一条是 \(x\) 到 \(lca\) ( \(x, y\) 最近公共祖先) 的 ...

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

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

  3. 「NOIP2016」天天爱跑步

    传送门 Luogu 解题思路 树上差分+桶计数. 我们发现在一条路径上的点 \(i\) ,它可以观测到玩家的条件是: \(i \in (u \to LCA),dep_u=w_i+dep_i\) \(i ...

  4. NOIP2016(D1T2)天天爱跑步题解

    首先声明这不是一篇算法独特的题解,仍然是"LCA+桶+树上差分",但这篇题解是为了让很多很多看了很多题解仍然看不懂的朋友们看懂的,其中就包括我,我也在努力地把解题的"思维 ...

  5. 【NOIP2016】天天爱跑步 题解(LCA+桶+树上差分)

    题目链接 题目大意:给定一颗含有$n$个结点的树,每个结点有一个权值$w$.给定$m$条路径,如果一个点与路径的起点的距离恰好为$w$,那么$ans[i]++$.求所有结点的ans. 题目分析 暴力的 ...

  6. UOJ261 【NOIP2016】天天爱跑步 LCA+动态开点线段树

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

  7. 「NOIP2009」最优贸易 题解

    「NOIP2009」最优贸易 题解 题目TP门 题目描述 \(C\)国有\(n\)个大城市和\(m\)条道路,每条道路连接这\(n\)个城市中的某两个城市.任意两个城市之间最多只有一条道路直接相连.这 ...

  8. NOIP2016天天爱跑步 题解报告【lca+树上统计(桶)】

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

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

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

随机推荐

  1. RESP协议

    RESP 是 Redis 序列化协议的简写.它是⼀种直观的⽂本协议,优势在于实现异常简单,解析性能极好. Redis 协议将传输的结构数据分为 5 种最⼩单元类型,单元结束时统⼀加上回⻋换⾏符号\r\ ...

  2. HTML 的属性

    HTML 属性赋予元素意义和语境. 下面的全局属性可用于任何 HTML 元 属性 描述 accesskey 规定激活元素的快捷键. class 规定元素的一个或多个类名(引用样式表中的类). cont ...

  3. ThreeJS学习6_几何体相关(BufferGeometry)

    ThreeJS学习6_几何体相关(BufferGeometry) 使用 BufferGeometry 可以有效减少向 GPU 传输几何体相关数据所需的开销 可以自定义顶点位置, 面片索引, 法向量, ...

  4. Vue3: 如何以 Vite 创建,以 Vue Router, Vuex, Ant Design 开始应用

    本文代码: https://github.com/ikuokuo/start-vue3 在线演示: https://ikuokuo.github.io/start-vue3/ Vite 创建 Vue ...

  5. Ngnix01

    Nginx(一)------简介与安装   目录 1.Nginx 的简介 2.Nginx 的常用功能 3.Nginx 安装 ①.下载地址 ②.Windows 版本安装 ③.Linux 版本安装 说到 ...

  6. Vue基础(2)

    fetch与axios请求数据 fetch基本语法: fetch(url,{parmas}).then(res=> res.json()  //返回promise对象 ).then(data=& ...

  7. Zookeeper(2)---节点属性、监听和权限

    之前通过客户端连接之后我们已经知道了zk相关的很多命令(Zookeeper(1)---初识). 节点属性: 现在我们就通过stat指令来看看节点都有哪些属性,或者使用get 指令和-s参数来查看节点数 ...

  8. non-local denoising methods

    NL-Means算法 在噪声先验为高斯噪声的基础上, 进行non-local的平均,在2005年由Baudes提出,该算法使用自然图像中普遍存在的冗余信息来去噪声.与常用的双线性滤波.中值滤波等利用图 ...

  9. 操作安装docker

    在本地建造起vue-cli服务 参考项目:https : //gitee.com/QiHanXiBei/myvue 在本地建造起一个django项目架构,通过/ hello能够打印出helloworl ...

  10. Java学习的第四十七天

    1.用类函数来写时间类 import java.util.Scanner; public class Cjava { public static void main(String[]args) { T ...