题解:洛谷 CF911F Tree Destruction

【题目来源】

洛谷:CF911F Tree Destruction - 洛谷

【题目描述】

给定一棵有 \(n\) 个顶点的无权树。然后对这棵树依次进行 \(n-1\) 次操作。每次操作包括以下步骤:

  1. 选择树上的两个叶子节点;
  2. 将这两个叶子间简单路径的长度加到当前答案中;
  3. 从树中移除选中的其中一个叶子节点。

初始答案为 \(0\)。显然,经过 \(n-1\) 次这样的操作后,树中只剩下一个顶点。

请计算你能获得的最大答案,并构造一种操作顺序,使你能获得这个最大答案!

【输入】

第一行包含一个整数 \(n\)\(2\le n\le 2\cdot10^{5}\))——树的顶点数。

接下来的 \(n-1\) 行描述树的边,每行用形式 \(a_i,b_i\) 给出一条边(\(1\le a_i, b_i\le n\)\(a_i\ne b_i\))。保证给出的图是一棵树。

【输出】

第一行输出一个整数——你能获得的最大答案。

接下来 \(n-1\) 行,按操作顺序输出每次操作,格式为 \(a_i,b_i,c_i\),其中 \(a_i,b_i\) 表示本次操作所选择的两个叶子结点(\(1\le a_i, b_i\le n\)),\(c_i\)\(1\le c_i\le n\)\(c_i=a_i\)\(c_i=b_i\))表示本次操作中被从树中移除的叶子。

具体见样例。

【输入样例】

3
1 2
1 3

【输出样例】

3
2 3 3
2 1 1

【核心思想】

  1. 问题分析:给定一棵 \(n\) 个节点的无权树,进行 \(n-1\) 次操作。每次选择树上的两个叶子,将两点间路径长度累加到答案中,然后删除其中一个叶子,最终只剩一个节点。目标是最大化答案总和,并构造操作序列。本质上是树上的贪心删除策略——为了最大化每次路径长度,应尽量让被删除的叶子与树的直径端点配对,确保每次路径尽可能长。

  2. 算法选择

    • 两次 DFS 求直径:先找出树的一条直径,记录两个端点 \(l\)\(r\),并标记直径路径上的所有节点。
    • 贪心删除非直径叶子:对于所有不在直径上的节点,将其视为“分支叶子”,它们最终会与直径的某个端点配对(选择到 \(l\)\(r\) 距离更大的那个),累加该距离,并记录操作。
    • 最后处理直径路径:直径上的节点依次与保留的端点配对(从一端向另一端删除),每次累加当前点到保留端点的距离。
    • 正确性证明:由于每次删除的叶子若不在直径上,它与直径端点的距离是它能形成的最大路径长度;直径上的节点最后删除,确保所有非直径节点都能与最优的直径端点配对,从而获得最大总和。
  3. 关键步骤

    • 读入与建图:读入 \(n\)\(n-1\) 条边,构建无向邻接表。
    • 两次 DFS 求直径
      • 第一次从任意点(如 1)出发,找到最远点 \(l\)
      • 第二次从 \(l\) 出发,找到最远点 \(r\),并记录每个节点的父节点 pre,用于回溯直径路径。
    • 标记直径路径:从 \(r\) 沿 pre 回溯到 \(l\),将路径上的节点标记为 col=1
    • 处理非直径节点:对直径上的每个节点,递归处理其非直径子树。对于每个非直径节点 \(x\),找到其在直径上的最近祖先 \(rt\),计算 \(x\)\(l\) 的距离 \(d_x\)\(x\)\(r\) 的距离(\(d_x + d_r - 2*d_{rt}\)),选择较大值累加到答案,并记录配对操作。
    • 处理直径路径:从 \(r\)\(l\) 遍历,每次将当前点与 \(l\) 配对,累加距离,并记录操作(删除当前点),直到只剩下 \(l\)
    • 输出:第一行输出最大答案,随后 \(n-1\) 行按操作顺序输出每次选择的两个叶子及删除的叶子(通常删除第二个)。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),两次 DFS 遍历整棵树,后续处理也是线性。
    • 空间复杂度:\(O(n)\),存储邻接表和辅助数组。
  5. 贪心 + 直径的核心思想

    • 最优策略的结构:为了最大化每次路径长度,应尽量让被删除的叶子与树的直径端点配对。所有非直径节点最终都会与直径的一个端点配对,而直径上的节点会依次与保留的端点配对。
    • 距离计算的简化:利用直径端点的距离信息,可以快速得到任意节点到直径端点的距离,避免重复计算。
    • 操作的可行性:每次删除的必须是当前叶子。在算法中,非直径节点删除时仍是叶子,直径上的节点在非直径节点全部删除后成为叶子,因此操作顺序合法。
    • 适用场景:适用于需要最大化“删除叶子并累加路径长度”的树形贪心问题,是树的直径与贪心策略结合的经典例题。

