P4745 [CERC2017] Gambling Guide
题意
给定一张 n
个点,m
条边的无权无向图。一个人从城市 1 出发,目的是到达城市 n
。在图中,当前城市可以随机选择一个与之直接相连的城市进行前进,或者选择不动,重新随机选择一个城市。每次操作都花费 1 的代价。我们的目标是计算到达城市 n
时的最小总花费。
解法
这道题目的核心在于利用 期望 来计算最小花费。我们可以使用 动态规划 和 图的最短路径算法,并结合堆优化(类似于 Dijkstra 算法)来得到最优解。
逆推思路
考虑逆向计算:我们从目标城市 n
开始,逐步推算出其他城市到目标城市 n
的最小期望花费。
- 设
f(x)
表示从城市x
到城市n
的期望花费。 - 初始时,
f(n) = 0
,因为已经到达目标城市n
,不需要再花费任何代价。
接下来,我们需要更新其他城市的期望值。对于城市 x
,它的期望花费由相邻城市的期望值加上 1(每次操作都花费 1)决定。具体的更新公式如下:
- 对于每个城市
x
,假设它有deg(x)
个邻居城市,f(x)
需要计算它所有邻居城市y
的期望值。我们可以通过对每个邻居y
进行期望值的更新来逐步得到f(x)
的值。
如果 y
的期望值 f(y)
小于 f(x)
,我们可以利用 f(y)
来更新 f(x)
,否则继续保持 f(x)
原有的值。
公式推导
对于城市 x
,有以下关系:
f
(
x
)
=
∑
y
∈
neighbor
(
x
)
f
(
y
)
deg
(
x
)
+
1
f(x) = \frac{\sum_{y \in \text{neighbor}(x)} f(y)}{\text{deg}(x)} + 1
f(x)=deg(x)∑y∈neighbor(x)f(y)+1
其中 deg(x)
是城市 x
的度数,neighbor(x)
是与 x
直接相连的城市集合。该公式的含义是:从 x
出发,随机选择一个与 x
直接相连的城市 y
,花费为 f(y)
,并且每次操作都会花费 1。所有相邻城市的期望花费平均值加 1 就是从 x
到目标城市 n
的期望花费。
时间复杂度
该问题类似于 Dijkstra 算法,使用优先队列来优化松弛操作,因此时间复杂度为:
O
(
(
m
+
n
)
log
n
)
O((m + n) \log n)
O((m+n)logn)
这里 m
是图中的边数,n
是图中的节点数。
Trick 和优化
-
堆优化的 Dijkstra: 我们通过优先队列来动态更新每个城市的期望花费。每次取出期望值最小的城市,并更新与之相邻的城市。通过堆来优化松弛操作,可以保证每次松弛都能保证最优解。
-
更新公式的正确性: 在更新
f(x)
时,只有当相邻城市y
的期望值小于f(x)
时,才可以利用f(y)
来更新f(x)
。通过这种方式,我们能确保每次更新后,f(x)
的值不会被错误地增大。
代码实现
#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define BoBoowen ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
using namespace std;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const int N = 5e5 + 10;
int n, m;
vector<int> g[N]; // 邻接表表示图
vector<double> sum(N); // 累计期望值
vector<double> siz(N); // 被访问次数
vector<int> flag(N); // 标记是否访问过
vector<double> val(N, inf); // 期望消耗值初始化为无穷大
void solved() {
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
val[n] = 0; // 从目标城市 n 开始,期望花费为 0
priority_queue<pair<double, int>> q; // 优先队列存储当前城市的期望值和城市编号
q.push({0, n});
while (!q.empty()) {
pair<double, int> now = q.top();
q.pop();
if (flag[now.second]) continue; // 如果该城市已访问,跳过
flag[now.second] = 1;
for (auto it : g[now.second]) {
if (flag[it]) continue; // 如果相邻城市已访问,跳过
siz[it] += 1.0;
sum[it] += val[now.second];
val[it] = (sum[it] + g[it].size() * 1.0) / siz[it]; // 更新期望消耗
q.push({-val[it], it}); // 通过负值实现最大堆
}
}
cout << fixed << setprecision(6) << val[1] << endl; // 输出城市 1 的期望消耗,保留 6 位小数
}
signed main() {
BoBoowen;
int T = 1;
while (T--) {
solved();
}
}
代码解读
- 输入与图的构建: 使用邻接表
g
来存储图的结构。读取每一条边并双向添加。 - 期望值计算: 使用一个最大堆(优先队列)来维护当前最小的期望值,并利用堆的性质每次从期望值最小的城市开始更新相邻城市的期望值。
- 输出: 最终输出从城市 1 到城市 n 的期望花费,并保留 6 位小数。
细节注意
- 堆优化: 注意堆中存储的是负值来模拟最大堆,确保每次从期望值最小的城市开始更新。
- 双向图处理: 本题是无向图,必须保证每条边都被双向处理。
- 终止条件: 每个城市更新完毕后,就不再进行任何更新,避免重复计算。
总结
这道题通过逆向推导和期望值计算的方法,将其转化为最短路径问题,并通过堆优化的方式高效地求解最小期望花费。通过 Dijkstra 风格的松弛操作,我们得到了城市 1 到城市 n 的最小期望花费。