[蓝桥杯 2015 省 B] 生命之树

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

设 dp[u] 表示必须包含 u 的最优连通块和,自底向上只吸收正贡献子树,就能在线性时间求树上最大连通子图和。

OJ: luogu

题目 ID: P8625

难度:普及/提高-

标签:树形DP动态规划建模

日期: 2026-06-21 03:24

题意

给一棵带点权的树,可以选一个连通点集,要求点权和最大。

空集也允许,所以答案至少是 0

思路

先看一个可以直接验证想法的朴素解:

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

const int MAXN = 25;

int n;
long long val[MAXN];
int adj[MAXN][MAXN];

bool is_connected_mask(int mask) {
    if (mask == 0) {
        return true;
    }

    int start = -1;
    for (int i = 0; i < n; i++) {
        if (mask & (1 << i)) {
            start = i;
            break;
        }
    }

    queue<int> q;
    int vis = 0;
    q.push(start);
    vis |= (1 << start);

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v = 0; v < n; v++) {
            if (!adj[u][v]) {
                continue;
            }
            if (!(mask & (1 << v))) {
                continue;
            }
            if (vis & (1 << v)) {
                continue;
            }
            vis |= (1 << v);
            q.push(v);
        }
    }

    return vis == mask;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // 这是一个小数据精确暴力:
    // 枚举点集,再判断是否连通并统计点权和。
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> val[i];
    }

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            adj[i][j] = 0;
        }
    }

    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        u--;
        v--;
        adj[u][v] = adj[v][u] = 1;
    }

    long long ans = 0;
    int total = 1 << n;
    for (int mask = 0; mask < total; mask++) {
        if (!is_connected_mask(mask)) {
            continue;
        }
        long long sum = 0;
        for (int i = 0; i < n; i++) {
            if (mask & (1 << i)) {
                sum += val[i];
            }
        }
        ans = max(ans, sum);
    }

    cout << ans << '\n';
    return 0;
}

brute.cpp 枚举所有点集,检查是否连通,再统计点权和。 这个做法完全正确,但只能处理很小的数据。

这题的关键是树形 DP。

dp[u] 表示:

  • 选出的连通块必须包含 u
  • 并且这块连通结构只能通过 u 再往父亲方向继续连

那么 u 的某个儿子子树要不要接上来,只看它的最优贡献 dp[v]

  • dp[v] > 0,接上来会更优
  • dp[v] <= 0,不如不接

所以转移非常自然:

dp[u] = val[u] + sum(max(0, dp[v]))

其中 vu 的所有儿子。

最后答案为什么是所有 dp[u] 的最大值?

因为任意一个非空最优连通块,都可以找到一个最靠近根的点。 把它看成这块连通结构的“顶端”,这整个连通块就一定被某个 dp[u] 覆盖。

再结合题目允许空集,所以最终答案应为:

max(0, 所有 dp[u] 的最大值)

这题样例可以用一棵小树来理解:

graph G {
  1 [label="1"];
  2 [label="-2"];
  3 [label="-3"];
  4 [label="4"];
  5 [label="5"];
  4 -- 2;
  3 -- 1;
  1 -- 2;
  2 -- 5;
}

从这棵树中,最优连通块会选 4-2-5-1 这一部分,总和是:

4 + (-2) + 5 + 1 = 8

而点 3 的贡献是负数,所以不接进来更优。

代码

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

const int MAXN = 100000 + 5;

int n;
long long val[MAXN];
vector<int> g[MAXN];
int parent_arr[MAXN];
long long dp[MAXN];
long long ans;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> val[i];
        g[i].clear();
    }

    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    vector<int> order;
    order.reserve(n);
    stack<int> st;
    st.push(1);
    parent_arr[1] = 0;

    while (!st.empty()) {
        int u = st.top();
        st.pop();
        order.push_back(u);
        for (size_t i = 0; i < g[u].size(); i++) {
            int v = g[u][i];
            if (v == parent_arr[u]) {
                continue;
            }
            parent_arr[v] = u;
            st.push(v);
        }
    }

    ans = -(1LL << 60);
    for (int idx = (int)order.size() - 1; idx >= 0; idx--) {
        int u = order[idx];
        dp[u] = val[u];

        // 只有对子树贡献为正时,才值得把这棵子树连进来。
        for (size_t i = 0; i < g[u].size(); i++) {
            int v = g[u][i];
            if (v == parent_arr[u]) {
                continue;
            }
            if (dp[v] > 0) {
                dp[u] += dp[v];
            }
        }

        ans = max(ans, dp[u]);
    }

    if (ans < 0) {
        ans = 0;
    }
    cout << ans << '\n';
    return 0;
}

复杂度

整棵树只需要一次遍历和一次倒序 DP。

时间复杂度是 O(n)O(n),空间复杂度是 O(n)O(n)

总结

这题最核心的判断只有一句话:

  • 一棵子树如果能提供正贡献,就接;否则就不要

这是树上“最大连通子图和”最典型的树形 DP 思路。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析