Train Driver - 题解
标签与难度
标签: 图论, 最短路, 广度优先搜索(BFS), Dijkstra, 多源最短路, 期望 难度: 2100
题目大意喵~
主人你好呀!这道题是说,在一个由 个地点和 条等长小路组成的校园里(可以看作一个无向连通图),有三位可爱的小伙伴 WJJ, DYX 和 ZZH 准备进行集训,喵~
他们需要找一个集合点,使得三个人从各自的出发点到集合点的总路程最短。
不过呢,他们的出发点不固定,充满了随机性,具体是这样的:
- DYX 会等概率地出现在集合 中的任意一个地点。
- ZZH 会等概率地出现在集合 中的任意一个地点。
- WJJ 就更随性啦,他会等概率地出现在校园里的任何一个地点。
我们需要计算,在这种随机模型下,他们为了集合所需要走的最短总路程的期望值是多少,的说。
输入:
- 地点数 和小路数 。
- 条小路连接的两个地点。
- 集合 的大小和其中包含的地点。
- 集合 的大小和其中包含的地点。
输出:
- 一个最简分数,表示期望的最短总路程。
解题思路分析
这道题的核心是计算一个期望值,喵~ 看到“期望”两个字,我们首先想到的应该是期望的定义:。在这里, 就是“最短总路程”,而 是每一种可能的出发组合所对应的最短总路程, 则是该组合出现的概率。
让我来一步步拆解这个问题吧!
1. 期望公式的展开
- 设 DYX 的出发点是 ,ZZH 的出发点是 ,WJJ 的出发点是 ( 是所有地点的集合)。
- 由于他们是等概率随机选择,任何一个具体组合 出现的概率是:
- 对于一个确定的出发组合 ,他们会选择一个集合点 ,使得三人的总路程 最小。我们把这个最小总路程记为 。
- 那么,总的期望值就是所有可能情况的(最小路程 概率)之和:
- 根据期望的线性性质,我们可以把常数项提出来:
现在,问题转化为了如何高效地计算 这个巨大的和,也就是期望的分子部分。
2. 简化求和
直接三重循环枚举 肯定会超时的说。我们不如换个思路,把求和的顺序调整一下。我们可以先固定 DYX 和 ZZH 的位置,也就是一对 ,然后计算 WJJ 在所有可能位置时,对应的最小总路程之和。
对于固定的 ,我们要求的是 。 其中 。
这个式子看起来还是很复杂,喵~ 别急,我们再深入一点。
3. 关键转化:多源最短路
对于固定的 和一个可能的 WJJ 出发点 ,我们要找一个汇合点 。 我们可以把 看作是选择 作为汇合点的“基础代价”。 那么,对于一个给定的 ,最小总路程就是:
这个式子是不是有点眼熟呐?它长得很像最短路的定义!
我们可以把它想象成一个多源最短路问题:
- 假设图中的每个节点 都是一个潜在的“源点”。
- 从源点 出发的初始“代价”不是 0,而是 。
- 我们想求的是,从所有这些带权重的源点出发,到达节点 的最短路径。
这个过程可以用 Dijkstra 算法来解决!因为所有边的权重都是 1,所以我们可以用一种更快的、线性的 Dijkstra 实现,也就是用 BFS 来模拟。
4. 算法流程
结合上面的分析,我们的完整算法就清晰啦:
Step 1: 预处理
- 因为我们需要频繁查询任意两点间的距离,而 和 的大小都不超过 20,很小。所以我们可以先为集合 和 中的每一个点,都跑一次 BFS,计算出它到图中所有其他点的最短距离。
- 我们可以用
dist_A[i][v]表示集合 中第 个点到点 的距离,dist_B[j][v]同理。 - 这一步的时间复杂度是 ,完全可以接受。
Step 2: 主循环计算
- 我们要计算总路程和,所以初始化一个
long long total_min_distance_sum = 0; - 然后,我们枚举所有可能的 组合:
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) 的实现 (核心部分)
- 对于固定的 ,我们要计算 。
- 我们定义一个
initial_cost数组,initial_cost[p] = dist(u_A, p) + dist(u_B, p)。这部分数据可以直接从预处理的结果中查到。 - 现在,我们要进行一次多源 BFS/Dijkstra。设
final_cost[v]为 WJJ 从 点出发时的最小总路程。 - 初始化
final_cost数组,final_cost[v] = initial_cost[v]对所有 成立。 - 创建一个优先队列,把所有节点
(p, initial_cost[p])都放进去。因为边权为1,我们可以用更高效的方式代替标准优先队列:- 桶排序优化:创建一个桶数组
buckets,buckets[d]存放所有initial_cost为d的节点。 - 然后按
d从小到大的顺序,把桶里的节点依次放入一个普通的队列q中。这样q里的节点就按初始代价排好序了!
- 桶排序优化:创建一个桶数组
- 接下来,对这个队列
q跑一个标准的 BFS 松弛操作:cppwhile (!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 从点 出发时,三人汇合的最短总路程。 - 我们将所有的 final_cost[v] (对于 ) 加起来,就得到了 sum_for_this_pair。
Step 4: 计算最终结果
- 经过主循环,我们得到了总和
total_min_distance_sum。 - 分母是
denominator = 1LL * |A| * |B| * n。 - 最后用
gcd求最大公约数,化简分数total_min_distance_sum / denominator并输出。
这样,我们就把一个复杂的期望问题,巧妙地转化成了一系列多源最短路问题,并用高效的算法解决了它,喵~ 是不是很酷!
代码实现
这是我根据上面的思路,精心为你准备的代码哦~ 加了详细的注释,希望能帮助你理解,喵!
#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;
}复杂度分析
时间复杂度:
- 预处理: 我们为集合 和 中的每个点都运行一次 BFS。这部分的复杂度是 。
- 主循环: 我们有 对组合。对于每一对,我们都执行了一次
calculate_sum_for_pair。 calculate_sum_for_pair内部,桶排序和填充队列是 ,随后的 BFS 过程是 。所以这一步的复杂度是 。- 因此,总时间复杂度是预处理加上主循环的复杂度。因为 和 都很小(),这个复杂度是完全可以通过的,喵~
空间复杂度:
- 我们需要存储从集合 和 中每个点出发到图中所有点的距离,这是空间占用的主要部分,大小为 。
- 邻接表需要 的空间。
- 在
calculate_sum_for_pair函数中,我们用到了大小为 的final_min_cost数组和桶,以及队列。 - 所以总空间复杂度由预处理的距离矩阵主导。
知识点总结
这道题真是一次有趣的冒险,它教会了我们:
- 期望的计算: 遇到期望问题,首先要回归其定义,并利用期望的线性性质来简化求和,这是解决这类问题的基本功,呐。
- 问题转化: 能够将复杂的式子 识别为多源最短路问题是解题的关键。这种洞察力需要多做题来培养哦!
- 多源最短路 (Multi-source Shortest Path): 当有多个源点,且每个源点有不同的初始代(权)价时,可以用 Dijkstra 算法求解。
- 线性时间 Dijkstra: 在边权为1(或很小)的图上,Dijkstra 算法可以用队列和桶排序等数据结构进行优化,使其时间复杂度从 降低到线性的 ,这是一个非常实用且高效的技巧!
希望这篇题解能帮到你,如果还有不明白的地方,随时可以再来问我哦!一起加油,变得更强吧,喵~!