1 条题解

  • 0
    @ 2025-10-29 16:44:27

    菜系餐厅最小交通费用问题 - 详细题解

    问题分析

    问题背景

    • 交通网络是一棵 n 个节点的树(n-1 条边,无环且连通),每条边有票价(权重)
    • 每个节点对应一家餐厅,属于 1~r 中的某类菜系
    • 每个查询给出两个用户的位置(p, q)和目标菜系 s
    • 需要找到菜系为 s 的餐厅 u,使得 dist(p,u) + dist(q,u) 最小

    关键难点

    • 直接对每个查询遍历所有颜色为 s 的节点会超时(O(nq))
    • 需要高效处理大量查询(q ≤ 100,000)

    核心算法思路

    1. 问题转化与数学推导

    对于查询 (p, q, s),设目标餐厅为 u,则总费用为:

    cost = dist(p,u) + dist(q,u)
    

    利用树上距离的性质,可以改写为:

    cost = dist(p,q) + 2 × dist(LCA(p,q), u)
    

    但这个公式只在 u 在 p-q 路径上时准确。

    更通用的方法是多源最短路:以所有菜系 s 的餐厅为起点,计算到 p 和 q 的最短距离。

    2. 整体算法框架

    代码采用按颜色分组处理的策略:

    1. 预处理阶段

      • 树的DFS遍历,计算深度、距离、DFS序
      • LCA倍增数组预处理
      • 按颜色分组存储节点
    2. 查询处理阶段

      • 对每个颜色,构建包含所有相关节点的虚树
      • 在虚树上运行多源最短路
      • 回答所有涉及该颜色的查询

    详细算法步骤

    步骤1:树的预处理

    void dfs(int x, int f) {
        st[x] = ++dc;           // DFS进入时间戳
        anc[x][0] = f;          // 直接父节点
        
        // 倍增数组预处理
        for (int j = 1; j <= 20; j++)
            anc[x][j] = anc[anc[x][j-1]][j-1];
        
        // 遍历子节点
        for (auto [v, w] : g[x]) {
            if (v == f) continue;
            d[v] = d[x] + w;    // 到根节点的距离
            dep[v] = dep[x] + 1; // 深度
            dfs(v, x);
        }
        ed[x] = dc;             // DFS离开时间戳
    }
    

    预处理内容

    • st[x], ed[x]:DFS序,用于判断节点间的祖先关系
    • d[x]:节点 x 到根节点的距离
    • dep[x]:节点深度
    • anc[x][k]:倍增数组,用于快速查询LCA

    步骤2:LCA查询函数

    int lca(int x, int y) {
        if (dep[x] < dep[y]) swap(x, y);
        
        // x 跳到与 y 同一深度
        for (int i = 20; i >= 0; i--)
            if (dep[x] - (1 << i) >= dep[y])
                x = anc[x][i];
        
        if (x == y) return x;
        
        // 同时向上跳
        for (int i = 20; i >= 0; i--)
            if (anc[x][i] != anc[y][i])
                x = anc[x][i], y = anc[y][i];
        
        return anc[x][0];
    }
    

    步骤3:查询预处理

    for (int i = 1; i <= q; i++) {
        int x, y, c;
        cin >> x >> y >> c;
        
        // 检查菜系是否存在
        if (!src[c].size()) {
            ans[i] = -1;
            continue;
        }
        
        // 计算固定部分:x 到 y 的距离
        ans[i] += d[x] + d[y] - 2 * d[lca(x, y)];
        
        // 记录相关节点
        key[c].push_back(x);
        key[c].push_back(y);
        qr[c].push_back({x, y, i});
    }
    

    这里利用了树上距离公式:

    dist(x,y) = d[x] + d[y] - 2 × d[lca(x,y)]
    

    步骤4:虚树构建(核心优化)

    虚树只保留与当前颜色相关的节点及其LCA,大幅减少节点数量。

    void SOLVE::solve(int C) {
        // 1. 收集关键节点
        auto v = key[C];  // 颜色C的所有相关节点
        
        // 2. 去重和排序
        sort(v.begin(), v.end());
        v.erase(unique(v.begin(), v.end()), v.end());
        
        // 3. 加入所有LCA
        for (int i = 1; i < m; i++)
            if (!vis[x = lca(a[i], a[i+1])])
                vis[a[++tot] = x] = 1;
        
        // 4. 按DFS序排序后构建虚树
        sort(a + 1, a + m + 1, cmp);
        
        // 5. 用栈构建虚树结构
        for (q[t = 1] = 1, i = 2; i <= m; q[++t] = a[i++]) {
            while (st[a[i]] < st[q[t]] || ed[a[i]] > ed[q[t]])
                t--;
            if (q[t] != a[i])
                add_edge(q[t], a[i]);
        }
    }
    

    虚树构建的关键

    • 只保留必要的节点(餐厅节点 + 查询节点 + 它们的LCA)
    • 保持原树的祖先关系
    • 边权为原树中对应路径的距离

    步骤5:多源最短路计算

    // 初始化距离
    for (auto x : use) ds[x] = 1e18;
    
    // 所有颜色C的餐厅作为起点
    for (auto x : src[C])
        ds[x] = 0, q.push({ds[x], x});
    
    // Dijkstra算法
    while (!q.empty()) {
        auto [dis, x] = q.top();
        q.pop();
        if (dis != ds[x]) continue;
        
        for (auto [v, w] : e[x]) {
            if (ds[v] > ds[x] + w) {
                ds[v] = ds[x] + w;
                q.push({ds[v], v});
            }
        }
    }
    

    步骤6:查询答案计算

    // 预处理倍增最小值数组
    for (auto x : use) 
        mn[x][0] = ds[x];
    
    for (int j = 1; j <= 20; j++)
        for (auto x : use) {
            anc[x][j] = anc[anc[x][j-1]][j-1];
            mn[x][j] = min(mn[x][j-1], mn[anc[x][j-1]][j-1]);
        }
    
    // 计算路径最小值
    for (auto [x, y, id] : qr[C])
        ans[id] += calc(x, y) * 2;
    

    calc(x, y) 函数计算 x 到 y 路径上的最小 ds 值,这对应着最优餐厅的选择。


    算法复杂度分析

    步骤 时间复杂度 说明
    树预处理 O(n log n) DFS + 倍增数组
    LCA查询 O(log n) per query 二进制跳跃
    虚树构建 O(k log k) per color k为相关节点数
    多源最短路 虚树上Dijkstra
    总复杂度 O((n + q) log n) 可处理1e5级别数据

    关键技术与学习要点

    1. 虚树技术

    • 目的:压缩树结构,只保留关键节点
    • 应用场景:需要频繁在树的某个子集上操作时
    • 构建方法:按DFS序排序,用栈维护当前链

    2. 多源最短路

    • 核心思想:多个起点到其他点的最短距离
    • 实现方法:初始将所有起点距离设为0,入优先队列
    • 在树上的优势:树结构保证最短路径唯一

    3. 倍增法应用

    • LCA查询
    • 路径最小值查询
    • 都需要 O(log n) 的预处理和查询时间

    4. 分组处理思想

    • 按颜色将查询分组
    • 每个颜色独立处理,减少重复计算
    • 适合颜色数较多但每个颜色的查询较集中的情况

    样例验证

    以样例第一天查询 (1, 3, 3) 为例:

    1. 颜色3的餐厅:节点3, 5
    2. 构建虚树包含:1, 3, 5 及相关LCA
    3. 多源最短路从节点3,5开始传播
    4. 计算得到最小费用:7

    验证:dist(1,3) + dist(3,3) = 7 + 0 = 7

    这个解法综合运用了多种树算法,是处理树上带约束最短路径问题的经典范例。

    • 1

    信息

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