[HAOI2009] 毛毛虫

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

把路径内部点的贡献化成 deg(u)-1,将答案转成树上最大点权路径和,再在结尾补上两个端点贡献。

OJ: luogu

题目 ID: P3174

难度:提高+/省选-

标签:树形DP树的直径推导

日期: 2026-06-21 04:44

题意

在树中选一条简单链作为“主链”。

这条链上的点,以及所有与链上点直接相邻的点,合起来构成一个毛毛虫。 要求最大化这个毛毛虫的点数。

思路

先看一个只用于小数据验证的暴力:

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

const int MAXN = 25;

int n, m;
vector<int> g[MAXN];
int parent_arr[MAXN];
int vis[MAXN];
int ans;

bool dfs_path(int u, int fa, int target, vector<int> &path) {
    path.push_back(u);
    if (u == target) {
        return true;
    }
    for (size_t i = 0; i < g[u].size(); i++) {
        int v = g[u][i];
        if (v == fa) {
            continue;
        }
        if (dfs_path(v, u, target, path)) {
            return true;
        }
    }
    path.pop_back();
    return false;
}

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

    // brute.cpp:枚举所有路径,直接统计这条路径及其相邻边带来的点数。
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        g[i].clear();
    }
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    ans = 1;
    for (int s = 1; s <= n; s++) {
        for (int t = s; t <= n; t++) {
            vector<int> path;
            dfs_path(s, 0, t, path);

            memset(vis, 0, sizeof(vis));
            for (size_t i = 0; i < path.size(); i++) {
                int u = path[i];
                vis[u] = 1;
                for (size_t j = 0; j < g[u].size(); j++) {
                    vis[g[u][j]] = 1;
                }
            }

            int cnt = 0;
            for (int i = 1; i <= n; i++) {
                cnt += vis[i];
            }
            ans = max(ans, cnt);
        }
    }

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

brute.cpp 枚举路径两端点,找出整条路径,再把路径和路径旁边一层的点都统计进去。

正解的关键是把问题改写成“树上最大路径和”。

考虑一条固定主链上的点 u

  • 如果 u 是链内部点,它贡献自己,以及除了链上前后两条边之外的所有相邻点
  • 如果 u 是链端点,它会比内部点多贡献一个端点方向

整理后可得:

  • 内部点贡献 deg(u)-1
  • 端点贡献 deg(u)

于是如果我们先给每个点一个基础权值:

w[u] = deg(u)-1

那么任意一条主链的毛毛虫大小,就等于:

  • 这条路径上所有 w[u] 的和
  • 再额外加 2

所以问题变成求树上的最大点权路径和。

下面这张图说明了“路径点 + 挂边点”的来源:

graph G {
  rankdir=LR;
  A -- B -- C;
  A -- X;
  B -- Y;
  B -- Z;
  C -- T;
}

如果 A-B-C 是主链,那么 X,Y,Z,T 这些挂在主链上的点都会被计入答案。 这正是把每个点转成 deg(u)-1 权值后所表达的含义。

于是直接做树上直径式 DP:

  • down[u]:从 u 出发往下走的一条最大点权链
  • ans:经过某个点拼出的一条最大点权路径

DP 转移方程

best1,best2u 的儿子中最大的两条向下链,则:

down[u]=w[u]+best1 down[u]=w[u]+best1
ans=max(ans, w[u]+best1+best2) ans=\max(ans,\ w[u]+best1+best2)

最后非单点树答案为 ans + 2,补回主链两个端点的额外贡献。

最后输出:

  • n=1 时答案是 1
  • 否则答案是 ans + 2

代码

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

const int MAXN = 300005;

int n, m;
vector<int> g[MAXN];
int w[MAXN];
int down_val[MAXN];
int ans;

void dfs(int u, int fa) {
    down_val[u] = w[u];

    int best1 = 0;
    int best2 = 0;

    for (size_t i = 0; i < g[u].size(); i++) {
        int v = g[u][i];
        if (v == fa) {
            continue;
        }
        dfs(v, u);

        int cand = down_val[v];
        if (cand > best1) {
            best2 = best1;
            best1 = cand;
        } else if (cand > best2) {
            best2 = cand;
        }
    }

    down_val[u] = w[u] + best1;
    ans = max(ans, w[u] + best1 + best2);
}

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

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

    for (int i = 1; i <= n; i++) {
        w[i] = (int) g[i].size() - 1;
    }

    ans = 0;
    dfs(1, 0);

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

    // 路径上所有点的 (deg-1) 之和,再补回路径两个端点。
    cout << ans + 2 << '\n';
    return 0;
}

复杂度

一次 DFS,时间复杂度 O(N)O(N),空间复杂度 O(N)O(N)

总结

这题最重要的不是直接枚举路径,而是先把一个点在毛毛虫中的贡献表达清楚。

一旦得到 deg(u)-1 这个点权,题目就直接变成了树上最大路径和问题。

一图流解析

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

一图流解析