【算法标签】

提高+ #树形DP

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long           // 将 int 定义为 long long,防止答案溢出
typedef pair<int, int> PII;     // 存储操作对:两个叶子节点
const int N = 200005, M = 2 * N;    // N: 最大节点数, M: 最大边数(无向图双向边)
int n;                          // 树的顶点数
int h[N], e[M], w[M], ne[M], idx;   // 链式前向星存图
int pre[N];                     // pre[u]: DFS时u的父节点(用于回溯直径路径)
int d[N];                       // d[u]: 从某个起点到u的距离
int col[N];                     // col[u]: 标记u是否在主直径路径上
int ans;                        // 最大答案(总路径长度之和)
int l, r, p, maxd;              // l: 直径左端点, r: 直径右端点, p: 临时最远点, maxd: 当前最大距离
vector<PII> s;                  // 存储所有操作的叶子节点对

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

// DFS:求树的直径端点,同时记录父节点信息
void dfs(int u, int fa)
{
    pre[u] = fa;                // 记录u的父节点

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

        d[v] = d[u] + w[i];       // 更新v到起点的距离

        // 更新最远点(距离相同时优先编号小的)
        if (d[v] > maxd || (d[v] == maxd && v < p))
        {
            maxd = d[v];
            p = v;
        }

        dfs(v, u);              // 递归DFS
    }
}

// work:处理以x为根的子树,rt是当前子树在直径上的根节点
// 对于非直径路径上的叶子节点,计算最优配对策略
void work(int x, int rt)
{
    // 先递归处理子节点
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int y = e[i];
        if (y == pre[x]) continue;  // 跳过父节点
        if (d[y] > d[x])            // y是x的子节点(距离递增方向)
            work(y, col[y] ? y : rt);   // 如果y在直径上,rt更新为y,否则继承rt
    }

    // 如果x不在直径路径上,则需要将x作为叶子删除
    // 选择配对对象使得路径长度最大
    if (!col[x])
    {
        // 比较两种配对策略:
        // 1. x与直径左端点l配对:路径长度 = d[x](x到l的距离)
        // 2. x与直径右端点r配对:路径长度 = d[x] + d[r] - 2*d[rt](x到r的距离,rt是x在直径上的最近祖先)
        if (d[x] > d[x] + d[r] - d[rt] * 2)
        {
            ans += d[x];
            s.push_back({l, x});
        }
        else
        {
            ans += d[x] + d[r] - d[rt] * 2;
            s.push_back({r, x});
        }
    }
}

signed main()                   // 使用 signed 替代 int,因为 #define int long long
{
    cin >> n;                   // 读入顶点数

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

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

    // ========== 第一步:求树的直径 ==========
    // 第一次DFS:从任意点1出发,找到最远点p
    dfs(1, 0);
    l = p;                      // l是直径的一个端点
    d[l] = maxd = 0;            // 重置距离数组,准备第二次DFS

    // 第二次DFS:从l出发,找到最远点r
    dfs(l, 0);
    r = p;                      // r是直径的另一个端点

    // 标记直径路径上的所有点
    for (int i = r; i; i = pre[i])
        col[i] = 1;

    // ========== 第二步:处理非直径路径上的叶子节点 ==========
    // 对于直径上的每个分支,递归处理其叶子节点
    work(l, l);

    // ========== 第三步:处理直径路径上的点 ==========
    // 从r向l遍历,依次删除直径上的点(保留l到最后)
    for (; l != r; r = pre[r])
    {
        ans += d[r];            // 当前配对(l, r)的路径长度
        s.push_back({l, r});    // 记录操作
    }

    // ========== 输出结果 ==========
    cout << ans << endl;

    // 输出所有操作,每次删除第二个叶子节点
    for (auto i : s)
        cout << i.first << " " << i.second << " " << i.second << endl;

    return 0;
}

【运行结果】

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