牛半仙的妹子树

GitHub跳转原题关系图返回列表

多源 BFS 寻找距离在区间内的点集,随后利用换根 DP 计算树上节点到多个点集距离和与平方和问题。

OJ: roj

题目 ID: 10015

难度:提高+/省选-

标签:树形 DP换根 DPBFS

创建: 2026-10-10 07:11

更新: 2026-10-10 07:11

形式化题目

给定一棵包含 nn 个节点的树,边长均为 11。 初始有 mm 个节点拥有“第一类超能力”(称作红点)。 对于剩下的任意节点,如果该节点到所有红点的最短距离落在 [l,r][l, r] 区间内,则该节点拥有“第二类超能力”(称作绿点)。 一个点受到的“磁场强度”定义为:所有红点到该点的距离平方和,加上所有绿点到该点的距离和。 共有 kk 次查询,每次查询给定一个节点 uu,求该点受到总的“磁场强度”。

数据范围:n,m,k≤5×105n, m, k \le 5 \times 10^5。

正解

思路

第一步,确定绿点: 多源 BFS 可以快速求解每个点到最近红点的距离。我们将所有初始的红点加入队列并使得距离为 00,进行一次 BFS。对于每个点 ii,若求出的最短距离在 [l,r][l, r] 之间,则标记为绿点。

第二步,换根 DP: 查询次数 kk 高达 5×1055 \times 10^5,因此我们必须预处理出所有点作为询问点时的答案,总复杂度应控制在 O(n)O(n)。 设 cntcnt 为红点数量,disdis 为红点距离和,dis2dis2 为红点距离平方和。 以任意节点(例如 1 号节点)为根将无根树转为有根树。

子树内贡献(自底向上) 由于 (d+1)2=d2+2d+1(d+1)^2 = d^2 + 2d + 1,对于当前节点 uu 的某一儿子 vv: cnt[u]+=cnt[v]cnt[u] += cnt[v] dis[u]+=dis[v]+cnt[v]dis[u] += dis[v] + cnt[v] dis2[u]+=dis2[v]+2×dis[v]+cnt[v]dis2[u] += dis2[v] + 2 \times dis[v] + cnt[v] 同理可以求绿点的 ycntycnt 和 ydisydis 贡献。

全局贡献(自顶向下) 在得知父节点 uu 的全局结果后,减掉儿子 vv 子树中自带的部分,剩下便是“子树外”的贡献。 其余红点数量:rem_cnt=cnt[1]−cnt[v]rem\_cnt = cnt[1] - cnt[v] 其余红点的距离和:rem_dis=dis[u]−(dis[v]+cnt[v])rem\_dis = dis[u] - (dis[v] + cnt[v]) 其余红点的平方和:rem_dis2=dis2[u]−(dis2[v]+2×dis[v]+cnt[v])rem\_dis2 = dis2[u] - (dis2[v] + 2 \times dis[v] + cnt[v]) 这些子树外的红点对 vv 的贡献会因边长 +1+1 而变化: dis[v]+=rem_dis+rem_cntdis[v] += rem\_dis + rem\_cnt dis2[v]+=rem_dis2+2×rem_dis+rem_cntdis2[v] += rem\_dis2 + 2 \times rem\_dis + rem\_cnt 对绿点的维护以此类推。

为了避免在链状极限数据下递归爆栈,利用 BFS 序来代替递归遍历实现自底向上和自顶向下的迭代。

代码

python
#!/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()
cpp
/**
 * 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 求最短路的时间为 O(n)O(n),求 BFS 序的时间为 O(n)O(n),两次迭代换根 DP 遍历各为 O(n)O(n)。因此总时间复杂度为 O(n)O(n)。
  • 空间复杂度:需要若干长为 nn 的数组维护 cnt,ycnt,dis,dis2,ydiscnt, ycnt, dis, dis2, ydis 及图结构,总体空间复杂度为 O(n)O(n)。

总结

  • 坑点一:输入可能在同一个位置重复给出超能力点,需要在读取时进行去重操作(直接利用 ts[u] = 1 覆盖即可),避免将其计数多次。
  • 坑点二:树深度极端时可达到 O(n)O(n),传统的递归 DFS 换根不仅极慢甚至会导致爆栈,通过一次普通 BFS 取出层序遍历顺序,正向与逆向迭代即可安全实现 DP 转移。
  • 坑点三:距离的平方和能够达到 N×N2=1.25×1017N \times N^2 = 1.25 \times 10^{17} 级别,所以相关 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(std 10 6 10 vs main 0 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 的行为:

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)。理由:

  1. 它是验证完成之后的改动 ⇒ 本报告的结论(含 410 输入的等价性检查、 7,304,796~7,314,078 组穷举)都不覆盖它(T77)
  2. 题面「5 个正整数」保证 m ≥ 1 ⇒ 该分支在合法输入下不可达 ⇒ 零收益
  3. 它把本报告已如实报告的 out-of-spec 分歧(m=0∧l=0 时 std 10 6 10 vs main 0 0 0) 掩盖掉了 —— 而「如实报告分歧并标注 out-of-spec」是合格结论(T53 / T62)
  4. 沿用「不要为了让错误数据通过而写错算法」的原则: 为越界输入迁就参考实现,即使技术上无害,也会污染「主解 == 语义」的清晰性