题解:洛谷 AT_agc001_c Shorten Diameter

【题目来源】

洛谷:AT_agc001_c [AGC001C] Shorten Diameter - 洛谷

【题目描述】

给你一棵 \(N\) 个点的无向树,定义点 \(u\)\(v\) 之间的距离是从 \(u\)\(v\) 的简单路径上的边数。

你需要删除一些点,使树的直径小于等于 \(K\),当且仅当删除某点不会对树的联通性产生影响时才可以删除。问至少删除多少点才可以满足要求。

【输入】

第一行两个整数 \(N, K\)

之后 \(N - 1\) 行描述一棵树。

【输出】

一个整数,表示最少删掉点的个数。

【输入样例】

6 2
1 2
3 2
4 2
1 6
5 6

【输出样例】

2

【核心思想】

  1. 问题分析:给定一棵 \(N\) 个节点的无向树,要求删除最少的节点,使得剩余树的直径不超过 \(K\)。删除节点后剩余部分必须保持连通(即只能删除一些“外部”节点,实际上保留的是一个连通子图)。问题等价于:在树中保留一个最大的连通子图,使其直径 \(\le K\),然后 \(答案 = N - 最大保留节点数\)。在树上,任意一个连通子图的直径 \(\le K\) 当且仅当它可以被一个半径 \(\le \lfloor K/2 \rfloor\) 的“中心”覆盖——若 \(K\) 为偶数,中心是一个点;若 \(K\) 为奇数,中心是一条边。因此,只需枚举所有可能的中心点或中心边,统计从中心出发在半径内能覆盖的节点数,取最大值。

  2. 算法选择

    • 枚举中心(点/边)+ DFS 统计:由于 \(N \le 2000\),可以枚举所有可能的中心点(\(N\) 个)和中心边(\(N-1\) 条),对每个中心执行 DFS 统计距离不超过给定半径的节点数。取最大值作为最多能保留的节点数。
    • 分奇偶处理
      • \(K\) 为偶数,中心为一个点,半径 \(r = K/2\),保留所有到该点距离 \(\le r\) 的节点。
      • \(K\) 为奇数,中心为一条边 \((u, v)\),半径 \(r = (K-1)/2\),保留所有到 \(u\) 距离 \(\le r\) 且不经过 \(v\) 的节点,以及所有到 \(v\) 距离 \(\le r\) 且不经过 \(u\) 的节点。
    • 正确性:任意直径 \(\le K\) 的连通子图必定可以被某个点或某条边(作为中心)覆盖,且上述枚举覆盖了所有可能的中心,因此能找出最大的保留集合。
  3. 关键步骤

    • 读入与建图:读入 \(N, K\)\(N-1\) 条边,构建无向邻接表。
    • 初始化max_keep = 0,表示最多能保留的节点数。
    • 情况1:\(K\) 为偶数
      • 枚举每个节点 \(i\) 作为中心。
      • \(i\) 出发 DFS,深度限制为 \(K/2\),统计能访问到的节点数 tot
      • 更新 max_keep = max(max_keep, tot)
    • 情况2:\(K\) 为奇数
      • 枚举每条无向边 \((u, v)\) 作为中心边。
      • \(v\) 出发 DFS(禁止经过 \(u\)),深度限制为 \((K-1)/2\),统计节点数。
      • \(u\) 出发 DFS(禁止经过 \(v\)),深度限制为 \((K-1)/2\),统计节点数。
      • 两者之和即为以该边为中心能保留的节点数,更新 max_keep
    • 输出答案N - max_keep
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^2)\),枚举 \(O(N)\) 个中心(点或边),每次 DFS 遍历 \(O(N)\) 个节点,\(N \le 2000\),可接受。
    • 空间复杂度:\(O(N)\),存储树和辅助栈。
  5. 枚举中心 + DFS 的核心思想

    • 直径与半径的等价性:树上连通子图的直径 \(\le K\) 等价于存在一个中心(点或边)使得子图中所有节点到中心的距离 \(\le \lfloor K/2 \rfloor\)(对奇数情况,中心为边,两端分别控制两侧)。
    • 覆盖最大化:为了保留尽可能多的节点,应选择能够覆盖最多节点的中心。直接枚举所有可能的中心即可找到最优解,无需复杂的树形 DP。
    • 适用场景:适用于 \(N\) 较小(\(\le 2000\))且需要删除最少节点使树的直径减小的题目,是暴力搜索与树的性质结合的经典思路。

