题解:洛谷 P1670 Tree Cutting

【题目来源】

洛谷:P1670 [USACO04DEC] Tree Cutting S - 洛谷

【题目描述】

约翰意识到贝茜建设网络花费了他巨额的经费,就把她解雇了。贝茜很愤怒,打算狠狠报复。她打算破坏刚建成的约翰的网络。约翰的网络是树形的,连接着 \(N\) 个牛棚。她打算切断某一个牛棚的电源,使和这个牛棚相连的所有电缆全部中断。之后,就会存在若干子网络。为保证破坏够大,每一个子网的牛棚数不得超过总牛棚数的一半,那哪些牛棚值得破坏呢?

【输入】

\(1\) 行:一个整数 \(N\)

\(2\)\(N\) 行:每行输入两个整数,表示一条电缆的两个端点。

【输出】

按从小到大的顺序,输出所有值得破坏的牛棚。如果没有一个值得破坏,就输出 NONE

【输入样例】

10
1 2
2 3
3 4
4 5
6 7
7 8
8 9
9 10
3 8

【输出样例】

3
8

【核心思想】

  1. 问题分析:给定一棵 \(N\) 个节点的树,求所有满足条件的节点:删除该节点后,所有连通子树的大小都不超过 \(N/2\)。这正是树的重心定义。需要找出所有重心,按编号从小到大输出。这是一个树形DP求重心问题。

  2. 算法选择

    • 两次DFS树形DP:第一次自底向上计算每个节点的子树大小 siz[u];第二次计算删除该节点后的最大连通块大小 maxP[u],若 maxP[u] <= n/2 则为重心
    • 最大连通块计算maxP[u] = max(n - siz[u], max(siz[v])),即父节点方向子树大小与所有子节点方向子树大小的最大值
  3. 关键步骤

    • 建树:读入 \(N\)\(N-1\) 条边,建立无向树
    • 第一次DFS dfs_1(u, fa):计算 siz[u],后序遍历累加所有子节点的子树大小
    • 第二次DFS dfs_2(u, fa)
      • maxP[u] = n - siz[u](父节点方向的子树大小)
      • 遍历子节点 \(v\)maxP[u] = max(maxP[u], siz[v])
      • maxP[u] <= n / 2ok[u] = true
    • 输出:遍历所有节点,输出 ok[i] == true 的编号;若无则输出 NONE
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),两次DFS各遍历整棵树一次
    • 空间复杂度:\(O(N)\),邻接表和DP数组
  5. 树形DP求重心的核心思想

    • 重心的定义:删除该节点后,所有连通子树的最大大小最小。若最大大小 \(\leq N/2\),则该节点为重心
    • 子树大小的后序计算dfs_1 自底向上,siz[u] 先初始化为 \(1\)(自身),再累加所有子节点的 siz[v]
    • 最大连通块的全面考虑:删除 \(u\) 后,树分为若干连通块:每个子节点 \(v\) 形成大小为 siz[v] 的块,以及父节点方向形成大小为 n - siz[u] 的块。maxP[u] 取这些块的最大值
    • 多重心情况:树可能有 \(1\) 个或 \(2\) 个重心,需遍历所有节点判断
    • 适用于"寻找树的重心"问题,核心在于两次DFS分别计算子树大小和最大连通块

【算法标签】

普及 #树形DP

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 10005, M = N * 2;
int n;  // n: 节点数
int h[N], e[M], ne[M], idx;  // 邻接表存储树
int siz[N], maxP[N];  // siz: 子树大小,maxP: 最大子树大小
bool ok[N], flag;  // ok: 标记节点是否是重心,flag: 标记是否有重心
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 第一次DFS,计算每个节点的子树大小
void dfs_1(int u, int fa)
{
    siz[u] = 1;  // 初始化子树大小为1(自身)
    for (int i = h[u]; i != -1; i = ne[i])  // 遍历u的所有子节点
    {
        int v = e[i];  // 子节点v
        if (v == fa)  // 跳过父节点
            continue;
        dfs_1(v, u);  // 递归计算子节点的子树大小
        siz[u] += siz[v];  // 累加子节点的子树大小
    }
}

// 第二次DFS,计算每个节点的最大子树大小,并判断是否是重心
void dfs_2(int u, int fa)
{
    maxP[u] = n - siz[u];  // 考虑u的父节点方向的子树大小
    for (int i = h[u]; i != -1; i = ne[i])  // 遍历u的所有子节点
    {
        int v = e[i];  // 子节点v
        if (v == fa)  // 跳过父节点
            continue;
        dfs_2(v, u);  // 递归计算子节点的最大子树大小
        maxP[u] = max(maxP[u], siz[v]);  // 更新最大子树大小
    }
    if (maxP[u] <= n / 2)  // 如果最大子树大小不超过n/2,则是重心
        ok[u] = true;
}

int main()
{
    cin >> n;  // 输入节点数
    memset(h, -1, sizeof(h));  // 初始化邻接表

    for (int i = 1; i < n; i++)  // 输入n-1条边
    {
        int u, v;
        cin >> u >> v;
        add(u, v), add(v, u);  // 添加无向边
    }

    dfs_1(1, 0);  // 第一次DFS,计算子树大小
    dfs_2(1, 0);  // 第二次DFS,判断重心

    for (int i = 1; i <= n; i++)  // 遍历所有节点
        if (ok[i])  // 如果节点i是重心
        {
            cout << i << endl;  // 输出重心编号
            flag = true;  // 标记有重心
        }

    if (!flag)  // 如果没有找到重心
        cout << "NONE" << endl;  // 输出NONE

    return 0;
}

【运行结果】

10
1 2
2 3
3 4
4 5
6 7
7 8
8 9
9 10
3 8
3
8
posted @ 2026-08-10 12:27  团爸讲算法  阅读(11)  评论(0)    收藏  举报