题解:洛谷 P11249 小杨寻宝

【题目来源】

洛谷:P11249 [GESP202409 七级] 小杨寻宝 - 洛谷

【题目描述】

小杨有一棵包含 \(n\) 个节点的树,树上的一些节点放置有宝物。

小杨可以任意选择一个节点作为起点并在树上移动,但是小杨只能经过每条边至多一次,当小杨经过一条边后,这条边就会消失。小杨每经过一个放置有宝物的节点就会取得该宝物。

小杨想请你帮他判断自己能否成功取得所有宝物。

【输入】

本题单个测试点内有多组测试数据。输入第一行包含一个正整数 \(t\),代表测试用例组数。
接下来是 \(t\) 组测试用例。对于每组测试用例,一共 \(n+1\) 行。

第一行包含一个正整数 \(n\),代表树的节点数。
第二行包含 \(n\) 个非负整数 \(a_1, a_2, \dots a_n\),其中如果 \(a_i = 1\),则节点 \(i\) 放置有宝物;若 \(a_i = 0\),则节点 \(i\) 没有宝物。
之后 \(n - 1\) 行,每行包含两个正整数 \(x_i, y_i\),代表存在一条连接节点 \(x_i\)\(y_i\) 的边。

【输出】

对于每组测试数据,如果小杨能成功取得所有宝物,输出 Yes,否则输出 No。

【输入样例】

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

【输出样例】

Yes
No

【核心思想】

  1. 问题分析:给定一棵 \(n\) 个节点的树,部分节点有宝物。小杨从某节点出发,每条边最多经过一次(经过后消失),要求经过所有有宝物的节点。判断是否存在这样的路径。这是一个树形DFS + 贪心验证问题,关键在于分析有宝物节点的分布约束:每个非根节点的子树中,若有多于1个"需要访问的宝物分支",则无法在不重复经过边的情况下覆盖。

  2. 算法选择

    • 树形DFS(后序遍历):自底向上统计每个节点的子树中需要访问的宝物节点数量
    • 贪心约束验证:对每个节点,检查其子树中需要访问的分支数是否超过限制(根节点最多2个分支,非根节点最多1个分支)
  3. 关键步骤

    • 读入数据\(t\) 组测试用例,每组读入 \(n\)、宝物数组 \(a[1..n]\)\(n-1\) 条边
    • 找根节点:任选一个 \(a_i = 1\) 的节点作为根(若没有宝物则 trivially Yes)
    • DFS 自底向上处理(参数 \(u\) 当前节点,\(fa\) 父节点):
      • res = 子树中需要访问的宝物节点之和
      • 遍历子节点 \(j\)(跳过父节点):
        • 递归 dfs(j, u)
        • res += a[j](子节点若有宝物或需要经过,则计入)
      • res > 0,则 a[u] = 1(当前节点需要被经过)
      • 约束检查
        • 根节点:若 res > 2flag = false(根最多连接2个需要访问的分支)
        • 非根节点:若 res > 1flag = false(非根节点最多连接1个需要访问的分支)
    • 输出 flag ? "Yes" : "No"
  4. 时间/空间复杂度

    • 时间复杂度:\(O(t \cdot n)\),每组数据一次DFS遍历整棵树
    • 空间复杂度:\(O(n)\),邻接表和递归栈
  5. 树形DFS贪心约束的核心思想

    • 边消失约束的本质:每条边只能走一次,意味着从父节点进入子树后,若子树中有多个分散的宝物,无法在不重复走边的情况下全部收集
    • 分支数限制:非根节点最多只能有1个"需要深入访问的子分支",否则进入该分支后无法再去另一个分支(边已消失)。根节点可以有2个(作为起点和终点)
    • 自底向上传播:子树中若有宝物需要收集,则父节点必须经过。a[u] 被置1表示该节点是"必须经过的节点"
    • 贪心验证的正确性:若某节点违反分支数限制,则不存在合法路径;若所有节点都满足,则可以构造一条欧拉路径式的遍历
    • 适用于"树上一笔画覆盖指定节点"的可行性判断问题

【解题思路】

【算法标签】

普及 #DFS-树

【代码详解】

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

const int N = 100005, M = N * 2;

int T;               // 测试用例数量
int n, a[N], root;  // n: 节点数, a[i]: 节点i的初始值, root: 根节点
int h[N], e[M], ne[M], idx;  // 邻接表存储树
bool flag;          // 标志位,表示当前树是否合法

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

// 深度优先搜索
// u: 当前节点
// fa: 父节点
void dfs(int u, int fa)
{
    if (!flag) return;  // 如果已经发现不合法,直接返回

    int res = 0;  // 记录子节点值的和

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

        // 递归处理子节点
        dfs(j, u);

        // 累加子节点的值
        res += a[j];
    }

    // 如果子节点的和不为0,将当前节点设为1
    if (res)
    {
        a[u] = 1;
    }

    // 检查约束条件
    if ((u == root && res > 2) || (u != root && res > 1))
    {
        flag = 0;  // 不满足条件,标记为非法
    }
}

int main()
{
    cin >> T;  // 读取测试用例数

    while (T--)
    {
        cin >> n;  // 读取节点数

        // 初始化邻接表
        memset(h, -1, sizeof(h));
        idx = 0;
        root = 0;
        flag = 1;  // 初始假设树是合法的

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

        // 找到值为1的节点作为根节点
        for (int i = 1; i <= n; i++)
        {
            if (a[i] == 1)
            {
                root = i;
                break;
            }
        }

        // 读取树的边
        for (int i = 1; i < n; i++)
        {
            int u, v;
            cin >> u >> v;
            add(u, v);
            add(v, u);
        }

        // 从根节点开始DFS
        dfs(root, 0);

        // 输出结果
        if (flag)
        {
            cout << "Yes" << endl;
        }
        else
        {
            cout << "No" << endl;
        }
    }

    return 0;
}

【运行结果】

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