【算法标签】

普及+ #树形DP

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 2005, M = N * 2;  // N: 最大节点数, M: 最大边数(无向图双向边)
int n, k;                       // n: 节点数, k: 直径限制
int h[N], e[M], ne[M], idx;     // 链式前向星存图
int tot, ans;                   // tot: 当前保留的节点数, ans: 最多能保留的节点数

// 链式前向星:添加无向边
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// DFS:从u出发,最多走step步,统计能到达的节点数
// 这些节点构成一个以u为中心、半径为step的"球"
// fa是父节点,防止走回头路
void dfs(int u, int fa, int step)
{
    tot++;                      // 当前节点u被保留

    if (step == 0)
        return;                 // 步数用完,不再扩展

    // 遍历u的所有邻接点
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if (v == fa)
            continue;           // 跳过父节点,避免走回头路

        dfs(v, u, step - 1);    // 递归向v扩展,剩余步数减1
    }
}

int main()
{
    cin >> n >> k;              // 读入节点数和直径限制

    // 初始化链式前向星
    memset(h, -1, sizeof(h));

    // 读入n-1条边(树结构)
    for (int i = 1; i < n; i++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);   // 无向图,双向建边
    }

    // ========== 核心思路 ==========
    // 要使直径 <= K,等价于找到一个最大的节点集合,使得集合内任意两点距离 <= K
    // 答案 = n - 最多保留的节点数(即删除最少的点 = 总数 - 最多保留)

    // 情况1:K为偶数
    // 最优策略:选择一个中心点,保留距离中心不超过 K/2 的所有点
    // 这些点构成的子树直径不超过 K(以中心为根,最远的两点距离为 2*(K/2) = K)
    if (k % 2 == 0)
    {
        for (int i = 1; i <= n; i++)
        {
            tot = 0;            // 重置计数
            dfs(i, 0, k / 2);   // 以i为中心,半径为K/2的球
            ans = max(ans, tot); // 更新最多保留的节点数
        }
    }
    // 情况2:K为奇数
    // 最优策略:选择一条边作为"中心边",保留两端分别距离该边不超过 (K-1)/2 的点
    // 即:以边(u,v)为中心,u侧保留距离u不超过 (K-1)/2 的点,v侧保留距离v不超过 (K-1)/2 的点
    // 最远的两点距离 = (K-1)/2 + 1 + (K-1)/2 = K
    else
    {
        for (int u = 1; u <= n; u++)
        {
            // 遍历u的所有邻接边
            for (int i = h[u]; i != -1; i = ne[i])
            {
                tot = 0;        // 重置计数

                int v = e[i];   // 邻接点v,边(u,v)作为中心边

                // 从v出发(不经过u),保留距离v不超过 (K-1)/2 的点
                dfs(v, u, (k - 1) / 2);
                // 从u出发(不经过v),保留距离u不超过 (K-1)/2 的点
                dfs(u, v, (k - 1) / 2);

                ans = max(ans, tot); // 更新最多保留的节点数
            }
        }
    }

    // 输出最少删除的点数 = 总点数 - 最多保留的点数
    cout << n - ans << endl;

    return 0;
}

【运行结果】

6 2
1 2
3 2
4 2
1 6
5 6
2
posted @ 2026-09-01 23:08  团爸讲算法  阅读(12)  评论(0)    收藏  举报