设 f[u] 为从 u 走到 N 的期望路径长,由全期望公式得 f[u]=(Σ边长+Σf[后继])/出度,DAG 上按逆拓扑序倒推一遍,时间 O(N+M)。
OJ: roj
题目 ID: 3095
难度:普及+/提高-
标签:图论DAG拓扑排序期望DPpython
创建: 2026-10-01 16:38
更新: 2026-10-07 13:50
形式化题目
给定一张有向无环图,
从
题面样例的图结构如下(每条边上的数字是长度):
graph LR
1 -->|1| 2
1 -->|2| 3
2 -->|3| 3
3 -->|4| 4
从点 7.00:两条路的平均长度
正解
思路
先看最难想到的一步:不要枚举路径。 最朴素的做法是列出所有可能的路径、按概率加权求平均。但每个点有
关键观察:期望满足一个只依赖后继的递推。 设
也就是说,
第二个观察:递推只指向后继,所以拓扑序倒推即可。 递推式的右边只出现后继的
终点怎么处理?题面是"到达终点即停止",所以长度只算到踏进
下面是样例的完整倒推过程,
| 步骤 | 出边 | 出度 |
||||
|---|---|---|---|---|---|---|
| 1 | 4 | 无 | — | — | 0 | |
| 2 | 3 | |||||
| 3 | 2 | |||||
| 4 | 1 |
表格逐列读:第 2 步 7.00 一致。
实现要点。 逆拓扑序靠 Kahn 算法:先把所有入度为
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 16:38
# update_at: 2026-10-04 11:36
# 记 f[u] = 从 u 出发走到 N 的期望路径长度。青蛙在 u 时先均匀随机挑一条出边:
# f[u] = Σ_{u->v 长度 c} (c + f[v]) / out[u] = (Σc + Σf[v]) / out[u]。
# 右边只用到后继,所以在 DAG 上按拓扑序倒着推一遍即可,终点 f[N] = 0。
import sys
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split())) # 读入游标:next() 顺序消费
n, m = next(data), next(data)
out_edges: list[list[int]] = [[] for _ in range(n + 1)] # out_edges[u] = u 的出边编号
edge_len_sum = [0] * (n + 1) # Σ 出边长度
end = [0] * m # 第 i 条边的终点
indeg = [0] * (n + 1)
for i in range(m):
a, b, c = next(data), next(data), next(data) # 每条边固定 u v c 三个 token
out_edges[a].append(i)
edge_len_sum[a] += c
end[i] = b
indeg[b] += 1
# Kahn 拓扑排序:入度为 0 的点入队,出队时把后继入度减 1;入队顺序就是拓扑序。
# 在同一个列表里边遍历边追加,就等价于一个先进先出的队列。
order = [u for u in range(1, n + 1) if indeg[u] == 0]
for u in order:
for i in out_edges[u]:
v = end[i]
indeg[v] -= 1
if indeg[v] == 0:
order.append(v) # 追加到正在遍历的列表尾部 = 入队
dp = [0.0] * (n + 1)
for u in reversed(order): # 逆拓扑序:算 f[u] 时所有后继的 f 都已就绪
k = len(out_edges[u])
if k and u != n: # 走到终点就停下,终点自身不再产生期望长度
dp[u] = (edge_len_sum[u] + sum(dp[end[i]] for i in out_edges[u])) / k
print(f"{dp[1]:.2f}")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 18:35
* update_at: 2026-10-06 18:35
*/
// main.cpp:绿豆蛙的归宿。
// 设 f[u] = 从 u 出发走到 N 的期望路径长度。青蛙在 u 时等概率挑一条出边,
// 由全期望公式:f[u] = (Σ出边长 + Σf[后继]) / 出度。
// 右边只依赖后继,图又是 DAG,所以按逆拓扑序倒推一遍即可,终点 f[N] = 0。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const int MAXM = 200005;
typedef long long ll;
int n, m;
int head[MAXN], to[MAXM], nxt[MAXM], edge_cnt; // 链式前向星存图
ll edge_len[MAXM]; // 每条边的长度
ll len_sum[MAXN]; // len_sum[u] = u 所有出边的长度和
int outdeg[MAXN]; // outdeg[u] = u 的出边数
int indeg[MAXN]; // Kahn 拓扑排序用的入度
double f[MAXN]; // f[u] = 从 u 走到 N 的期望路径长度
// 加一条 u -> v、长度 c 的有向边。
void add_edge(int u, int v, ll c) {
edge_cnt++;
to[edge_cnt] = v;
edge_len[edge_cnt] = c;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
len_sum[u] += c; // 读入时顺手累加每个点的出边长度和
outdeg[u]++;
indeg[v]++;
}
void solve() {
// Kahn 拓扑排序:入度为 0 的点入队,出队时把后继入度减 1。
// 队列的正序就是拓扑序,倒过来即逆拓扑序。
static int order[MAXN]; // 保存拓扑序
int cnt = 0;
queue<int> q;
for (int u = 1; u <= n; u++) {
if (indeg[u] == 0) q.push(u);
}
while (!q.empty()) {
int u = q.front();
q.pop();
cnt++;
order[cnt] = u;
for (int i = head[u]; i != 0; i = nxt[i]) {
int v = to[i];
indeg[v]--;
if (indeg[v] == 0) q.push(v);
}
}
// 逆拓扑序倒推:算 f[u] 时所有后继的 f 都已经算好。
// 终点 N 到达即停止,f[N] 保持 0,不参与递推。
for (int j = cnt; j >= 1; j--) {
int u = order[j];
if (u == n || outdeg[u] == 0) continue;
ll sum_f = 0;
for (int i = head[u]; i != 0; i = nxt[i]) {
int v = to[i];
sum_f += f[v];
}
f[u] = (double)(len_sum[u] + sum_f) / outdeg[u];
}
printf("%.2f\n", f[1]);
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int a, b;
ll c;
scanf("%d %d %lld", &a, &b, &c);
add_edge(a, b, c);
}
solve();
return 0;
}复杂度
- 时间:建图
;拓扑排序每个点出队一次、每条边被松弛一次 ;逆拓扑序递推对每条出边访问一次 。合计 。题面 ,即 ,与标准 C++ 写法同阶。 - 空间:出边表存
个边编号,终点表存 个整数,入度数组、拓扑序、 数组各 ,合计 。
总结
- 期望类问题优先试"把整条路径的期望换成一个可递推的状态"。本题里
只与后继有关,是马尔可夫性把指数级的路径枚举压成了线性递推。 - 递推方向由依赖关系决定:只依赖后继
逆拓扑序;只依赖前驱 正拓扑序。DAG 保证了这种顺序一定存在。 - 常数项(边长和)在递推式里是仿射项,可以在读入时预先按点累加,避免倒推时重复扫边。
- 边界要按题面语义写:终点是"到达即停止"(
就不递推),而不是"没有出边就不递推"——后者是数据性质,不是题目保证。
图示解析
这张图串起本题从建模到得到答案的主线:
枚举所有路径按概率加权 路径数最坏 K^N,不可行
`- 换成状态:f[u] = 从 u 出发到 N 的期望路程
|- 马尔可夫性:之后怎么走与怎么来的无关
| `- 全期望公式:f[u] = (Σ 边长 + Σ f[后继]) / 出度
`- 依赖方向:只用后继
|- 正向 Kahn:只有 1 初始入度为 0 → 队列正序即拓扑序(1 在头)
`- 逆拓扑序倒推:先 f[N]=0,最后读出 f[1] → 输出两位小数读图时注意两个分叉的因果关系:第一个分叉说明"为什么可以只用后继",第二个分叉说明"为什么可以一遍算完"。图中每一步都只用到正文已经证明过的结论,不替代正文里的递推式与样例推演。