1 条题解

  • 0
    @ 2025-10-19 17:51:51

    这是一道树形DP二分答案结合的题目,难度较高。下面给出详细题解。

    题目理解

    Bajtazar需要在一棵nn个节点的树(城市路网)上,从根节点11(披萨店)出发,访问所有n1n-1个其他节点(客户),最多进行kk次配送,求最短的加热器总运行时间

    关键点

    • 每次配送:从披萨店出发 → 访问若干客户 → 返回披萨店
    • 加热器只在配送途中开启,停留时间不计
    • 目标是最小化加热器总运行时间(即所有配送路径长度之和)

    问题转化

    将问题抽象为:在树上找至多kk条从根出发的路径,覆盖所有节点,使得路径总长度最小

    观察:如果允许kn1k \geq n-1,那么每条路径只访问一个客户,总时间固定。当k<n1k < n-1时,需要在单次配送中访问多个客户,精心规划路线。

    核心算法思路

    1. 二分答案 + 树形DP验证

    二分答案框架

    • 答案具有单调性:加热时间越长,需要的配送次数越少
    • 二分加热时间TT,判断能否用k\leq k次配送完成

    判定问题:能否用k\leq k条从根出发的路径,每条长度T\leq T,覆盖整棵树?

    2. 树形DP状态设计

    对于子树uu,定义状态:

    • dp[u]dp[u]:覆盖子树uu所需的最少路径数
    • 或更精细的状态:f[u][j]f[u][j]表示用jj条路径覆盖子树uu的最小总时间

    但由于n,kn,k较大,需要更高效的设计。

    3. 贪心策略

    实际求解时常用贪心

    1. 预处理所有节点到根的距离dist[u]dist[u]
    2. dist[u]dist[u]从大到小排序(最远的先处理)
    3. 每次选择未覆盖且最远的点,找能覆盖它的最长路径

    标准解法详解

    步骤1:预处理

    • 建树,计算每个节点到根的距离
    • 求出树的直径等信息

    步骤2:关键性质

    性质1:每条配送路径的时间 = 该路径上到根最远点距离 × 2 - 重复利用的边权

    性质2:最优解中,每条路径必然结束于某个叶子节点(或延伸至最远点)

    步骤3:算法实现

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    
    const int N = 100010;
    vector<pair<int, int>> g[N];
    ll dist[N];
    int n, k;
    
    void dfs(int u, int p) {
        for (auto [v, w] : g[u]) {
            if (v == p) continue;
            dist[v] = dist[u] + w;
            dfs(v, u);
        }
    }
    
    bool check(ll T) {
        // 判断能否在T总时间内用≤k次配送完成
        // 实现细节:贪心选择路径
        int cnt = 0;
        vector<bool> covered(n + 1, false);
        
        // 按距离排序,最远的先处理
        vector<pair<ll, int>> nodes;
        for (int i = 2; i <= n; i++) {
            nodes.emplace_back(dist[i], i);
        }
        sort(nodes.rbegin(), nodes.rend());
        
        for (auto [d, u] : nodes) {
            if (covered[u]) continue;
            
            // 找到从根到u的路径上最远的未覆盖点
            ll max_dist = d;
            int v = u;
            while (v != 1 && !covered[v]) {
                max_dist = max(max_dist, dist[v]);
                covered[v] = true;
                // 找父节点...
            }
            
            if (max_dist * 2 > T) return false;
            cnt++;
            if (cnt > k) return false;
        }
        return cnt <= k;
    }
    
    int main() {
        cin >> n >> k;
        for (int i = 1; i < n; i++) {
            int a, b, c;
            cin >> a >> b >> c;
            g[a].emplace_back(b, c);
            g[b].emplace_back(a, c);
        }
        
        dist[1] = 0;
        dfs(1, 0);
        
        // 二分答案
        ll left = 0, right = 2e14, ans = 0;
        while (left <= right) {
            ll mid = (left + right) / 2;
            if (check(mid)) {
                ans = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        
        cout << ans << endl;
        return 0;
    }
    

    样例分析

    对于样例:

    7 3
    1 2 5
    2 3 11  
    2 4 2
    5 2 6
    1 6 1
    7 1 1
    

    最优配送方案:

    1. $1 \rightarrow 2 \rightarrow 4 \rightarrow 2 \rightarrow 5 \rightsquigarrow 1$(15分钟)
    2. 12311 \rightarrow 2 \rightarrow 3 \rightsquigarrow 1(16分钟)
    3. $1 \rightarrow 6 \rightarrow 1 \rightarrow 7 \rightsquigarrow 1$(3分钟)

    总时间:15 + 16 + 3 = 34分钟

    复杂度分析

    • 预处理:DFS O(n)O(n)
    • 二分答案O(log(2e14))O(50)O(\log (2e14)) \approx O(50)
    • check函数O(nlogn)O(n \log n)(排序)
    • 总复杂度O(nlognlogA)O(n \log n \log A),其中AA是答案范围

    算法标签

    • 树形DP / 树形贪心
    • 二分答案
    • 路径覆盖
    • 最近公共祖先(LCA)

    总结

    这道题是树结构上的路径覆盖问题,通过二分答案将最优化问题转化为判定问题,再运用树形贪心策略进行验证。关键在于发现路径覆盖的性质并设计高效的检查算法。

    对于n,k100000n,k \leq 100000的大数据范围,O(nlognlogA)O(n \log n \log A)的算法能够通过所有测试点。

    • 1

    「POI2017 R3」披萨配送员 Pizza delivery

    信息

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