1 条题解

  • 0
    @ 2025-11-2 19:07:00

    题目概述

    nn 个嫌疑人,通过 n1n-1 条线索连接成一棵树。每条线索连接两个嫌疑人,有权重 ww。每个嫌疑人节点上有若干贴纸(数字),数量等于该节点的度数。

    线索按时间顺序出现,每次出现线索 (A,B,w)(A,B,w) 时:

    • AABB 之间连边,权重为 ww
    • AA 上贴数字 cAc_A,在 BB 上贴数字 cBc_B
    • 必须满足 wcA+cBw \leq c_A + c_B

    已知最终的树结构和每个嫌疑人所有的贴纸数字(按从上到下的顺序),要求判断是否存在合法的线索出现顺序,如果存在则输出一种可能的顺序。


    问题分析

    关键约束

    1. 线索条件wcA+cBw \leq c_A + c_B
    2. 贴纸顺序:贴纸从上到下对应线索出现的时间顺序
    3. 树结构:最终形成一棵树

    问题难点

    • 需要为每条边分配两个端点的贴纸
    • 贴纸的分配必须满足时间顺序约束
    • 需要重建整个时间线

    算法思路

    核心思想:自底向上的贪心匹配

    步骤1:建立树结构

    • 用邻接表存储树
    • 记录每条边的权重和编号

    步骤2:DFS遍历树

    从叶子节点向根节点处理,对于每个节点 uu

    • 收集所有子节点的"剩余需求"
    • 将当前节点的贴纸与连接的边进行匹配

    步骤3:匹配规则

    对于节点 uu,设其度数为 dd,有 dd 个贴纸 c1,c2,,cdc_1, c_2, \ldots, c_d(按时间顺序)。

    对于每条边 (u,v)(u,v),设其权重为 ww

    • 如果 vvuu 的子节点,已知 vvuu 的"上传值" upvup_v
    • 那么 uu 需要提供的值为 wupvw - up_v

    将所有的需求排序,将贴纸排序,进行贪心匹配。

    步骤4:构建时间依赖图

    根据匹配结果,构建边之间的时间先后关系,然后拓扑排序输出顺序。


    算法详解

    数据结构

    vector<tuple<int, int, int>> e(n - 1);  // 边列表 (u, v, w)
    vector<vector<tuple<int, int, int>>> g(n);  // 邻接表 (邻居, 权重, 边编号)
    vector<vector<int>> c(n);  // 每个节点的贴纸
    vector<vector<int>> l(n);  // l[u][i] = 贴纸i对应的边编号
    

    DFS匹配过程

    function<void(int, int)> dfs = [&](int u, int f) {
        // 先递归处理所有子节点
        for (auto [i, w, n] : g[u])
            if (i != f) 
                dfs(i, u);
        
        // 准备匹配
        vector<pii> s(c[u].size()), o(c[u].size());
        
        // o: 贴纸排序 (值, 原始索引)
        for (int i = 0; i < c[u].size(); i++)
            o[i] = {c[u][i], i};
        sort(o.begin(), o.end());
        
        if (u != 0) {  // 非根节点
            int p = 0;
            // s: 需求列表 (需求值, 边编号)
            for (auto [i, w, n] : g[u]) {
                if (i != f) 
                    s[p++] = {w - up[i], n};
                else 
                    s[c[u].size() - 1] = {0, n};  // 父边的需求特殊处理
            }
            
            sort(s.begin(), prev(s.end()));  // 排序前d-1个需求
            
            // 贪心匹配
            int d = 0;
            for (int i = 0; i < c[u].size() - 1; i++) {
                if (o[i + d].first < s[i].first) {
                    if (d) return false;  // 无法匹配
                    d = 1;
                    up[u] = o[i].first;   // 记录上传值
                    ps[u] = o[i].second;  // 记录使用的贴纸
                    
                    if (o[i + d].first < s[i].first) 
                        return false;     // 仍然不满足
                }
                l[u][o[i + d].second] = s[i].second;  // 记录匹配关系
            }
            
            if (!d) {  // 如果还没使用上传值
                up[u] = o.back().first;
                ps[u] = o.back().second;
            }
            l[u][ps[u]] = s.back().second;  // 父边匹配最后一个贴纸
        } else {  // 根节点特殊处理
            // 根节点的所有边都是子边
            for (int i = 0; i < c[u].size(); i++) {
                auto [v, w, n] = g[u][i];
                s[i] = {w - up[v], n};
            }
            sort(s.begin(), s.end());
            
            for (int i = 0; i < c[u].size(); i++) {
                if (o[i].first < s[i].first) 
                    return false;  // 无法匹配
                l[u][o[i].second] = s[i].second;
            }
        }
    };
    

    时间顺序构建

    // 构建依赖图:贴纸顺序意味着边的时间顺序
    vector<vector<int>> g2(n - 1);  // 边之间的时间依赖
    vector<int> d(n - 1, 0);        // 入度
    
    for (int i = 0; i < n; i++) {
        for (int j = 1; j < c[i].size(); j++) {
            // 贴纸j必须在贴纸j-1之后出现
            int prev_edge = l[i][j - 1];
            int curr_edge = l[i][j];
            g2[prev_edge].push_back(curr_edge);
            d[curr_edge]++;
        }
    }
    
    // 拓扑排序输出顺序
    queue<int> q;
    for (int i = 0; i < n - 1; i++) 
        if (d[i] == 0) q.push(i);
    
    while (!q.empty()) {
        int u = q.front(); q.pop();
        cout << u + 1 << " ";
        for (int v : g2[u]) {
            if (--d[v] == 0) 
                q.push(v);
        }
    }
    

    样例解析

    样例1

    输入:
    5 0
    1 2 3
    1 3 1  
    3 4 12
    3 5 6
    0 4
    2
    6 1 3
    8
    3
    
    输出:
    Yes
    1 4 2 3
    

    树结构:

        1
       / \
      2   3
         / \
        4   5
    

    匹配过程:

    • 节点2(叶子):贴纸[2],边(1,2)需求3,满足3≤2+?
    • 节点4(叶子):贴纸[8],边(3,4)需求12,满足12≤8+?
    • 节点5(叶子):贴纸[3],边(3,5)需求6,满足6≤3+?
    • 节点3:贴纸[6,1,3],需要匹配边(1,3),(3,4),(3,5)
    • 节点1:贴纸[0,4],需要匹配边(1,2),(1,3)

    最终得到合法顺序:1→4→2→3


    正确性证明

    匹配条件的必要性

    对于边 (u,v)(u,v) 权重 ww,如果 vvuu 的子节点,那么:

    • vv 上传的值 upvup_vvv 为这条边贡献的部分
    • uu 需要贡献 wupvw - up_v
    • 必须存在一个贴纸 cc 使得 cwupvc \geq w - up_v

    时间顺序的保持

    贴纸从上到下的顺序确定了边出现的时间顺序,通过构建依赖图和拓扑排序保证时间约束。


    复杂度分析

    • DFS遍历O(n)O(n)
    • 排序操作:每个节点排序度数次,总 O(nlogn)O(n \log n)
    • 拓扑排序O(n)O(n)
    • 总复杂度O(nlogn)O(n \log n)

    对于 n200,000n \leq 200,000 可以接受。


    算法亮点

    1. 自底向上处理:从叶子到根,逐步确定匹配关系
    2. 贪心匹配:排序后贪心匹配需求和贴纸
    3. 上传值机制:子节点向父节点传递需求信息
    4. 依赖图构建:将贴纸顺序转化为边的时间顺序
    5. 拓扑排序:输出合法的时间序列

    总结

    这道题的核心在于将时间顺序约束转化为图上的匹配问题:

    1. 问题转化:将线索出现顺序转化为贴纸与边的匹配问题
    2. 树形DP:利用树结构自底向上传递信息
    3. 贪心策略:在约束条件下进行最优匹配
    4. 拓扑排序:处理时间依赖关系

    这种"树形结构 + 贪心匹配 + 拓扑排序"的组合在解决带有顺序约束的树问题时非常有效。

    • 1

    信息

    ID
    4871
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    2
    已通过
    1
    上传者