NOIP2016 天天爱跑步

一道综合性非常强的好题
最朴素的做法,便是模拟:模拟某一个玩家的移动(假设是从u跑到v,那么求出u与v的LCA w,然后考察u->w与v->w上的点即可),在到达某个节点时判断能否被这个点上的观察员观察到。
这样做肯定是会t的,不过会为我们提供一个思路:
假设出发时间是st_u,并且j是从u->v路径上的一个节点,那么如果有st_u+dis(u,j)==W[j],这个u->的玩家便会对j产生贡献
dis(u,j)怎么计算呢?用LCA,u,j的dep进行计算就可以了。
为了方便讨论,我们把路径分为向上的路径和向下的路径;对于u->lca->v的路径,只需要拆成u->w和w->v即可。
开始大力推式子:
在这里插入图片描述
(本人十分懒惰,于是直接把草稿纸给照了上来)
所以我们可以用开桶的方式来对每个玩家产生贡献的“特征值“进行统计
在计算某个观察员观察到的玩家数量时,根据桶里面相应特征值的数目即可得到答案
不过还有几个比较麻烦的细节:
考虑u->w->v的情况,如果这个玩家能被w处观察员看到,那么会被w处观察员统计两次在这里插入图片描述
所以我们要在处理lca的时候就预判这种情况,先给Ans[w]–
另外,如何保证我们统计到的玩家数量一定会经过这个点呢?
这个比较麻烦,得分情况讨论(我还是认真点作个图好了)
在这里插入图片描述
以上,代码如下

#include<bits/stdc++.h>
using namespace std;

const int MAXN=3e5+5;
const int D=6e5;

int N,M;
int Dep[MAXN],lg2[MAXN],W[MAXN],Ans[MAXN];
int Fa[MAXN][20];
int Bac_Up[MAXN<<3],Bac_Down[MAXN<<3];
vector<int>G[MAXN];
vector<int>Up_st[MAXN],Up_ed[MAXN],Down_st[MAXN],Down_ed[MAXN];

void Prepare(int cur,int pre) {
	Dep[cur]=Dep[pre]+1;
	Fa[cur][0]=pre;
	for(int k=1; k<=lg2[Dep[cur]]; k++)
		Fa[cur][k]=Fa[Fa[cur][k-1]][k-1];
	for(int i=0; i<G[cur].size(); i++) {
		int ver=G[cur][i];
		if(ver==pre)
			continue;
		Prepare(ver,cur);
	}
}

int LCA(int u,int v) {
	if(Dep[u]<Dep[v])
		swap(u,v);
	while(Dep[u]>Dep[v])
		u=Fa[u][lg2[Dep[u]-Dep[v]]];
	if(u==v)
		return u;
	for(int k=lg2[Dep[u]]; k>=0; k--)
		if(Fa[u][k]!=Fa[v][k])
			u=Fa[u][k],v=Fa[v][k];
	return Fa[u][0];
}

void Dfs_Up(int cur,int pre) {
	int tmp=Bac_Up[W[cur]+Dep[cur]];
	for(int i=0; i<G[cur].size(); i++) {
		int ver=G[cur][i];
		if(ver==pre)
			continue;
		Dfs_Up(ver,cur);
	}
	for(int i=0; i<Up_st[cur].size(); i++)
		Bac_Up[Up_st[cur][i]]++;
	Ans[cur]+=Bac_Up[W[cur]+Dep[cur]]-tmp;
	for(int i=0; i<Up_ed[cur].size(); i++)
		Bac_Up[Up_ed[cur][i]]--;
}

void Dfs_Down(int cur,int pre) {
	for(int i=0; i<Down_st[cur].size(); i++)
		Bac_Down[Down_st[cur][i]+D]++;
	//start from this pos to go down the tree
	int tmp=Bac_Down[W[cur]-Dep[cur]+D];
	for(int i=0; i<Down_ed[cur].size(); i++)
		Bac_Down[Down_ed[cur][i]+D]--;
	for(int i=0; i<G[cur].size(); i++) {
		int ver=G[cur][i];
		if(ver==pre)
			continue;
		Dfs_Down(ver,cur);
	}
	//minus the roads end in the subtree to calclulate the answer
	Ans[cur]+=tmp-Bac_Down[W[cur]-Dep[cur]+D];
}

