Skip to content

Train Driver - 题解

标签与难度

标签: 图论, 最短路, 广度优先搜索(BFS), Dijkstra, 多源最短路, 期望 难度: 2100

题目大意喵~

主人你好呀!这道题是说,在一个由 nn 个地点和 mm 条等长小路组成的校园里(可以看作一个无向连通图),有三位可爱的小伙伴 WJJ, DYX 和 ZZH 准备进行集训,喵~

他们需要找一个集合点,使得三个人从各自的出发点到集合点的总路程最短。

不过呢,他们的出发点不固定,充满了随机性,具体是这样的:

  1. DYX 会等概率地出现在集合 AA 中的任意一个地点。
  2. ZZH 会等概率地出现在集合 BB 中的任意一个地点。
  3. WJJ 就更随性啦,他会等概率地出现在校园里的任何一个地点。

我们需要计算,在这种随机模型下,他们为了集合所需要走的最短总路程的期望值是多少,的说。

输入

  • 地点数 nn 和小路数 mm
  • mm 条小路连接的两个地点。
  • 集合 AA 的大小和其中包含的地点。
  • 集合 BB 的大小和其中包含的地点。

输出

  • 一个最简分数,表示期望的最短总路程。

解题思路分析

这道题的核心是计算一个期望值,喵~ 看到“期望”两个字,我们首先想到的应该是期望的定义:E[X]=xiP(xi)E[X] = \sum x_i P(x_i)。在这里,XX 就是“最短总路程”,而 xix_i 是每一种可能的出发组合所对应的最短总路程, P(xi)P(x_i) 则是该组合出现的概率。

让我来一步步拆解这个问题吧!

1. 期望公式的展开

  • 设 DYX 的出发点是 uAAu_A \in A,ZZH 的出发点是 uBBu_B \in B,WJJ 的出发点是 uCVu_C \in VVV 是所有地点的集合)。
  • 由于他们是等概率随机选择,任何一个具体组合 (uA,uB,uC)(u_A, u_B, u_C) 出现的概率是:

    P(uA,uB,uC)=1A×1B×1nP(u_A, u_B, u_C) = \frac{1}{|A|} \times \frac{1}{|B|} \times \frac{1}{n}

  • 对于一个确定的出发组合 (uA,uB,uC)(u_A, u_B, u_C),他们会选择一个集合点 pVp \in V,使得三人的总路程 dist(uA,p)+dist(uB,p)+dist(uC,p)dist(u_A, p) + dist(u_B, p) + dist(u_C, p) 最小。我们把这个最小总路程记为 D(uA,uB,uC)D(u_A, u_B, u_C)
  • 那么,总的期望值就是所有可能情况的(最小路程 ×\times 概率)之和:

    E=uAAuBBuCVD(uA,uB,uC)×1ABnE = \sum_{u_A \in A} \sum_{u_B \in B} \sum_{u_C \in V} D(u_A, u_B, u_C) \times \frac{1}{|A| \cdot |B| \cdot n}

  • 根据期望的线性性质,我们可以把常数项提出来:

    E=1ABn(uAAuBBuCVD(uA,uB,uC))E = \frac{1}{|A| \cdot |B| \cdot n} \left( \sum_{u_A \in A} \sum_{u_B \in B} \sum_{u_C \in V} D(u_A, u_B, u_C) \right)

现在,问题转化为了如何高效地计算 uAAuBBuCVD(uA,uB,uC)\sum_{u_A \in A} \sum_{u_B \in B} \sum_{u_C \in V} D(u_A, u_B, u_C) 这个巨大的和,也就是期望的分子部分。

2. 简化求和

直接三重循环枚举 (uA,uB,uC)(u_A, u_B, u_C) 肯定会超时的说。我们不如换个思路,把求和的顺序调整一下。我们可以先固定 DYX 和 ZZH 的位置,也就是一对 (uA,uB)(u_A, u_B),然后计算 WJJ 在所有可能位置时,对应的最小总路程之和。

