[NOIp 2016]天天爱跑步

Description

小C同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。

这个游戏的地图可以看作一棵包含 $n$ 个结点和 $n-1$ 条边的树,每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从 $1$ 到 $n$ 的连续正整数。

现在有 $m$ 个玩家,第 $i$ 个玩家的起点为 $S_i$,终点为 $T_i$。每天打卡任务开始时,所有玩家在第 $0$ 秒同时从自己的起点出发,以每秒跑一条边的速度,不间断地沿着最短路径向着自己的终点跑去,跑到终点后该玩家就算完成了打卡任务。(由于地图是一棵树,所以每个人的路径是唯一的)

小C想知道游戏的活跃度,所以在每个结点上都放置了一个观察员。在结点 $j$ 的观察员会选择在第 $W_j$ 秒观察玩家,一个玩家能被这个观察员观察到当且仅当该玩家在第 $W_j$ 秒也正好到达了结点 $j$。小C想知道每个观察员会观察到多少人?

注意:我们认为一个玩家到达自己的终点后该玩家就会结束游戏,他不能等待一段时间后再被观察员观察到。即对于把结点 $j$ 作为终点的玩家:若他在第 $W_j$ 秒到达终点,则在结点 $j$ 的观察员不能观察到该玩家;若他正好在第 $W_j$ 秒到达终点,则在结点 $j$ 的观察员可以观察到这个玩家。

Input

从标准输入读入数据。

第一行有两个整数 $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$。

Output

输出到标准输出。

输出 $1$ 行 $n$ 个整数,第 $j$ 个整数表示结点 $j$ 的观察员可以观察到多少人。

Sample Input

6 3
2 3
1 2
1 4
4 5
4 6
0 2 5 1 2 3
1 5
1 3
2 6

Sample Output

2 0 0 1 1 1

Sample Explanation

对于 $1$ 号点,$W_1=0$,故只有起点为 $1$ 号点的玩家才会被观察到,所以玩家 $1$ 和玩家 $2$ 被观察到,共 $2$ 人被观察到。

对于 $2$ 号点,没有玩家在第 $2$ 秒时在此结点,共 $0$ 人被观察到。

对于 $3$ 号点,没有玩家在第 $5$ 秒时在此结点,共 $0$ 人被观察到。

对于 $4$ 号点,玩家 $1$ 被观察到,共 $1$ 人被观察到。

对于 $5$ 号点,玩家 $1$ 被观察到,共 $1$ 人被观察到。

对于 $6$ 号点,玩家 $3$ 被观察到,共 $1$ 人被观察到。

HINT

每个测试点的数据规模及特点如下表所示。提示:数据范围的个位上的数字可以帮助判断是哪一种数据类型。

测试点编号 $n$ $m$ 约定
1 $=991$ $=991$ 所有人的起点等于自己的终点,即 $S_i = T_i$
2
3 $=992$ $=992$ $W_j=0$
4
5 $=993$ $=993$
6 $=99994$ $=99994$ 树退化成一条链,其中 $1$ 与 $2$ 有边,$2$ 与 $3$ 有边,$\dots$,$n-1$ 与 $n$ 有边
7
8
9 $=99995$ $=99995$ 所有的 $S_i=1$
10
11
12
13 $=99996$ $=99996$ 所有的 $T_i=1$
14
15
16
17 $=99997$ $=99997$
18
19
20 $=299998$ $=299998$

时间限制:$2\texttt{s}$

空间限制:$512\texttt{MB}$

题解

$O(n^3)$算法

1、容易得出$O(n^3)$最裸的暴力算法,然而。。。

$O(n^2)$算法

1、$O(n^2)$算法是枚举每个玩家和观察员,然后直接判断能不能观察到;

2、首先观察员必须要在玩家的路上,还要得出观察员离玩家多远才能直接判断;

3、一个结论是如果起点在$S$,终点在$T$,观察员在$A$点,那么$A$在$S→T$上当且仅当$Dis(S,T)=Dis(S,A)+Dis(A,T)$,那么就只需要解决求距离的问题了;

4、以每个点为根节点$DFS$整棵树就可以方便地预处理出两点之间的距离了。

继续优化

1、对于枚举已经没有优化的空间了,我们来优化判断的过程:对于玩家$i$和观察员$j$,$j$能观察到$i$当且仅当$j$在$i$的路径上且$Dis(i,j)=W(j)$;

2、假定指定一个点为树根之后,如果$i$和$j$互为祖孙关系,那么还可以写成:

$$|Hi-Hj|=Wj(Hi为i这个点离树根的距离)$$

那么只需要

$$(+/-)Hi=Wj(+/-)Hj$$

就可以了。

继续优化

1、那么我们就可以把$Wj(+/-)Hj$设为观察员的特征值,同一特征值的所有点(包括玩家)只要满足一个点在另一个点路径上就可以都算到答案里面了;

2、我们做一个转化:一条在树上的路径一定是先往上走一段再往下走一段的,所以我们按最高点分成左右两条路径分别算答案;

3、这样就当做分成两个玩家,一个玩家起点与之前相同但是终点在最高点处,另一个玩家起点在最高点并且出现时间接着上一个玩家的到达时间;

