题解:洛谷 P10135 Potion Farming

【题目来源】

洛谷:P10135 [USACO24JAN] Potion Farming S - 洛谷

【题目描述】

你正在玩你最喜欢的手机游戏。在尝试收集药水,以便有机会击败传说中的牛头怪boss。游戏地图是一系列标有1...N的房间,这些房间由N-1条边连接形成一棵树\((2\le N\le 10^5)\)

你可以通过进行一系列的“遍历”来探索地图。一次遍历是从房间1到树中任何其他房间的一条简单路径。一次遍历结束后,你可以从房间1开始另一次遍历。一旦地图的每个房间至少被遍历过一次,地图就算完成。你的主要目标是用最少的遍历次数完成地图。你的次要目标是尽可能多地收集药水。在一次遍历开始前,一瓶药水会在地图上的某个房间生成。通过在当前遍历中生成药水的房间你就可以捡起它。如果你没有捡起药水,那么它将在当前遍历结束后消失,所以你不能在未来的遍历中捡起它。作为一个聪明的程序员,在查看游戏文件后,你能够在接下来的N次遍历之前找出药水将出现在哪里。如果你在最少的遍历次数内完成地图,你可以从地图上收集到的最大数量的药水是多少?

【输入】

输入的第一行包含一个整数N,表示地图中房间的数量。然后是N个空格分隔的整数\(p_1p_2\dots p_N\),其中\(1\le p_i\le N\)\(p_i\)表示在第i次遍历之前药水会出现在哪个房间。

最后,接着是N-1行,每行有两个用空格分隔的整数\(a b(1\le a,b\le N)\),代表房间a和b之间的一条边。可以保证这些边组成了一棵树。

【输出】

输出一行包含一个整数,即你可以在最少遍历次数的情况下从地图中获得的最大药水量。

【输入样例】

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

【输出样例】

2

【核心思想】

  1. 问题分析:给定一棵 \(N\) 个节点的树,\(N\) 次遍历中药水依次出现在房间 \(p_i\)。每次遍历是从根节点 \(1\) 到某叶子的一条简单路径。要求用最少遍历次数覆盖所有房间(即覆盖所有叶子),并在最少遍历次数下最大化收集的药水数。本质上是树形 DP + 贪心分配问题:最少遍历次数等于叶子数(每条叶子路径至少一次),药水收集取决于药水位置是否在必经路径上。

  2. 算法选择

    • 树形 DP 求叶子数dfs 计算每棵子树的叶子数量 \(leaf[x]\)
    • 贪心分配药水:前 \(leaf[1]\) 个药水(优先级最高,即编号最小)优先分配给叶子数多的子树
    • 自底向上 DP 汇总calc 从叶子向根递推,每棵子树最多收集的药水数不超过其叶子数
  3. 关键步骤

    • 建图:读入 \(N\),药水序列 \(p[1..N]\),以及 \(N-1\) 条边建无向树
    • DFS 求叶子数:从根 \(1\) 开始,leaf[x] 累加所有子树的叶子数,非根叶子节点 \(leaf[x] = 1\)
    • 贪心初始化:前 \(leaf[1]\) 个药水的位置 \(p_i\) 各加 \(1\)(这些药水在最优遍历中可被收集)
    • DP 汇总calc(x) 累加所有子节点的 \(d[y]\),然后 \(d[x] = \min(d[x], leaf[x])\)(药水数不超过叶子数,每个叶子路径最多贡献一个)
    • 输出 \(d[1]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),两次 DFS/DP 遍历树
    • 空间复杂度:\(O(N)\),邻接表和数组存储
  5. 树形 DP + 贪心分配的核心思想

    • 最少遍历次数 = 叶子数:每条从根到叶子的路径至少遍历一次,且存在方案恰好 \(leaf[1]\) 次覆盖全树
    • 药水的路径覆盖:药水在房间 \(u\) 出现,只有当某次遍历经过 \(u\) 时才能收集,而遍历路径是根到叶子的链
    • 子树容量限制:以 \(x\) 为根的子树最多贡献 \(leaf[x]\) 个药水(每个叶子路径最多一个),故 \(d[x] \leq leaf[x]\)
    • 贪心优先分配:前 \(leaf[1]\) 个药水位置是"最优可收集"的,优先分配给子树容量大的分支可最大化总数
    • 适用于树覆盖、资源分配、贪心容量限制类问题

【解题思路】

【算法标签】

普及 #树形DP

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;

int n, p[N], leaf[N], d[N];
// leaf[x] -- 以 x 为根的子树中的叶子节点数量
// d[x]    -- 以 x 为根的子树最多能获得的药水数量
vector<int> e[N]; // 邻接表存储树

// 深度优先搜索计算每个子树的叶子节点数量
void dfs(int x, int fa)
{
    // 如果 x 不是根节点且是叶子节点(度为1),则 leaf[x] = 1
    if (x != 1 && e[x].size() == 1)
    {
        leaf[x] = 1;
    }

    // 遍历所有子节点
    for (int i = 0; i < e[x].size(); i++)
    {
        int y = e[x][i];
        if (y == fa) continue; // 避免回溯父节点
        dfs(y, x);
        leaf[x] += leaf[y]; // 累加子树的叶子数量
    }
}

// 计算每个子树最多能获得的药水数量
void calc(int x, int fa)
{
    // 遍历所有子节点
    for (int i = 0; i < e[x].size(); i++)
    {
        int y = e[x][i];
        if (y == fa) continue; // 避免回溯父节点
        calc(y, x);
        d[x] += d[y]; // 累加子树的药水数量
    }
    d[x] = min(d[x], leaf[x]); // 药水数量不超过叶子数量
}

int main()
{
    scanf("%d", &n); // 输入节点数量

    // 输入每个节点的优先级 p[i]
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &p[i]);
    }

    // 构建树的邻接表
    for (int i = 1; i < n; i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        e[a].push_back(b);
        e[b].push_back(a);
    }

    dfs(1, 0); // 从根节点开始计算叶子数量

    // 根据优先级分配初始药水(前 leaf[1] 个优先级最高的节点获得 1 药水)
    for (int i = 1; i <= leaf[1]; i++)
    {
        d[p[i]]++;
    }

    calc(1, 0); // 从根节点开始计算最大药水数量

    printf("%d\n", d[1]); // 输出根节点能获得的最大药水数量
    return 0;
}

【运行结果】

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