对于固定的 (uA,uB)(u_A, u_B),我们要求的是 uCVD(uA,uB,uC)\sum_{u_C \in V} D(u_A, u_B, u_C)。 其中 D(uA,uB,uC)=minpV{dist(uA,p)+dist(uB,p)+dist(uC,p)}D(u_A, u_B, u_C) = \min_{p \in V} \{ dist(u_A, p) + dist(u_B, p) + dist(u_C, p) \}

这个式子看起来还是很复杂,喵~ 别急,我们再深入一点。

3. 关键转化:多源最短路

对于固定的 (uA,uB)(u_A, u_B) 和一个可能的 WJJ 出发点 uCu_C,我们要找一个汇合点 pp。 我们可以把 dist(uA,p)+dist(uB,p)dist(u_A, p) + dist(u_B, p) 看作是选择 pp 作为汇合点的“基础代价”。 那么,对于一个给定的 uCu_C,最小总路程就是:

D(uA,uB,uC)=minpV{(dist(uA,p)+dist(uB,p))+dist(p,uC)}D(u_A, u_B, u_C) = \min_{p \in V} \{ (dist(u_A, p) + dist(u_B, p)) + dist(p, u_C) \}

这个式子是不是有点眼熟呐?它长得很像最短路的定义!

我们可以把它想象成一个多源最短路问题:

  • 假设图中的每个节点 pp 都是一个潜在的“源点”。
  • 从源点 pp 出发的初始“代价”不是 0,而是 dist(uA,p)+dist(uB,p)dist(u_A, p) + dist(u_B, p)
  • 我们想求的是,从所有这些带权重的源点出发,到达节点 uCu_C 的最短路径。

这个过程可以用 Dijkstra 算法来解决!因为所有边的权重都是 1,所以我们可以用一种更快的、线性的 Dijkstra 实现,也就是用 BFS 来模拟。

4. 算法流程

结合上面的分析,我们的完整算法就清晰啦:

Step 1: 预处理

  • 因为我们需要频繁查询任意两点间的距离,而 A|A|B|B| 的大小都不超过 20,很小。所以我们可以先为集合 AABB 中的每一个点,都跑一次 BFS,计算出它到图中所有其他点的最短距离。
  • 我们可以用 dist_A[i][v] 表示集合 AA 中第 ii 个点到点 vv 的距离,dist_B[j][v] 同理。
  • 这一步的时间复杂度是 (A+B)×O(N+M)(|A| + |B|) \times O(N+M),完全可以接受。

Step 2: 主循环计算

  • 我们要计算总路程和,所以初始化一个 long long total_min_distance_sum = 0;
  • 然后,我们枚举所有可能的 (uA,uB)(u_A, u_B) 组合:
    for each u_A in A:
        for each u_B in B:
            // 计算这对 (u_A, u_B) 贡献的总和
            sum_for_this_pair = calculate_sum(u_A, u_B)
            total_min_distance_sum += sum_for_this_pair

Step 3: calculate_sum(u_A, u_B) 的实现 (核心部分)

  • 对于固定的 (uA,uB)(u_A, u_B),我们要计算 uCVminpV{(dist(uA,p)+dist(uB,p))+dist(p,uC)}\sum_{u_C \in V} \min_{p \in V} \{ (dist(u_A, p) + dist(u_B, p)) + dist(p, u_C) \}
  • 我们定义一个 initial_cost 数组,initial_cost[p] = dist(u_A, p) + dist(u_B, p)。这部分数据可以直接从预处理的结果中查到。
  • 现在,我们要进行一次多源 BFS/Dijkstra。设 final_cost[v] 为 WJJ 从 vv 点出发时的最小总路程。
  • 初始化 final_cost 数组,final_cost[v] = initial_cost[v] 对所有 vVv \in V 成立。
  • 创建一个优先队列,把所有节点 (p, initial_cost[p]) 都放进去。因为边权为1,我们可以用更高效的方式代替标准优先队列:
    • 桶排序优化:创建一个桶数组 bucketsbuckets[d] 存放所有 initial_costd 的节点。
    • 然后按 d 从小到大的顺序,把桶里的节点依次放入一个普通的队列 q 中。这样 q 里的节点就按初始代价排好序了!
  • 接下来,对这个队列 q 跑一个标准的 BFS 松弛操作:
    cpp
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : neighbors of u) {
            if (final_cost[v] > final_cost[u] + 1) {
                final_cost[v] = final_cost[u] + 1;
                q.push(v);
            }
        }
    }
  • 当 BFS 结束后,final_cost[v] 就存储了当 WJJ 从点 vv 出发时,三人汇合的最短总路程。
  • 我们将所有的 final_cost[v] (对于 vVv \in V) 加起来,就得到了 sum_for_this_pair。