4、这样就可以按照我们之前互为祖孙关系的做法做了。

5、往上的半个玩家我们就算其特征值为$Hi$,另外半个假如最高点是$k$,那么特征值为$Dis(i,k)-Hk$。当然$Wj+/-Hj$的正负号也对应了玩家往上往下走,所以我们向上向下两种情况做两遍;

6、但怎么快速地将路径上同一特征值所有观察员答案加一呢?

7、我们注意到路径都是互为祖孙关系的,可以联想到序列上的前缀和,那么树上的前缀和就是每个点的值往上加到父亲上;

8、我们对每半个玩家在其起点终点处打上$+1$或$-1$的标记,其中较低点为$+1$、较高点为$-1$,然后$DFS$一遍,将标记加到前缀和加到父亲处,答案为当前点的前缀和;

9、类比数组上的前缀和可以知道只会有这一段路径上的点答案会$+1$;

10、当然如果要对于某一个特征值有标记的话我们可以对于每个特征值都建一棵树,方法是$DFS$一遍,记录从根到当前点每个特征值的最近祖先,然后重置当前点的父亲为最近同特征祖先;

11、复杂度主要花在分离一个玩家上,需要找到$S$和$T$的最高点,可以用树链剖分,常数比较小,或者$Tarjan$。

 //It is made by Awson on 2017.10.22
#include <set>
#include <map>
#include <cmath>
#include <ctime>
#include <cmath>
#include <stack>
#include <queue>
#include <vector>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define LL long long
#define Min(a, b) ((a) < (b) ? (a) : (b))
#define Max(a, b) ((a) > (b) ? (a) : (b))
#define sqr(x) ((x)*(x))
using namespace std;
const int N = ; struct tt {
int to, next, id;
}edge[(N<<)+], query[(N<<)+];
int pathe[N+], tope, pathq[N+], topq;
int n, m, u, v;
int w[N+], s[N+], tt[N+];
int dep[N+], fa[N+], lca[N+];
int mark[N+];
vector<int>q1[N+], q2[N+], q3[N+];
int cnt1[(N<<)+], cnt2[(N<<)+], ans[N+]; int find(int r) {
return fa[r] ? fa[r] = find(fa[r]) : r;
}
void add(int u, int v) {
edge[++tope].to = v;
edge[tope].next = pathe[u];
pathe[u] = tope;
}
void add2(int s, int t, int id) {
query[++topq].to = t;
query[topq].id = id;
query[topq].next = pathq[s];
pathq[s] = topq;
}
void tarjan(int u, int depth) {
dep[u] = depth;
for (int i = pathe[u]; i; i = edge[i].next)
if (!dep[edge[i].to]) {
tarjan(edge[i].to, depth+);
fa[edge[i].to] = u;
}
for (int i = pathq[u]; i; i = query[i].next) if (dep[query[i].to]) lca[query[i].id] = find(query[i].to);
}
void dfs1(int u, int fa) {
int tmp = w[u]+dep[u];
int pre = cnt1[tmp];
for (int i = pathe[u]; i; i = edge[i].next)
if (edge[i].to != fa) dfs1(edge[i].to, u);
cnt1[dep[u]] += mark[u];
ans[u] += cnt1[tmp]-pre;
for (int i = ; i < q1[u].size(); i++) cnt1[q1[u][i]]--;
}
void dfs2(int u, int fa) {
int tmp = w[u]-dep[u]+n;
int pre = cnt2[tmp];
for (int i = pathe[u]; i; i = edge[i].next)
if (edge[i].to != fa) dfs2(edge[i].to, u);
for (int i = ; i < q2[u].size(); i++) cnt2[q2[u][i]]++;
ans[u] += cnt2[tmp]-pre;
for (int i = ; i < q3[u].size(); i++) cnt2[q3[u][i]]--;
}
void work() {
scanf("%d%d", &n, &m);
for (int i = ; i < n; i++) {
scanf("%d%d", &u, &v);
add(u, v); add(v, u);
}
for (int i = ; i <= n; i++) scanf("%d", &w[i]);
for (int i = ; i <= m; i++) {
scanf("%d%d", &s[i], &tt[i]);
add2(s[i], tt[i], i); add2(tt[i], s[i], i);
}
tarjan(, );
for (int i = ; i <= m; i++) {
mark[s[i]]++; q1[lca[i]].push_back(dep[s[i]]);
}
dfs1(, );
for (int i = ; i <= m; i++) {
int l = dep[s[i]]+dep[tt[i]]-*dep[lca[i]];
q2[tt[i]].push_back(l-dep[tt[i]]+n);
q3[lca[i]].push_back(l-dep[tt[i]]+n);
}
dfs2(, );
for (int i = ; i <= m; i++) if (w[lca[i]]+dep[lca[i]] == dep[s[i]]) ans[lca[i]]--;
for (int i = ; i <= n; i++) printf("%d ", ans[i]);
}
int main() {
work();
return ;
}
上一篇:ruby pluck用法,可以快速从数据库获取 对象的 指定字段的集合数组


下一篇:学习Python爬虫的4幅思维导图