int main() {
	//freopen("in.txt","r",stdin);
	//freopen("ou.txt","w",stdout);
	ios::sync_with_stdio(false);
	cin>>N>>M;
	for(int i=2; i<=N; i++)
		lg2[i]=lg2[i>>1]+1;
	for(int i=1,u,v; i<N; i++) {
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	Prepare(1,0);
	for(int i=1; i<=N; i++)
		cin>>W[i];
	//for(int i=1;i<=N;i++)
	//	cout<<W[i]<<" ";
	//cout<<endl;
	for(int i=1,s,t; i<=M; i++) {
		cin>>s>>t;
		//++Ans[s];
		int w=LCA(s,t);
		if(w!=s&&w!=t) {
			Up_st[s].push_back(Dep[s]);
			Up_ed[w].push_back(Dep[s]);
			Down_st[w].push_back(Dep[s]-2*Dep[w]);
			Down_ed[t].push_back(Dep[s]-2*Dep[w]);
			if(W[w]==Dep[s]-Dep[w])
				--Ans[w];
		} else if(s==w) {
			Down_st[s].push_back(-Dep[s]);
			Down_ed[t].push_back(-Dep[s]);
		} else {
			Up_st[s].push_back(Dep[s]);
			Up_ed[t].push_back(Dep[s]);
		}
	}
	Dfs_Up(1,0);
	Dfs_Down(1,0);
	for(int i=1; i<=N; i++)
		cout<<Ans[i]<<' ';
	return 0;
}
题目背景 NOIP2016 提高组 D1T2 题目描述 小 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 的观察员可以观察到这个玩家。 输入格式 第一行有两个整数 n 和 m。其中 n 代表树的结点数量, 同时也是观察员的数量, m 代表玩家的数量。 接下来 n−1 行每行两个整数 u 和 v,表示结点 u 到结点 v 有一条边。 接下来一行 n 个整数,其中第 j 个整数为 w j ​ , 表示结点 j 出现观察员的时间。 接下来 m 行,每行两个整数 s i ​ ,和 t i ​ ,表示一个玩家的起点和终点。 对于所有的数据,保证 1≤s i ​ ,t i ​ ≤n,0≤w j ​ ≤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 说明/提示 样例 1 说明 对于 1 号点,w i ​ =0,故只有起点为 1 号点的玩家才会被观察到,所以玩家 1 和玩家 2 被观察到,共有 2 人被观察到。 对于 2 号点,没有玩家在第 2 秒时在此结点,共 0 人被观察到。 对于 3 号点,没有玩家在第 5 秒时在此结点,共 0 人被观察到。 对于 4 号点,玩家 1 被观察到,共 1 人被观察到。 对于 5 号点,玩家 1 被观察到,共 1 人被观察到。 对于 6 号点,玩家 3 被观察到,共 1 人被观察到。 子任务 每个测试点的数据规模及特点如下表所示。 提示:数据范围的个位上的数字可以帮助判断是哪一种数据类型。 测试点编号 n= m= 约定 1∼2 991 991 所有人的起点等于自己的终点,即 ∀i, s i ​ =t i ​ 3∼4 992 992 所有 w j ​ =0 5 993 993 无 6∼8 99994 99994 ∀i∈[1,n−1],i 与 i+1 有边。即树退化成 1,2,…,n 按顺序连接的链 9∼12 99995 99995 所有 s i ​ =1 13∼16 99996 99996 所有 t i ​ =1 17∼19 99997 99997 无 20 299998 299998 无 提示 (提示:由于原提示年代久远,不一定能完全反映现在的情况,现在已经对该提示做出了一定的修改,提示的原文可以在该剪贴板查看) 在最终评测时,调用栈占用的空间大小不会有单独的限制,但在我们的工作环境中默认会有 1MiB 的限制。 这可能会引起函数调用层数较多时,程序发生栈溢出崩溃,程序中较深层数的递归往往会导致这个问题。如果你的程序需要用到较大的栈空间,请务必注意该问题。 我们可以使用一些方法修改调用栈的大小限制。 Linux 我们可以在终端中输入下列命令:ulimit -s 1048576。此命令的意义是,将调用栈的大小限制修改为 1048576KiB=1GiB。 例如,对于如下程序 sample.cpp: #include <bits/stdc++.h> using namespace std; int f[1000005]; void dfs(int a){ if(a == 0){ f[a] = 0; return; } dfs(a - 1); f[a] = f[a - 1] + 1; } int main(){ dfs(1000000); return 0; } 将上述源代码用命令 g++ sample.cpp -o sample 编译为可执行文件 sample 后,使用 ./sample 执行程序。 如果在没有使用命令 ulimit -s 1048576 的情况下运行该程序,sample 会因为栈溢出而崩溃;如果使用了上述命令后运行该程序,该程序则不会崩溃。 特别地,当你打开多个终端时,它们并不会共享该命令,你需要分别对它们运行该命令。 请注意,调用栈占用的空间会计入总空间占用中,和程序其他部分占用的内存共同受到内存限制。 Windows 如果你使用 Windows 下的 Dev-C++,请选择 工具-编译选项 并在如下区域填入以下命令 -Wl,--stack=1073741824,填入后注意确认“编译时加入以下命令的”的框是已勾选状态。 此处 1073741824 的单位是 B/Bytes。
最新发布
12-27
这是一个经典的 **树上差分 + 时间轴分析** 问题,出自 NOIP2016 提高组 D1T2《天天跑步》。 --- ### ✅ 解决方法 #### 🔍 问题简述 - 给定一棵 $ n $ 个节点的树,$ m $ 个玩家。 - 每个玩家从起点 $ s_i $ 出发,沿唯一路径走向终点 $ t_i $,速度为每秒一条边。 - 每个节点 $ j $ 上有一个观察员,在第 $ w_j $ 秒进行观察。 - 问:每个观察员能观察到多少名恰好在第 $ w_j $ 秒到达节点 $ j $ 的玩家? > 玩家一旦到达终点就停止,不能再被后续时间观测。 --- ### 🧠 核心思想 一个玩家 $ i $ 能被节点 $ j $ 的观察员看到 ⇔ 该玩家在第 $ w_j $ 秒时正好位于节点 $ j $,且此时他在前往 $ t_i $ 的路上。 我们考虑将每个玩家的路径拆解,并判断其是否经过某个点 $ j $,并且到达该点的时间恰好是 $ w_j $。 但直接枚举所有玩家和所有点会超时($n,m \leq 3\times10^5$),必须使用高效算法。 --- ### ✅ 关键洞察:路径分解与时间关系 设: - $ \text{dep}[u] $:节点 $ u $ 到根的距离(深度) - $ \text{lca}(s,t) $:$ s $ 和 $ t $ 的最近公共祖先 - $ d(u,v) $:$ u $ 到 $ v $ 的距离 = $ \text{dep}[u] + \text{dep}[v] - 2 \times \text{dep}[\text{lca}] $ 玩家 $ i $ 从 $ s_i $ 到 $ t_i $,路径分为两段: 1. 从 $ s_i $ 向上走到 $ \text{lca}(s_i, t_i) $ 2. 从 $ \text{lca} $ 向下走到 $ t_i $ 对于任意节点 $ j $ 在这条路径上: - 如果 $ j $ 在 **上升段**(即在 $ s_i \to \text{lca} $): - 到达 $ j $ 的时间为:$ \text{dep}[s_i] - \text{dep}[j] $ - 所以要求:$ \text{dep}[s_i] - \text{dep}[j] = w_j $ ⇒ $ \text{dep}[s_i] = w_j + \text{dep}[j] $ - 如果 $ j $ 在 **下降段**(即在 $ \text{lca} \to t_i $): - 到达 $ j $ 的时间为:$ \text{dep}[s_i] - \text{dep}[\text{lca}] + (\text{dep}[j] - \text{dep}[\text{lca}]) = \text{dep}[s_i] + \text{dep}[j] - 2 \times \text{dep}[\text{lca}] $ - 要求:这个时间等于 $ w_j $ ⇒ $ \text{dep}[s_i] - 2 \times \text{dep}[\text{lca}] + \text{dep}[j] = w_j $ ⇒ $ \text{dep}[j] - w_j = 2 \times \text{dep}[\text{lca}] - \text{dep}[s_i] $ --- ### ✅ 差分优化思路 我们可以对每个玩家的路径进行“事件标记”,然后通过 DFS 统计满足条件的玩家数量。 定义两个数组(用 `map` 或桶实现): - `up_cnt[x]`:当前有多少玩家正在上升路径中,且其 $ \text{dep}[s_i] = x $ - `down_cnt[y]`:当前有多少玩家正在下降路径中,且其 $ 2 \times \text{dep}[\text{lca}] - \text{dep}[s_i] = y $ 但在实际操作中,我们采用 **树上差分 + 回溯更新** 的方式。 更高效的经典做法是: > 使用 **桶(数组)记录进入/退出的影响**,配合 DFS 实现贡献的添加与删除。 --- ### ✅ 算法步骤 1. 预处理: - 建图、倍增预处理 LCA(或使用 Tarjan,但推荐倍增) - 计算每个点的深度 `dep[]` - 预处理每个玩家的路径信息:$ s_i, t_i, \text{lca}_i $ 2. 对每个玩家 $ i $: - 将其路径拆成两部分 - 在起点 $ s_i $ 处添加一个“上升事件”:期望值 $ A = w_j + \text{dep}[j] $ 应等于 $ \text{dep}[s_i] $ ⇒ 所以我们在 $ s_i $ 添加一个请求:将来遇到一个点 $ j $ 满足 $ w_j + \text{dep}[j] = \text{dep}[s_i] $ 时应加 1 - 类似地,在终点 $ t_i $ 添加“下降事件” 3. 使用 **树上差分 + DFS 回溯统计** 具体做法如下(标准解法): - 定义两个差分数组(桶): - `add_up[x]`:表示有多少条路径在当前子树中需要匹配上升条件 $ \text{dep}[s] = x $ - `add_down[x]`:表示有多少条路径需要匹配下降条件 $ \text{dep}[j] - w_j = x $ - 对于每个玩家 $ (s, t) $,记 $ l = \text{lca}(s,t) $: - 从 $ s \to l $ 是上升段 → 在 $ s $ 处插入一个 `up` 请求:值为 $ \text{dep}[s] $ - 从 $ l \to t $ 是下降段 → 在 $ t $ 处插入一个 `down` 请求:值为 $ \text{dep}[s] + \text{dep}[t] - 2 \times \text{dep}[l] - \text{dep}[t]? $ 不对 正确做法: 使用 **链上修改 + 子树查询** 的思想,转化为: - 在 $ s $ 处添加一个事件:当访问到某点 $ j $ 时,如果 $ \text{dep}[j] + w_j = \text{dep}[s] $,则答案 +1 - 在 $ l $ 的父节点处撤销该事件(因为超出路径范围) 同理,对下降段: - 在 $ t $ 处添加事件:若 $ \text{dep}[j] - w_j = \text{dep}[t] - d(s,t) $? 复杂 --- ### ✅ 正确实现:桶 + 差分回溯 ```cpp #include <iostream> #include <vector> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 300005; const int DEP_MAX = 600005; // dep up to 3e5, so offset for negative index int n, m; vector<int> G[MAXN]; int w[MAXN], dep[MAXN], fa[MAXN][20], ans[MAXN]; // 差分桶:用于上升和下降路径 vector<int> add_up[MAXN], sub_up[MAXN]; vector<int> add_down[MAXN], sub_down[MAXN]; // 查询桶 int cnt_up[DEP_MAX * 2] {}, cnt_down[DEP_MAX * 2] {}; void dfs_pre(int u, int parent) { dep[u] = dep[parent] + 1; fa[u][0] = parent; for (int i = 1; i < 20; ++i) { fa[u][i] = fa[fa[u][i-1]][i-1]; } for (int v : G[u]) { if (v != parent) { dfs_pre(v, u); } } } int lca(int u, int v) { if (dep[u] < dep[v]) swap(u, v); for (int i = 19; i >= 0; --i) { if (dep[fa[u][i]] >= dep[v]) { u = fa[u][i]; } } if (u == v) return u; for (int i = 19; i >= 0; --i) { if (fa[u][i] != fa[v][i]) { u = fa[u][i]; v = fa[v][i]; } } return fa[u][0]; } void dfs_ans(int u, int parent) { int old_up = cnt_up[dep[u] + w[u]]; int old_down = cnt_down[dep[u] - w[u] + DEP_MAX]; // 防负数 // 先处理子树前加入本节点的新增事件 for (int val : add_up[u]) { cnt_up[val]++; } for (int val : add_down[u]) { cnt_down[val + DEP_MAX]++; } // 递归子树 for (int v : G[u]) { if (v != parent) { dfs_ans(v, u); } } // 当前节点的答案 = 新增的数量 ans[u] = cnt_up[dep[u] + w[u]] - old_up + cnt_down[dep[u] - w[u] + DEP_MAX] - old_down; // 回溯前删除事件 for (int val : sub_up[u]) { cnt_up[val]--; } for (int val : sub_down[u]) { cnt_down[val + DEP_MAX]--; } } int main() { ios::sync_with_stdio(false); cin >> n >> m; for (int i = 1; i < n; ++i) { int u, v; cin >> u >> v; G[u].push_back(v); G[v].push_back(u); } for (int i = 1; i <= n; ++i) { cin >> w[i]; } dfs_pre(1, 0); // 处理每个玩家 for (int i = 0; i < m; ++i) { int s, t; cin >> s >> t; int l = lca(s, t); int dist_s_to_t = dep[s] + dep[t] - 2 * dep[l]; int time_to_l_from_s = dep[s] - dep[l]; // 上升段: s -> l // 在 s 插入事件:dep[s] 将在未来满足 dep[j] + w[j] == dep[s] add_up[s].push_back(dep[s]); // 在 l 的父亲处移除 if (fa[l][0]) { sub_up[fa[l][0]].push_back(dep[s]); } // 下降段: l -> t // 到达 j 的时间 = time_to_l_from_s + (dep[j] - dep[l]) // 要求:time_to_l_from_s + dep[j] - dep[l] == w[j] // => dep[j] - w[j] == dep[l] - time_to_l_from_s // => dep[j] - w[j] == dep[l] - (dep[s] - dep[l]) = 2*dep[l] - dep[s] int key = 2 * dep[l] - dep[s]; add_down[t].push_back(key); // 在 l 处移除(因为下降段从 l 开始) sub_down[l].push_back(key); } memset(ans, 0, sizeof ans); dfs_ans(1, 0); for (int i = 1; i <= n; ++i) { cout << ans[i] << " "; } cout << endl; return 0; } ``` --- ### 🔍 解释 1. **预处理阶段**: - 使用 DFS 构建深度和倍增数组 - 计算每个玩家的 LCA 2. **事件添加**: - 对于上升段:玩家在 $ s \to l $ 中,到达点 $ j $ 的时间是 $ \text{dep}[s] - \text{dep}[j] $,要求等于 $ w_j $ ⇒ $ \text{dep}[s] = \text{dep}[j] + w_j $ ⇒ 我们在 $ s $ 处添加一个“需匹配 $ \text{dep}[s] $”的事件,在 $ \text{parent}[l] $ 处删除 - 对于下降段:到达 $ j $ 的时间是 $ \text{dep}[s] - \text{dep}[l] + \text{dep}[j] - \text{dep}[l] = \text{dep}[j] + (\text{dep}[s] - 2\text{dep}[l]) $ ⇒ 要求 $ \text{dep}[j] - w_j = 2\text{dep}[l] - \text{dep}[s] $ ⇒ 在 $ t $ 添加该键值,在 $ l $ 删除 3. **DFS 统计**: - 使用全局桶 `cnt_up`, `cnt_down` 维护当前路径上的活跃事件 - 进入节点时先记录旧值,再添加新事件,遍历子树后计算增量,回溯前清除事件 4. **防负数索引**: - `dep[u] - w[u]` 可能为负,所以整体偏移 `DEP_MAX` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值