超级无敌毒瘤好题——天天爱跑步

本文探讨了一款名为《天天爱跑步》的游戏算法,玩家在树状地图上完成打卡任务,观察者在特定时间记录玩家数量。通过分析不同路径与观察时间,采用桶维护和DFS等算法解决复杂游戏场景下的玩家计数问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

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

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

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

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

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

输入格式

第一行有两个整数nnnmmm。其中nnn代表树的结点数量,同时也是观察员的数量,mmm代表玩家的数量。

接下来n−1n-1n1行每行两个整数uuuvvv,表示结点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 n1Si,Tin,0Wjn

输出格式

输出111nnn个整数,第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

因为nnnmmm都很小,直接模拟每个人走的过程,求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]iw[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

当当当当!!!正解要出现了!!!其实就是综上所述(逃

某王说的还是很有道理的,部分分果然是个好东西。

考虑正解时,我们先把每条路径拆分成两部分

  1. s→lcas\rightarrow lcaslca的上升路径
  2. lca→tlca\rightarrow tlcat的下降路径

当节点uuu处于第一类路径时,当且仅当满足下面222个条件时对uuu的答案有贡献:

  1. sssuuu的子树中,tttuuuuuu的祖先
  2. dep[s]=dep[u]+w[u]dep[s]=dep[u]+w[u]dep[s]=dep[u]+w[u]

用桶维护即可

小细节:当我们扫描完一个节点uuu时,我们需要消除所有以uuulcalcalca的路径对桶的影响(下文也会提到)

当节点uuu处于第二类路径时,当且仅当满足下面222个条件时对uuu的答案有贡献:

  1. sss不在uuu的子树中,tttuuu的子树中
  2. 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]2dep[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]2dep[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]2dep[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\rightarrow5154→54\rightarrow545

当我们统计111号节点的答案时,如果不消除4→54\rightarrow545的影响,就会得出错误答案,为此,我们在扫描完一个节点uuu时,需要消除所有以uuulcalcalca的路径对桶的影响

贴上代码

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' : ' ');
	}
}
T23713 [愚人节目2]数据结构大毒瘤 提交答案加入单复制目 提交 1.47k 通过 273 时间限制 1.00s 内存限制 125.00MB 目编号 T23713 提供者 洛谷官方团队 难度 暂无评定 历史分数 暂无 提交记录 标签 洛谷原创 推荐目 暂无 复制 Markdown 展开 进入 IDE 模式 目背景 这是一道毒瘤太难了,所以窝先卖个萌0=w=0 窝从没出过这么难的!!!! 目描述 你好啊~这是一道数据结构毒瘤~ 您需要维护一个数列S~ 有7种操作,形如w a b c w=0 输出S a ​ +S a+1 ​ +...+S b ​ 。c没有用 w=1 将[S a ​ ,S b ​ ]翻转。c没有用 w=2 将[S a ​ ,S b ​ ]内的数全部加上c。 w=3 将[S a ​ ,S b ​ ]内的数全部乘上c。 w=4 将[S a ​ ,S b ​ ]内的数全部开根号。c没有用 w=5 将S a ​ 加上c,将S a+1 ​ 加上2c,...,将S b ​ 加上c*(b-a+1) w=6 将[S a ​ ,S b ​ ]和[S b+1 ​ ,S c ​ ]交换。保证c-b=b-a+1。 输入格式 第一行是n和m,n表示初始序列的长度,m表示操作数量 然后n个整数,表示初始序列S 之后m行每行四个数w a b c,代表一个操作 输出格式 对于每个0操作,输出一行表示答案 输入输出样例 输入 #1复制 5 1 1 2 3 4 5 0 1 2 3 输出 #1复制 3 说明/提示 样例解释 第一次操作,询问的答案为1+2=3 数据范围 1≤n,m≤5×10 4 ,0≤w≤9,1≤a≤b≤n 保证任何时候S i ​ ∈[−10 9 ,10 9 ] 保证输入所有数∈[−10 9 ,10 9 ]
最新发布
07-19
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值