多源 BFS 寻找距离在区间内的点集,随后利用换根 DP 计算树上节点到多个点集距离和与平方和问题。
OJ: roj
题目 ID: 10015
难度:提高+/省选-
标签:树形 DP换根 DPBFS
创建: 2026-10-10 07:11
更新: 2026-10-10 07:11
目录
形式化题目
给定一棵包含
数据范围:
正解
思路
第一步,确定绿点:
多源 BFS 可以快速求解每个点到最近红点的距离。我们将所有初始的红点加入队列并使得距离为
第二步,换根 DP:
查询次数
子树内贡献(自底向上)
由于
全局贡献(自顶向下)
在得知父节点
为了避免在链状极限数据下递归爆栈,利用 BFS 序来代替递归遍历实现自底向上和自顶向下的迭代。
代码
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-10 08:06
# update_at: 2026-10-10 08:30
import sys
def mark_green(n: int, adj: list[list[int]], is_red: list[int], l: int, r: int) -> list[int]:
"""用多源 BFS 标出到最近红点距离位于 [l,r] 的绿点。"""
dist = [-1] * (n + 1)
queue = [i for i in range(1, n + 1) if is_red[i]]
for i in queue:
dist[i] = 0
head = 0
while head < len(queue):
u = queue[head]
head += 1
for v in adj[u]:
if dist[v] == -1:
dist[v] = dist[u] + 1
queue.append(v)
return [0] + [1 if l <= dist[i] <= r else 0 for i in range(1, n + 1)]
def tree_order(n: int, adj: list[list[int]]) -> tuple[list[int], list[int]]:
"""从 1 号点出发得到父亲数组和遍历序,用来迭代换根。"""
parent = [0] * (n + 1)
order: list[int] = []
queue = [1]
seen = [False] * (n + 1)
seen[1] = True
head = 0
while head < len(queue):
u = queue[head]
head += 1
order.append(u)
for v in adj[u]:
if not seen[v]:
seen[v] = True
parent[v] = u
queue.append(v)
return parent, order
def all_answers(n: int, adj: list[list[int]], is_red: list[int], is_green: list[int]) -> list[int]:
"""换根计算每个询问点收到的红点平方距离和与绿点距离和。"""
parent, order = tree_order(n, adj)
red_count = [0] * (n + 1)
green_count = [0] * (n + 1)
red_dist = [0] * (n + 1)
red_dist2 = [0] * (n + 1)
green_dist = [0] * (n + 1)
for u in reversed(order):
red_count[u] = is_red[u]
green_count[u] = is_green[u]
for v in adj[u]:
if v == parent[u]:
continue
red_count[u] += red_count[v]
green_count[u] += green_count[v]
red_dist[u] += red_dist[v] + red_count[v]
red_dist2[u] += red_dist2[v] + 2 * red_dist[v] + red_count[v]
green_dist[u] += green_dist[v] + green_count[v]
total_red = red_count[1]
total_green = green_count[1]
for u in order:
for v in adj[u]:
if v == parent[u]:
continue
outside_red = total_red - red_count[v]
outside_red_dist = red_dist[u] - red_dist[v] - red_count[v]
outside_red_dist2 = red_dist2[u] - red_dist2[v] - 2 * red_dist[v] - red_count[v]
red_dist[v] += outside_red_dist + outside_red
red_dist2[v] += outside_red_dist2 + 2 * outside_red_dist + outside_red
outside_green = total_green - green_count[v]
outside_green_dist = green_dist[u] - green_dist[v] - green_count[v]
green_dist[v] += outside_green_dist + outside_green
return [red_dist2[i] + green_dist[i] for i in range(n + 1)]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
try:
n, m, k, l, r = next(data), next(data), next(data), next(data), next(data)
except StopIteration:
return
adj = [[] for _ in range(n + 1)]
for _ in range(n - 1):
u, v = next(data), next(data)
adj[u].append(v)
adj[v].append(u)
is_red = [0] * (n + 1)
for _ in range(m):
is_red[next(data)] = 1
queries = [next(data) for _ in range(k)]
is_green = mark_green(n, adj, is_red, l, r)
ans = all_answers(n, adj, is_red, is_green)
if queries: # 无询问时不输出空行(与标准程序逐字节一致)
sys.stdout.write("\n".join(str(ans[x]) for x in queries) + "\n")
if __name__ == "__main__":
solve()/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 08:06
* update_at: 2026-10-10 08:06
*/
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
const int MAXN = 500005;
int n, m, k, left_limit, right_limit;
vector<int> adj[MAXN]; // 树的邻接表
int is_red[MAXN]; // 初始具有超能力的红点
int is_green[MAXN]; // 到最近红点距离在区间内的绿点
int nearest_dist[MAXN];
int parent_node[MAXN];
int order_node[MAXN];
bool visited[MAXN];
ll red_count[MAXN]; // 当前根下子树/全树红点数量
ll green_count[MAXN]; // 当前根下子树/全树绿点数量
ll red_dist[MAXN]; // 到红点的距离和
ll red_dist2[MAXN]; // 到红点的距离平方和
ll green_dist[MAXN]; // 到绿点的距离和
ll answer[MAXN];
void clear_case() {
for (int i = 1; i <= n; i++) {
adj[i].clear();
is_red[i] = 0;
is_green[i] = 0;
nearest_dist[i] = -1;
parent_node[i] = 0;
visited[i] = false;
red_count[i] = 0;
green_count[i] = 0;
red_dist[i] = 0;
red_dist2[i] = 0;
green_dist[i] = 0;
answer[i] = 0;
}
}
void mark_green_points() {
queue<int> que;
for (int i = 1; i <= n; i++) {
if (is_red[i]) {
nearest_dist[i] = 0;
que.push(i);
}
}
while (!que.empty()) {
int u = que.front();
que.pop();
for (size_t j = 0; j < adj[u].size(); j++) {
int v = adj[u][j];
if (nearest_dist[v] == -1) {
nearest_dist[v] = nearest_dist[u] + 1;
que.push(v);
}
}
}
for (int i = 1; i <= n; i++) {
if (nearest_dist[i] >= left_limit && nearest_dist[i] <= right_limit) {
is_green[i] = 1;
}
}
}
int build_order() {
int head = 0, tail = 0;
order_node[tail++] = 1;
visited[1] = true;
while (head < tail) {
int u = order_node[head++];
for (size_t j = 0; j < adj[u].size(); j++) {
int v = adj[u][j];
if (!visited[v]) {
visited[v] = true;
parent_node[v] = u;
order_node[tail++] = v;
}
}
}
return tail;
}
void calc_subtree(int order_size) {
for (int pos = order_size - 1; pos >= 0; pos--) {
int u = order_node[pos];
red_count[u] = is_red[u];
green_count[u] = is_green[u];
for (size_t j = 0; j < adj[u].size(); j++) {
int v = adj[u][j];
if (v == parent_node[u]) {
continue;
}
red_count[u] += red_count[v];
green_count[u] += green_count[v];
red_dist[u] += red_dist[v] + red_count[v];
red_dist2[u] += red_dist2[v] + 2 * red_dist[v] + red_count[v];
green_dist[u] += green_dist[v] + green_count[v];
}
}
}
void reroot_all(int order_size) {
ll total_red = red_count[1];
ll total_green = green_count[1];
for (int pos = 0; pos < order_size; pos++) {
int u = order_node[pos];
for (size_t j = 0; j < adj[u].size(); j++) {
int v = adj[u][j];
if (v == parent_node[u]) {
continue;
}
ll outside_red = total_red - red_count[v];
ll outside_red_dist = red_dist[u] - red_dist[v] - red_count[v];
ll outside_red_dist2 = red_dist2[u] - red_dist2[v] - 2 * red_dist[v] - red_count[v];
red_dist[v] += outside_red_dist + outside_red;
red_dist2[v] += outside_red_dist2 + 2 * outside_red_dist + outside_red;
ll outside_green = total_green - green_count[v];
ll outside_green_dist = green_dist[u] - green_dist[v] - green_count[v];
green_dist[v] += outside_green_dist + outside_green;
}
}
}
void solve() {
if (!(cin >> n >> m >> k >> left_limit >> right_limit)) {
return;
}
clear_case();
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (int i = 1; i <= m; i++) {
int u;
cin >> u;
is_red[u] = 1; // 重复给出的红点只算一个
}
mark_green_points();
int order_size = build_order();
calc_subtree(order_size);
reroot_all(order_size);
for (int i = 1; i <= n; i++) {
answer[i] = red_dist2[i] + green_dist[i];
}
for (int i = 1; i <= k; i++) {
int query_node;
cin >> query_node;
cout << answer[query_node] << '\n';
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}复杂度
- 时间复杂度:多源 BFS 求最短路的时间为
,求 BFS 序的时间为 ,两次迭代换根 DP 遍历各为 。因此总时间复杂度为 。 - 空间复杂度:需要若干长为
的数组维护 及图结构,总体空间复杂度为 。
总结
- 坑点一:输入可能在同一个位置重复给出超能力点,需要在读取时进行去重操作(直接利用
ts[u] = 1覆盖即可),避免将其计数多次。 - 坑点二:树深度极端时可达到
,传统的递归 DFS 换根不仅极慢甚至会导致爆栈,通过一次普通 BFS 取出层序遍历顺序,正向与逆向迭代即可安全实现 DP 转移。 - 坑点三:距离的平方和能够达到
级别,所以相关 DP 数组及计算过程必须使用 long long,否则会发生整型溢出。
验证
本题由于无官方数据,因此完全依照给定的官方参考代码(std.cpp)推测的格式。后续可使用本实现与 std.cpp 进行对拍以确保各种边界情形的处理一致。
验证记录(R2,与官方 std.cpp 三方对拍)
⚠ 本题无本地
data/⇒check_new_analysis报「无 data/(跳过)」= 空转(T1/T65)。 证据来源是官方std.cpp(★★★★);不存在 ★★★★★ 等级证据。
结论
在 7,314,078 组小规模穷举(n≤10 全部无标号树 × 全部红点集 × 全部 [l,r])
与 395 例真进程三方对拍 上,main.cpp / main.py 与官方 std.cpp 一致;
分歧仅出现在违反题面约束的 m=0、k=0 输入上(见下)。
★ 不能写成「验证通过」。
★★ 对先前结论的证伪与订正(这是本题最重要的部分)
订正①:「题面暗示绿点不含红点」缺原文依据
| 立场 | 原文 |
|---|---|
| 题面(PDF) | 「离所有具有超能力的妹子的最短距离在 [l,r] 间的妹子会受到影响,也会具有超能力」 |
| ★ 机械核实 | grep -c '剩下的' docs/plans/roj-283/artifacts/10015-pdf-text.txt(PDF 原文,已归档) = 0 ⇒ 原文没有「剩下的」这个词 |
index.md |
该词出现 1 次 —— 它来自专家自写的形式化段落,不是题面 |
⇒ 真正成立的是:题面「5 个正整数」保证 l ≥ 1,红点距离为 0 不可及
⇒ l=0 的歧义在合法输入下不存在。
✅ 保留:std 在 l=0 时确实把红点也算绿点(与 main.cpp 一致)。
订正②:m=0 不只是「语义歧义」,std 与 main 在这里真的分歧
| 输入 | std |
main |
|---|---|---|
m=0, l=0 |
dis[]=0 ⇒ 全员变绿,输出 10 6 10 |
nearest_dist 保持 −1 ⇒ 0 0 0 |
实测 2010 个 m=0∧l=0 组中 2008 组不同(2 组差异为 n=1 全 0)。
m ≥ 1 时无差异 ⇒ 属 out-of-spec 输入上的分歧。
T67 清单(核对 std.cpp)
| 项 | 结果 |
|---|---|
| ① 循环结构 | 单组(Read() 读 5 个变量一次,无 while);拼两份输入 ⇒ 都只答第一组 ✓ |
| ② 变量是否都参与 | 5 个全参与(无 10013 式 no-op):l/r 变 ⇒ 输出变;m 变 ⇒ 变;k 变 ⇒ 行数变 |
| ③ 输入格式 | 与题面一致:5 整数 / n−1 边 / m 红点 / k 询问 ✓ |
| ④ 输出格式 | k 行 ✓ |
| ⑤ 边界 / UB | ★ std 空输入死循环(实测 real 3.00s 被杀);★ std 递归 dfs1/dfs2 在链 n≥1e5 处 SIGSEGV;数组按 5e5 固定 |
V6 独立 brute + 穷举(同进程 C++ harness)
- brute = 从每个点各跑一次 BFS 算全点对距离 → 按定义求和 (不复用多源 BFS,也不复用换根)
- 覆盖:
106/47/23/11/6/3/2/1/1/1棵n=1..10的无标号树(AHU 去重) × 全部2^n−1红点集 × 全部1≤l≤r≤n - 7,314,078 组:main 逻辑 vs brute = 0 分歧;
std逻辑(含cnt[u]>cnt[v]guard 的仿真)vs brute 也 0 分歧
V1 反事实表
| 变体 | 失败数 / 7,314,078 | 判定 |
|---|---|---|
[l,r] 改开区间 (l,r) |
2,530,830 | 闭区间 承重 |
| 多源 BFS 改单源 BFS | 3,985,869 | 多源 承重 |
树遍历改递归(= std 的 dfs1/dfs2) |
链 n≥100000 SIGSEGV (rc=−11) |
迭代 承重 |
m=0 不按 std 处理 |
仅 2008/2010 个 l=0 组不同,l≥1 时 0 |
★ 不承重(T53,如实报告) |
去掉 std 的 if(cnt[u]>cnt[v]) guard |
0 | ★ 不承重(越界时 rem_* 恒为 0) |
真进程三方对拍(395 例)
- 393/395 三方逐字节一致;2 例分歧均 out-of-spec:
m=0∧l=0(std10 6 10vs main0 0 0)、k=0(main.py多输出一个空行 → 已修) - 题面合法输入(
m,k,l≥1):390/390 一致 - 结构覆盖:链/星/二叉/毛毛虫/扫帚/随机 ·
n=1,2,3,20,50,400,1000~2000,5e5·m=1/m=n·l=r/r=n/r>n· 重复红点 ·k=5e5
V9 UBSan + 对照组 · T38
- ★ 对照程序(强制
−INT_MIN)确实报negation of -2147483648 cannot be represented in type 'int'⇒ 探针有效;被测main.cpp跑 408 个输入 stderr 全空 - config:time 1000ms / memory 128MB。实测
n=k=5e5:0.07–0.18 s / 22.1–60.3 MB (star 峰值 60.3MB = 限额 47%)⇒ ≥5× 余量 - 答案量级达
1e11⇒ll必需(链 2e5 单点答案 1.0e10 已实测)
与 std.cpp 的逐行差异点(10 条)
| # | std.cpp |
本实现 | 影响 |
|---|---|---|---|
| 1 | 链式前向星(#define int long long,约 24MB) |
vector<int> adj[] |
无 |
| 2 | 手写 Read()/getchar(空输入死循环) |
cin(EOF 返回) |
main 更稳健 |
| 3 | 仅 memset(dis) |
clear_case() 清 12 个数组 |
单组下等价 |
| 4 | dis[i]>=l&&dis[i]<=r |
同(改名) | 无 |
| 5 | ★ m=0 时未访问点保持 0 |
保持 −1 |
★ 唯一真实分歧(out-of-spec) |
| 6 | 递归 dfs1/dfs2 |
BFS 序迭代 | ★ std 深链 SIGSEGV |
| 7 | 沿途覆盖成全局的 cnt[u] |
常量 total_red |
等价(穷举已证) |
| 8 | 多一个 if(cnt[u]>cnt[v]) guard |
无 | ★ 不承重 |
| 9 | ts[x]=1 去重 |
is_red[x]=1 |
同(dup_red 已测) |
| 10 | #define int long long |
显式 int 下标 + ll 计数 |
同;数组上限均 5e5 |
★ 验证期间的并发改写(记录)
验证过程中 main.cpp 被并发改写(专家把主解拆成
mark_green_points / build_order / calc_subtree / reroot_all + clear_case())。
worker 做了 refactor 等价性检查:新旧二进制在 410 个输入上逐字节一致
(395 小/中 + 7 链 + 5 大 + sample + 空)⇒ 穷举结论对新实现同样成立;
随后全部测试用新版重跑。(T70 的同类风险:多路写同一目录)
★ 一次被回退的未验证改动(T77)
worker-42 在本报告的验证完成之后改了 main.cpp / main.py(08:35:09 / 08:35:14),
内容是给 m=0 ∧ l≤0≤r 加一个 guard,让主解在越界输入上模仿 std.cpp 的行为:
// 题面保证 m 为正;这里保留官方 std.cpp 在 m=0,l=0 时的行为。
if (source_count == 0 && left_limit <= 0 && 0 <= right_limit) {
for (int i = 1; i <= n; i++) is_green[i] = 1;
return;
}该改动已回退(备份归档在 docs/plans/roj-283/artifacts/10015-with-guard.cpp / .py)。理由:
- 它是验证完成之后的改动 ⇒ 本报告的结论(含 410 输入的等价性检查、
7,304,796~7,314,078 组穷举)都不覆盖它(
T77) - 题面「5 个正整数」保证
m ≥ 1⇒ 该分支在合法输入下不可达 ⇒ 零收益 - 它把本报告已如实报告的 out-of-spec 分歧(
m=0∧l=0时 std10 6 10vs main0 0 0) 掩盖掉了 —— 而「如实报告分歧并标注 out-of-spec」是合格结论(T53/T62) - 沿用「不要为了让错误数据通过而写错算法」的原则: 为越界输入迁就参考实现,即使技术上无害,也会污染「主解 == 语义」的清晰性