Step 4: 计算最终结果

  • 经过主循环,我们得到了总和 total_min_distance_sum
  • 分母是 denominator = 1LL * |A| * |B| * n
  • 最后用 gcd 求最大公约数,化简分数 total_min_distance_sum / denominator 并输出。

这样,我们就把一个复杂的期望问题,巧妙地转化成了一系列多源最短路问题,并用高效的算法解决了它,喵~ 是不是很酷!

代码实现

这是我根据上面的思路,精心为你准备的代码哦~ 加了详细的注释,希望能帮助你理解,喵!

cpp
#include <iostream>
#include <vector>
#include <queue>
#include <numeric>

// 一个求最大公约数的函数,用来化简分数
long long gcd(long long a, long long b) {
    return b == 0 ? a : gcd(b, a % b);
}

const int MAXN = 100005;
const int MAX_SET_SIZE = 25;

int n, m;
std::vector<int> adj[MAXN];
int dist_from_A[MAX_SET_SIZE][MAXN];
int dist_from_B[MAX_SET_SIZE][MAXN];
int locations_A_size, locations_B_size;
int locations_A[MAX_SET_SIZE], locations_B[MAX_SET_SIZE];

// 标准的BFS,用于计算单源最短路
void run_bfs(int start_node, int* dist_array) {
    for (int i = 1; i <= n; ++i) {
        dist_array[i] = -1;
    }
    std::queue<int> q;

    dist_array[start_node] = 0;
    q.push(start_node);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int v : adj[u]) {
            if (dist_array[v] == -1) {
                dist_array[v] = dist_array[u] + 1;
                q.push(v);
            }
        }
    }
}

// 核心函数:为固定的 (u_A, u_B) 计算 WJJ 在所有可能位置出发的最小总路程之和
long long calculate_sum_for_pair(int idx_a, int idx_b) {
    std::vector<long long> final_min_cost(n + 1);
    int max_initial_cost = 0;

    // 1. 计算每个点的初始代价
    for (int i = 1; i <= n; ++i) {
        final_min_cost[i] = dist_from_A[idx_a][i] + dist_from_B[idx_b][i];
        if (final_min_cost[i] > max_initial_cost) {
            max_initial_cost = final_min_cost[i];
        }
    }

    // 2. 使用桶排序优化Dijkstra的初始化
    std::vector<std::vector<int>> buckets(max_initial_cost + 1);
    for (int i = 1; i <= n; ++i) {
        buckets[final_min_cost[i]].push_back(i);
    }
    
    std::queue<int> q;
    for (int d = 0; d <= max_initial_cost; ++d) {
        for (int node : buckets[d]) {
            q.push(node);
        }
    }

    // 3. 运行BFS进行松弛操作
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : adj[u]) {
            if (final_min_cost[v] > final_min_cost[u] + 1) {
                final_min_cost[v] = final_min_cost[u] + 1;
                q.push(v);
            }
        }
    }
    
    // 4. 求和
    long long current_pair_sum = 0;
    for (int i = 1; i <= n; ++i) {
        current_pair_sum += final_min_cost[i];
    }
    return current_pair_sum;
}

