题目描述
小ccc同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。
这个游戏的地图可以看作一棵包含nnn个结点和n−1n−1n−1条边的树, 每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从111到nnn的连续正整数。
现在有mmm个玩家,第iii个玩家的起点为SiS_iSi,终点为TiT_iTi。每天打卡任务开始时,所有玩家在第000秒同时从自己的起点出发, 以每秒跑一条边的速度, 不间断地沿着最短路径向着自己的终点跑去, 跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树, 所以每个人的路径是唯一的)
小ccc想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点jjj的观察员会选择在第WjW_jWj秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第WjW_jWj秒也理到达了结点jjj。小CCC想知道每个观察员会观察到多少人?
注意:我们认为一个玩家到达自己的终点后该玩家就会结束游戏,他不能等待一段时间后再被观察员观察到。即对于把结点jjj作为终点的玩家:若他在第WjW_jWj秒前到达终点,则在结点jjj的观察员不能观察到该玩家;若他正好在第WjW_jWj秒到达终点,则在结点jjj的观察员可以观察到这个玩家。
输入格式
第一行有两个整数nnn和mmm。其中nnn代表树的结点数量,同时也是观察员的数量,mmm代表玩家的数量。
接下来n−1n-1n−1行每行两个整数uuu和vvv,表示结点uuu到结点vvv有一条边。
接下来一行nnn个整数,其中第jjj个整数为WjW_jWj,表示结点jjj出现观察员的时间。
接下来mmm行,每行两个整数SiS_iSi,和TiT_iTi,表示一个玩家的起点和终点。
对于所有的数据,保证1≤Si,Ti≤n,0≤Wj≤n1\leq S_i,T_i\leq n, 0\leq W_j\leq n1≤Si,Ti≤n,0≤Wj≤n
输出格式
输出111行nnn个整数,第jjj个整数表示结点jj的观察员可以观察到多少人。
输入样例#1:
666 333
222 333
111 222
111 444
444 555
444 666
000 222 555 111 222 333
111 555
111 333
222 666
输出样例#1:
222 000 000 111 111 111
输入样例#2:
555 333
111 222
222 333
222 444
111 555
000 111 000 333 000
333 111
111 444
555 555
输出样例#2:
111 222 111 000 111
说明
【样例1说明】
对于111号点,Wi=0W_i=0Wi=0,故只有起点为111号点的玩家才会被观察到,所以玩家111和玩家222被观察到,共有222人被观察到。
对于222号点,没有玩家在第222秒时在此结点,共000人被观察到。
对于333号点,没有玩家在第555秒时在此结点,共000人被观察到。
对于444号点,玩家111被观察到,共111人被观察到。
对于555号点,玩家111被观察到,共111人被观察到。
对于666号点,玩家333被观察到,共111人被观察到。
【子任务】
每个测试点的数据规模及特点如下表所示。
提示:数据范围的个位上的数字可以帮助判断是哪一种数据类型。
前言
不得不说这是一道毒瘤好题。。。
毒瘤在众多的细节因为本蒟蒻太弱了
好在用非常低级的知识解决了非常麻烦的一道题,很锻炼思维。
关于部分分
根据某狗姓竞赛教练的说法,联赛出题人一般都会非常好心的出一些部分分来帮助选手们简化问题,思考问题,最终将你引向正解。
本人花了一天将此题每档部分分+正解做了一遍,一把辛酸泪。。。
测试点111~222
非常无脑,当且仅当iii点有人且wi=0w_i=0wi=0时对iii的答案才有贡献。
给一段仅供娱乐的代码
namespace code1 {
void main() {
for(int i = 1; i < n; i++) read(), read();
for(int i = 1; i <= n; i++) w[i] = read();
for(int i = 1; i <= n; i++) ans[i] = 0;
while(m--) {
int u = read(); read();
ans[u] += w[u] == 0;
}
for(int i = 1; i <= n; i++) write(ans[i]), putchar(i == n ? '\n' : ' ');
}
}
测试点333~444
依旧非常无脑,当且仅当iii点有人出发时对iii的答案有贡献。
再给一段仅供娱乐的代码
namespace code2 {
void main() {
for(int i = 1; i < n; i++) read(), read();
for(int i = 1; i <= n; i++) read();
for(int i = 1; i <= n; i++) ans[i] = 0;
while(m--) {
int u = read(); read();
ans[u]++;
}
for(int i = 1; i <= n; i++) write(ans[i]), putchar(i == n ? '\n' : ' ');
}
}
测试点555
因为nnn,mmm都很小,直接模拟每个人走的过程,求LCALCALCA直接用O(n)O(n)O(n)的即可。
本文第一段也是最后一段正经的暴力
namespace code3 {
void dfs(int u) {
dep[u] = dep[fa[u]] + 1;
for(int e = fir[u]; e; e = E[e].nxt) {
int v = E[e].to;
if(v == fa[u]) continue;
fa[v] = u;
dfs(v);
}
}
int LCA(int u, int v) {
if(dep[u] < dep[v]) swap(u, v);
while(dep[u] > dep[v]) u = fa[u];
while(u != v) u = fa[u], v = fa[v];
return u;
}
void main() {
clear_edge();
for(int i = 1; i < n; i++) {
int u = read(), v = read();
add_edge(u, v), add_edge(v, u);
}
dfs(1);
for(int i = 1; i <= n; i++) w[i] = read();
for(int i = 1; i <= n; i++) ans[i] = 0;
while(m--) {
int u = read(), v = read(), lca = LCA(u, v), tim = 0, dis = dep[u] + dep[v] - (dep[lca] << 1);
while(u != lca) {
ans[u] += w[u] == tim;
u = fa[u], tim++;
}
ans[u] += w[u] == tim;
tim = 0;
while(v != lca) {
ans[v] += w[v] == dis - tim;
v = fa[v], tim++;
}
}
for(int i = 1; i <= n; i++) write(ans[i]), putchar(i == n ? '\n' : ' ');
}
}
测试点666~888
树退化成一条链,这时候该怎么办呢?
这时每条路径只有两种走法:从左到右或从右到左。
考虑从左到右(从右到左类似):当且仅当i−w[i]i - w[i]i−w[i]有人出发时对iii的答案有贡献。
那就从右往左扫一遍,维护cntcntcnt数组,其中cnt[i]cnt[i]cnt[i]表示当前未结束路径中起点为iii的路径条数,边更新边统计答案就可以了。
注意特判越界的情况。
又臭又长的代码
namespace code4 {
struct node {int s, t;} a[maxn], b[maxn];
vector<int> tag[maxn];
void main() {
for(int i = 1; i < n; i++) read(), read();
for(int i = 1; i <= n; i++) w[i] = read();
int cnt1 = 0, cnt2 = 0;
while(m--) {
int s = read(), t = read();
if(s <= t) a[++cnt1] = (node) {s, t};
else b[++cnt2] = (node) {s, t};
}
for(int i = 1; i <= n; i++) ans[i] = 0;
for(int i = 1; i <= n; i++) bac[i] = 0, tag[i].clear();
for(int i = 1; i <= cnt1; i++) bac[a[i].s]++, tag[a[i].t].push_back(a[i].s);
for(int i = 1; i <= n; i++) {
ans[i] += i > w[i] ? bac[i - w[i]] : 0;
for(int j = 0; j < tag[i].size(); j++) bac[tag[i][j]]--;
}
for(int i = 1; i <= n; i++) bac[i] = 0, tag[i].clear();
for(int i = 1; i <= cnt2; i++) bac[b[i].s]++, tag[b[i].t].push_back(b[i].s);
for(int i = n; i >= 1; i--) {
ans[i] += i + w[i] <= n ? bac[i + w[i]] : 0;
for(int j = 0; j < tag[i].size(); j++) bac[tag[i][j]]--;
}
for(int i = 1; i <= n; i++) write(ans[i]), putchar(i == n ? '\n' : ' ');
}
}
测试点999~121212
当所有路径的出发点是111,有该怎么办呢?
因为树上路径问题与根无关,因此如果我们规定以111为根的话,当且仅当dep[i]==w[i]dep[i]==w[i]dep[i]==w[i]时对iii的答案有贡献(根节点深度为000)。
因此对于每个节点uuu,求以uuu为根的子树中有多少个终点即可,用dfsdfsdfs序+前缀和乱搞一下即可。
是时候展示我真正丑陋的码风了
namespace code5 {
int tid_cnt;
int siz[maxn], tid[maxn], num[maxn], s[maxn];
void dfs(int u, int fa) {
siz[u] = 1, tid[u] = ++tid_cnt, dep[u] = dep[fa] + 1;
for(int e = fir[u]; e; e = E[e].nxt) {
int v = E[e].to;
if(v == fa) continue;
dfs(v, u);
siz[u] += siz[v];
}
}
void main() {
clear_edge();
for(int i = 1; i < n; i++) {
int u = read(), v = read();
add_edge(u, v), add_edge(v, u);
}
for(int i = 1; i <= n; i++) w[i] = read();
tid_cnt = 0;
dep[0] = -1;
dfs(1, 0);
for(int i = 1; i <= n; i++) num[i] = 0;
while(m--) {
read(); int u = read();
num[tid[u]]++;
}
s[0] = 0;
for(int i = 1; i <= n; i++) s[i] = s[i - 1] + num[i];
for(int i = 1; i <= n; i++) write(w[i] == dep[i] ? s[tid[i] + siz[i] - 1] - s[tid[i] - 1] : 0), putchar(i < n ? ' ' : '\n');
}
}
测试点131313~161616
刚刚我们解决了起点为111的情况,那终点为111呢?
还是指定111为根,那么当且仅当w[i]+dep[i]==dep[t]w[i]+dep[i]==dep[t]w[i]+dep[i]==dep[t]时对iii的答案有贡献。
当我们扫描节点uuu时,只需要知道以uuu为根的子树中有多少个ttt满足dep[t]==w[u]+dep[u]dep[t]==w[u]+dep[u]dep[t]==w[u]+dep[u]。
用桶维护即可,但是我们会发现答案可能会受到其他子树中节点的影响,因此当我们刚扫描到uuu时先记下pre=cnt[dep[u]+w[u]]pre=cnt[dep[u]+w[u]]pre=cnt[dep[u]+w[u]],当递归访问完uuu的子树后再用此时的cnt[dep[u]+w[u]]−precnt[dep[u]+w[u]]-precnt[dep[u]+w[u]]−pre所得到的差值即为我们所需要的答案了。
关于我的码风,没有最丑,只有更丑
namespace code6 {
int num[maxn], bac[maxn];
void dfs(int u, int fa) {
dep[u] = dep[fa] + 1;
int pre = bac[dep[u] + w[u]];
bac[dep[u]] += num[u];
for(int e = fir[u]; e; e = E[e].nxt) {
int v = E[e].to;
if(v == fa) continue;
dfs(v, u);
}
ans[u] += bac[dep[u] + w[u]] - pre;
}
void main() {
clear_edge();
for(int i = 1; i < n; i++) {
int u = read(), v = read();
add_edge(u, v), add_edge(v, u);
}
for(int i = 1; i <= n; i++) w[i] = read();
for(int i = 1; i <= n; i++) num[i] = 0;
for(int i = 1; i <= m; i++) {
int u = read(); read();
num[u]++;
}
for(int i = 1; i <= n; i++) ans[i] = 0;
for(int i = 1; i <= n + n; i++) bac[i] = 0;
dep[0] = -1;
dfs(1, 0);
for(int i = 1; i <= n; i++) write(ans[i]), putchar(i == n ? '\n' : ' ');
}
}
测试点171717~202020
当当当当!!!正解要出现了!!!其实就是综上所述(逃
某王说的还是很有道理的,部分分果然是个好东西。
考虑正解时,我们先把每条路径拆分成两部分
- s→lcas\rightarrow lcas→lca的上升路径
- lca→tlca\rightarrow tlca→t的下降路径
当节点uuu处于第一类路径时,当且仅当满足下面222个条件时对uuu的答案有贡献:
- sss在uuu的子树中,ttt是uuu或uuu的祖先
- dep[s]=dep[u]+w[u]dep[s]=dep[u]+w[u]dep[s]=dep[u]+w[u]
用桶维护即可
小细节:当我们扫描完一个节点uuu时,我们需要消除所有以uuu为lcalcalca的路径对桶的影响(下文也会提到)
当节点uuu处于第二类路径时,当且仅当满足下面222个条件时对uuu的答案有贡献:
- sss不在uuu的子树中,ttt在uuu的子树中
- dis(s,u)=w[u]dis(s,u)=w[u]dis(s,u)=w[u]
对上式进行转化:
dis(s,u)=w[u]dis(s,u)=w[u]dis(s,u)=w[u]
dep[s]+dep[u]−2∗dep[LCA(s,u)]=w[u]dep[s]+dep[u]-2*dep[LCA(s,u)]=w[u]dep[s]+dep[u]−2∗dep[LCA(s,u)]=w[u]
dep[s]+dep[u]−2∗dep[LCA(s,t)]=w[u]dep[s]+dep[u]-2*dep[LCA(s,t)]=w[u]dep[s]+dep[u]−2∗dep[LCA(s,t)]=w[u]
dep[s]+dep[t]−2∗dep[LCA(s,t)]−dep[u]=w[u]−dep[u]dep[s]+dep[t]-2*dep[LCA(s,t)]-dep[u]=w[u]-dep[u]dep[s]+dep[t]−2∗dep[LCA(s,t)]−dep[u]=w[u]−dep[u]
dis(s,t)−dep[u]=w[u]−dep[u]dis(s,t)-dep[u]=w[u]-dep[u]dis(s,t)−dep[u]=w[u]−dep[u]
因此对于节点uuu,我们只需要统计满足上述两个条件的路径条数即可,用桶维护等式左边
来看一组样例(圆内是节点编号,圆外是wiw_iwi的值)
两条路径分别为1→51\rightarrow51→5和4→54\rightarrow54→5
当我们统计111号节点的答案时,如果不消除4→54\rightarrow54→5的影响,就会得出错误答案,为此,我们在扫描完一个节点uuu时,需要消除所有以uuu为lcalcalca的路径对桶的影响
贴上代码
namespace code7 {
int num[maxn], f[maxn][20];
vector<int> tag[maxn], era_up[maxn], era_down[maxn];
struct query {int s, t, lca;} que[maxn];
void init(int u, int fa) {
dep[u] = dep[fa] + 1;
for(int i = 1; i <= 18; i++) f[u][i] = f[f[u][i - 1]][i - 1];
for(int e = fir[u]; e; e = E[e].nxt) {
int v = E[e].to;
if(v == fa) continue;
f[v][0] = u;
init(v, u);
}
}
int LCA(int u, int v) {
if(dep[u] < dep[v]) swap(u, v);
for(int i = 18; i >= 0; i--)
if(dep[f[u][i]] >= dep[v]) u = f[u][i];
if(u == v) return u;
for(int i = 18; i >= 0; i--)
if(f[u][i] != f[v][i]) u = f[u][i], v = f[v][i];
return f[u][0];
}
int dis(int u, int v) {return dep[u] + dep[v] - (dep[LCA(u, v)] << 1);}
void calc_up(int u) {
int pre = bac[w[u] + dep[u]];
bac[dep[u]] += num[u];
for(int e = fir[u]; e; e = E[e].nxt) {
int v = E[e].to;
if(v == f[u][0]) continue;
calc_up(v);
}
ans[u] += bac[w[u] + dep[u]] - pre;
for(int i = 0; i < era_up[u].size(); i++) bac[era_up[u][i]]--;
}
void calc_down(int u) {
int pre = bac[w[u] - dep[u] + n];
for(int i = 0; i < tag[u].size(); i++) bac[tag[u][i] + n]++;
for(int e = fir[u]; e; e = E[e].nxt) {
int v = E[e].to;
if(v == f[u][0]) continue;
calc_down(v);
}
ans[u] += bac[w[u] - dep[u] + n] - pre;
for(int i = 0; i < era_down[u].size(); i++) bac[era_down[u][i] + n]--;
}
void main() {
clear_edge();
for(int i = 1; i < n; i++) {
int u = read(), v = read();
add_edge(u, v), add_edge(v, u);
}
dep[0] = -1;
init(1, 0);
for(int i = 1; i <= n; i++) w[i] = read();
for(int i = 1; i <= n; i++) num[i] = 0;
for(int i = 1; i <= m; i++) {
int s = read(), t = read(), lca = LCA(s, t);
num[s]++;
tag[t].push_back(dis(s, t) - dep[t]);
era_up[lca].push_back(dep[s]);
era_down[lca].push_back(dis(s, t) - dep[t]);
que[i] = (query) {s, t, lca};
}
for(int i = 1; i <= n; i++) ans[i] = 0;
for(int i = 1; i <= n + n; i++) bac[i] = 0;
calc_up(1);
for(int i = 1; i <= n + n; i++) bac[i] = 0;
calc_down(1);
for(int i = 1; i <= m; i++) {
int s = que[i].s, t = que[i].t, lca = que[i].lca;
if(dep[s] == w[lca] + dep[lca] && dis(s, t) - dep[t] == w[lca] - dep[lca]) ans[lca]--;
}
for(int i = 1; i <= n; i++) write(ans[i]), putchar(i == n ? '\n' : ' ');
}
}