题解:洛谷 P11378 燃烧

【题目来源】

洛谷:P11378 [GESP202412 七级] 燃烧 - 洛谷

【题目描述】

小杨有一棵包含 \(n\) 个节点的树,其中节点的编号从 \(1\)\(n\)。节点 \(i\) 的权值为 \(a_i\)

小杨可以选择一个初始节点引燃,每个燃烧的节点会将其相邻节点中权值严格小于自身权值的节点也引燃,火焰会在节点间扩散直到不会有新的节点被引燃。

小杨想知道在合理选择初始节点的情况下,最多可以燃烧多少个节点。

【输入】

第一行包含一个正整数 \(n\),代表节点数量。

第二行包含 \(n\) 个正整数 \(a_1,a_2,\dots,a_n\),代表节点权值。

之后 \(n-1\) 行,每行包含两个正整数 \(u_i,v_i\),代表存在一条连接节点 \(u_i\)\(v_i\) 的边。

【输出】

输出一个正整数,代表最多燃烧的节点个数。

【输入样例】

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

【输出样例】

3

【核心思想】

  1. 问题分析:给定一棵 \(n\) 个节点的树,每个节点有权值 \(a_i\)。选择一个初始节点引燃,燃烧的节点会将相邻且权值严格小于自身的节点引燃,火焰持续扩散。求最多能燃烧的节点数。这是一个树形DP + 记忆化搜索问题,关键在于每个节点作为起点时,火焰沿权值严格递减的方向扩散,求最长递减路径长度。

  2. 算法选择

    • 树形DP(记忆化搜索)dp[u] 表示以节点 \(u\) 为起点的最长递减路径长度(即最多能燃烧的节点数)
    • DFS 自顶向下:从节点 \(u\) 出发,只能向权值严格小于 \(a_u\) 的邻居扩散,递归计算各分支的最长路径
  3. 关键步骤

    • 读入数据\(n\)、权值数组 \(a[1..n]\)\(n-1\) 条边建立无向树
    • 初始化dp 数组设为 \(-1\)(未计算),ans = 0
    • 记忆化DFS dfs(u)
      • dp[u] != -1,直接返回
      • res = 1(至少包含自身)
      • 遍历邻居 \(j\):若 a[u] > a[j]res += dfs(j)
      • dp[u] = res,返回 res
    • 枚举起点:遍历 \(i\)\(1\)\(n\)ans = max(ans, dfs(i))
    • 输出 ans
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个节点最多被计算一次,每条边最多被遍历一次
    • 空间复杂度:\(O(n)\),邻接表、dp 数组、递归栈
  5. 树形DP记忆化搜索的核心思想

    • 状态定义dp[u] 表示以 \(u\) 为火焰起点的最大燃烧节点数,具有最优子结构(从 \(u\) 出发的最优路径 = \(1\) + 所有可扩散邻居的最优路径之和)
    • 严格递减约束:火焰只能向权值更小的邻居扩散,形成有向无环图(DAG),保证无后效性
    • 记忆化避免重复:树中无环,但一个节点可能作为多个节点的邻居被多次访问。记忆化确保每个节点只计算一次
    • 枚举所有起点:火焰起点可以是任意节点,需遍历所有节点取最大值
    • 注意:该DP计算的是以 \(u\) 为根的"递减树"的节点总数,而非简单路径长度。因为火焰会同时向所有满足条件的邻居扩散,是树形结构而非链状
    • 适用于"树上带权值约束的最大扩散/覆盖"问题,核心在于记忆化DFS和最优子结构的利用

【解题思路】

【算法标签】

普及 #树形DP

【代码详解】

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

const int N = 100005;     // 最大节点数
const int M = N * 2;      // 无向图边数要×2
int n;                    // 节点数量
int a[N];                 // a[i]: 节点i的值
int dp[N];                // dp[i]: 以节点i为起点的最长递降路径长度
int ans;                  // 全局答案,最长路径长度
int h[N], e[M], ne[M], idx;  // 邻接表存储图

// 添加一条a到b的无向边
void add(int a, int b)
{
    e[idx] = b;          // 存储终点
    ne[idx] = h[a];      // 头插法
    h[a] = idx++;        // 更新头节点
}

// 深度优先搜索,返回以u为起点的最长递降路径长度
int dfs(int u)
{
    // 记忆化:如果已经计算过,直接返回结果
    if (dp[u] != -1)
    {
        return dp[u];
    }

    // 至少包含自己,所以初始长度为1
    int res = 1;

    // 遍历u的所有邻居节点
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];  // 邻居节点j

        // 如果节点u的值大于邻居节点j的值,说明可以向下延伸
        if (a[u] > a[j])
        {
            // 递归计算以j为起点的路径长度
            // 加上u节点自身
            res += dfs(j);
        }
    }

    // 记忆化存储结果
    dp[u] = res;
    return dp[u];
}

int main()
{
    cin >> n;  // 输入节点数

    // 初始化邻接表
    memset(h, -1, sizeof(h));

    // 输入每个节点的值
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 输入n-1条边,构建树
    for (int i = 1; i < n; i++)
    {
        int u, v;
        cin >> u >> v;
        add(u, v);  // 添加u->v的边
        add(v, u);  // 添加v->u的边,因为是无向图
    }

    // 初始化dp数组为-1,表示未计算
    memset(dp, -1, sizeof(dp));

    // 以每个节点为起点计算最长递降路径
    ans = 0;  // 初始化答案为0
    for (int i = 1; i <= n; i++)
    {
        ans = max(ans, dfs(i));
    }

    // 输出最长递降路径长度
    cout << ans << endl;

    return 0;
}

【运行结果】

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