题目描述

给定一棵包含 个节点的无根树,每个节点有一个类型 和权值 。

一条简单路径的权值是路径上所有节点权值之和。如果一条路径至少经过一个类型为 的节点,那么它就是一条 -路径。

对于每个 ,求所有 -路径中的最大权值。如果不存在类型为 的节点,输出 。

输入描述

第一行输入测试用例数量 。

每组数据包含:

  • 第一行:节点数 ;
  • 第二行: 个整数 ,表示节点类型;
  • 第三行: 个整数 ,表示节点权值;
  • 接下来 行:每行两个整数 ,表示树上的一条无向边。

约束:

输出描述

输出一行 个整数,第 个数表示所有 -路径的最大权值;如果不存在类型为 的节点,输出 。


初步思路

如果只求整棵树的最大路径,可以使用树形 DP:枚举每个节点作为路径的最高点,取来自不同子树的两条最大贡献,再加上当前节点权值。

本题的难点在于:需要同时求出每一种类型的答案,而且一条路径中的类型节点可能不是路径的转折点。因此需要在每一个节点 都计算经过它的最大路径,并用当前节点类型更新答案:

经过的最大路径权值

这样,任意一条包含类型 节点的路径,都可以在该类型节点处被统计到。


树形 DP:计算子树方向贡献

以节点 为根。定义从节点 向其子树方向延伸的贡献:

其中 表示从 出发、只向 的子树方向延伸时能够取得的最大正贡献。如果继续向下不能带来正收益,就选择不延伸,因此初始值为 。

对节点 来说,只需要保留所有子节点贡献中的前两大值:

  • mx1[u]:最大贡献;
  • mx2[u]:次大贡献。

第一次 DFS 自底向上计算这两个值:

mx2[u] = max(mx2[u], w[v] + mx1[v]);
if (mx2[u] > mx1[u]) {
    swap(mx1[u], mx2[u]);
}

因为路径经过 时,最多只会从 向两个不同方向延伸,所以只保留前两大贡献即可。

注意:由于 mx1、mx2 初始化为 ,负贡献会被自动舍弃。这等价于允许路径在当前节点处停止,能够正确处理负权节点。


换根 DP:补充父节点方向

第一次 DFS 只计算了子树方向,节点 还可能通过父节点方向连接到树的其他部分。

第二次 DFS 从根向下传递 fw:

  • fw 表示从节点 的父节点方向进入 时,除去 本身能够获得的最大贡献;
  • 将 fw 加入当前节点的候选贡献后,重新维护最大的两条贡献;
  • 当前节点的最大路径值就是:

然后更新:

如何传给子节点

假设当前从 走向子节点 ,需要把「不经过 子树」的最大贡献传给 。

如果 对应的贡献是 mx1[u],就不能再次使用它,应改用 mx2[u];否则可以继续使用 mx1[u]:

w[u] + (w[v] + mx1[v] == mx1[u] ? mx2[u] : mx1[u])

加上 w[u] 后,就得到了从父节点方向进入 的贡献。


正确性说明

  1. 第一次 DFS 对每个节点保留子树方向的最大、次大贡献,因此能够得到从该节点向下延伸的最优两条路径。
  2. 第二次 DFS 将父节点方向的贡献加入候选集合,并在向子节点转移时排除该子节点自身的贡献,因此每个节点都能得到来自整棵树各个方向的最优两条路径。
  3. 经过节点 的最大路径一定由 的权值和两个不同方向的最大正贡献组成,即 w[u] + mx1[u] + mx2[u]。
  4. 每个类型为 的节点都会用经过它的最大路径更新 ans[k],因此最终得到的就是所有 -路径的最大权值。

算法分析

  • 时间复杂度:,每条边在两次 DFS 中各访问一次;
  • 空间复杂度:,用于邻接表和各个 DP 数组;
  • 使用 long long:节点权值可能为负数,路径和也需要使用 64 位整数保存。

代码实现

以下给出 C++、Python、Java、Go、Rust 五种语言的实现,逻辑完全一致。切换上方的标签即可对比。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int N = 1e6 + 6;
const ll INF = 1e18;

int n;
int color[N];
ll weight[N], best1[N], best2[N], answer[N];
vector<int> graph[N];

void dfs1(int u, int parent) {
    best1[u] = best2[u] = 0;
    for (int v : graph[u]) {
        if (v == parent) continue;
        dfs1(v, u);
        ll contribution = weight[v] + best1[v];
        best2[u] = max(best2[u], contribution);
        if (best2[u] > best1[u]) swap(best1[u], best2[u]);
    }
}

void dfs2(int u, int parent, ll from_parent) {
    best2[u] = max(best2[u], from_parent);
    if (best2[u] > best1[u]) swap(best1[u], best2[u]);
    answer[color[u]] = max(answer[color[u]],
                           weight[u] + best1[u] + best2[u]);
    for (int v : graph[u]) {
        if (v == parent) continue;
        ll next_from_parent = weight[u] +
            (weight[v] + best1[v] == best1[u] ? best2[u] : best1[u]);
        dfs2(v, u, next_from_parent);
    }
}

void solve() {
    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> color[i];
    for (int i = 1; i <= n; ++i) cin >> weight[i];
    for (int i = 1; i < n; ++i) {
        int u, v;
        cin >> u >> v;
        graph[u].push_back(v);
        graph[v].push_back(u);
    }
    fill(answer + 1, answer + n + 1, -INF);
    dfs1(1, 0);
    dfs2(1, 0, 0);
    for (int i = 1; i <= n; ++i) {
        cout << (answer[i] == -INF ? -1 : answer[i]);
        cout << (i == n ? '\n' : ' ');
    }
    for (int i = 1; i <= n; ++i) graph[i].clear();
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int test_cases;
    cin >> test_cases;
    while (test_cases--) solve();
    return 0;
}

易错点

  1. 不能只在路径转折点更新答案:类型节点可能是路径端点,所以必须在每个节点处用 color[u] 更新答案。
  2. 负贡献不能强行加入:best1 和 best2 从 开始,表示可以选择不继续向某个方向延伸。
  3. 换根转移要排除当前子树:如果子节点贡献等于当前节点的最大贡献,就必须使用次大贡献。
  4. 答案数组需要按测试用例重新初始化:否则上一个测试用例的结果会污染当前答案。
  5. 深度风险:题目允许 达到 ,如果树退化成链,递归 DFS 可能触发栈溢出。提交时应确认评测环境的栈空间;若不稳定,可以把两次 DFS 改写为显式栈的迭代遍历。

可改进点

  • 本实现为了代码清晰采用递归 DFS。若评测机栈空间较小,可改成迭代版,避免链状树爆栈。
  • Python 与 Java 版本在 时递归深度较大,建议配合迭代或提高递归限制。
  • 答案数组只需开到「出现的最大类型编号」,可用离散化进一步压缩空间。

总结

本题的核心是「树形 DP + 换根 DP」:

  • 第一次 DFS 统计子树方向的最大两条贡献;
  • 第二次 DFS 补充父节点方向的信息;
  • 每个节点利用两个方向的最大贡献计算经过它的最大路径;
  • 用节点类型将结果分组更新。

最终在 时间内求出所有类型的答案。