void solve(int case_num) {
    // 初始化图
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        adj[i].clear();
    }
    for (int i = 0; i < m; ++i) {
        int u, v;
        scanf("%d %d", &u, &v);
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    // 读取集合A和B
    scanf("%d", &locations_A_size);
    for (int i = 1; i <= locations_A_size; ++i) {
        scanf("%d", &locations_A[i]);
    }
    scanf("%d", &locations_B_size);
    for (int i = 1; i <= locations_B_size; ++i) {
        scanf("%d", &locations_B[i]);
    }

    // Step 1: 预处理,计算A、B中各点到全图的距离
    for (int i = 1; i <= locations_A_size; ++i) {
        run_bfs(locations_A[i], dist_from_A[i]);
    }
    for (int i = 1; i <= locations_B_size; ++i) {
        run_bfs(locations_B[i], dist_from_B[i]);
    }

    // Step 2: 主循环,累加所有(u_A, u_B)对的贡献
    long long total_numerator = 0;
    for (int i = 1; i <= locations_A_size; ++i) {
        for (int j = 1; j <= locations_B_size; ++j) {
            total_numerator += calculate_sum_for_pair(i, j);
        }
    }

    // Step 4: 计算最终结果
    long long total_denominator = 1LL * locations_A_size * locations_B_size * n;
    long long common_divisor = gcd(total_numerator, total_denominator);

    printf("Case #%d: ", case_num);
    if (total_denominator / common_divisor == 1) {
        printf("%lld\n", total_numerator / common_divisor);
    } else {
        printf("%lld/%lld\n", total_numerator / common_divisor, total_denominator / common_divisor);
    }
}

int main() {
    int T;
    scanf("%d", &T);
    for (int i = 1; i <= T; ++i) {
        solve(i);
    }
    return 0;
}

复杂度分析

  • 时间复杂度: O((A+B)(N+M)+AB(N+M))O((|A| + |B|) \cdot (N+M) + |A| \cdot |B| \cdot (N+M))

    • 预处理: 我们为集合 AABB 中的每个点都运行一次 BFS。这部分的复杂度是 (A+B)×O(N+M)(|A| + |B|) \times O(N+M)
    • 主循环: 我们有 A×B|A| \times |B| 对组合。对于每一对,我们都执行了一次 calculate_sum_for_pair
    • calculate_sum_for_pair 内部,桶排序和填充队列是 O(N)O(N),随后的 BFS 过程是 O(N+M)O(N+M)。所以这一步的复杂度是 O(N+M)O(N+M)
    • 因此,总时间复杂度是预处理加上主循环的复杂度。因为 A|A|B|B| 都很小(20\le 20),这个复杂度是完全可以通过的,喵~
  • 空间复杂度: O((A+B)N)O((|A| + |B|) \cdot N)

    • 我们需要存储从集合 AABB 中每个点出发到图中所有点的距离,这是空间占用的主要部分,大小为 (A+B)×N(|A| + |B|) \times N
    • 邻接表需要 O(N+M)O(N+M) 的空间。
    • calculate_sum_for_pair 函数中,我们用到了大小为 O(N)O(N)final_min_cost 数组和桶,以及队列。
    • 所以总空间复杂度由预处理的距离矩阵主导。

知识点总结

这道题真是一次有趣的冒险,它教会了我们:

  1. 期望的计算: 遇到期望问题,首先要回归其定义,并利用期望的线性性质来简化求和,这是解决这类问题的基本功,呐。
  2. 问题转化: 能够将复杂的式子 minpV{C(p)+dist(p,uC)}\min_{p \in V} \{ C(p) + dist(p, u_C) \} 识别为多源最短路问题是解题的关键。这种洞察力需要多做题来培养哦!
  3. 多源最短路 (Multi-source Shortest Path): 当有多个源点,且每个源点有不同的初始代(权)价时,可以用 Dijkstra 算法求解。
  4. 线性时间 Dijkstra: 在边权为1(或很小)的图上,Dijkstra 算法可以用队列和桶排序等数据结构进行优化,使其时间复杂度从 O(MlogN)O(M \log N) 降低到线性的 O(N+M)O(N+M),这是一个非常实用且高效的技巧!

希望这篇题解能帮到你,如果还有不明白的地方,随时可以再来问我哦!一起加油,变得更强吧,喵~!