在有向无环图上逆向定义到达终点的剩余期望长度,利用全期望公式与拓扑序通过记忆化搜索在 O(n+m) 内求解。
OJ: luogu
题目 ID: P4316
难度:普及+/提高-
标签:动态规划概率与期望记忆化搜索图论
创建: 2026-10-23 23:04
更新: 2026-10-23 23:15
形式化题目
给定一个包含
暴力解法
思路
根据离散型随机变量数学期望的原始定义:
代码
/**
* brute.cpp:基于期望离散定义的全路径枚举暴力解法
*
* 为什么只适合小数据:
* 本解法采用深度优先搜索,枚举所有从起点 1 到终点 n 的合法走法(简单路径)。
* 在最坏情况下(例如完全分层 DAG),两点间的简单路径数量随节点数呈指数级爆炸(O(2^n))。
* 当 n 超过 20~30 时,递归搜索所有路径会引发严重的运行超时(TLE)。
* 该暴力仅用于小规模数据下验证数学定义的正确性,并作为对拍的基准。
*
* 浮点精度说明:
* 采用 long double 进行高精度概率和路径长度累加,尽量减少中间阶段的大量乘除截断误差。
*/
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;
typedef long long ll;
const int maxn = 100005;
struct Edge {
int to;
ll w;
};
int n, m;
vector<Edge> g[maxn];
int out_deg[maxn];
long double total_expected_len = 0.0;
// dfs 搜索所有从 1 到 n 的完整路径,累加路径概率与长度之积
void dfs(int u, long double curr_prob, long double curr_len) {
if (u == n) {
total_expected_len += curr_prob * curr_len;
return;
}
for (const auto &e : g[u]) {
long double next_prob = curr_prob / (long double)out_deg[u];
dfs(e.to, next_prob, curr_len + (long double)e.w);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 0; i < m; ++i) {
int u, v;
ll w;
cin >> u >> v >> w;
g[u].push_back({v, w});
out_deg[u]++;
}
dfs(1, 1.0L, 0.0L);
cout << fixed << setprecision(2) << (double)total_expected_len << "\n";
return 0;
}复杂度
- 时间复杂度:
。在最坏的有向无环图中,两点之间的简单路径数量随节点规模呈指数级增长,可达 甚至更高。 - 空间复杂度:
,为递归调用栈所占用的深度。
瓶颈
暴力枚举的做法独立考虑了每一条路径,但在 DAG 中,大量不同的路径在经过相同中间节点
正解
思路
为了消除重复计算,我们需要利用有向无环图的无后效性,通过动态规划将公共后缀合并。
为什么逆向设状态?
在马尔可夫决策或随机游走中,如果正向定义状态(如“从起点到节点
全期望公式转移
由全期望公式,在节点
- 边界条件:
(到达终点后不再移动,剩余期望长度为 0)。 - 目标答案:
。
拓扑序与记忆化搜索
由于图为有向无环图,转移依赖关系天然构成拓扑序。我们可以直接使用记忆化搜索实现:
- 从起点
dfs(1)开始递归; - 若已访问过节点
( vis[u] == true),直接返回记录的值; - 否则遍历
的所有出边 ,递归计算后继 的期望,按公式累加并除以出度; - 递归天然按照逆拓扑序求值,代码极其简明,且自动跳过了与
无关的节点。
代码
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;
typedef long long ll;
const int maxn = 100005;
struct Edge {
int to;
ll w;
};
int n, m;
vector<Edge> g[maxn];
int out_deg[maxn];
double f[maxn];
bool vis[maxn];
// dfs(u) 计算从节点 u 出发到达终点 n 的期望路径长度
double dfs(int u) {
if (u == n) {
return 0.0;
}
if (vis[u]) {
return f[u];
}
vis[u] = true;
double sum = 0.0;
for (const auto &e : g[u]) {
sum += (dfs(e.to) + (double)e.w);
}
// 全期望公式:以 1/out_deg[u] 的等概率走向每个后继节点
f[u] = sum / (double)out_deg[u];
return f[u];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 0; i < m; ++i) {
int u, v;
ll w;
cin >> u >> v >> w;
g[u].push_back({v, w});
out_deg[u]++;
}
cout << fixed << setprecision(2) << dfs(1) << "\n";
return 0;
}复杂度
- 时间复杂度:
。记忆化搜索保证每个节点 只被计算一次,每条有向边 恰好被遍历一次。对于 ,总运算次数在 左右,实测耗时在 20ms 以内。 - 空间复杂度:
。邻接表存图占用 空间,记忆化数组与访问标记占用 空间;递归栈在最坏的长链图下深度为 ,占用内存约 3.2MiB,在常规评测限制下非常安全。
总结
本题是典型的有向无环图(DAG)上的期望动态规划模型。通过将正向概率加权转化为“从当前节点出发到达终点的剩余期望”这一逆向状态定义,完美利用了马尔可夫无后效性,规避了条件概率的复杂推导。配合记忆化搜索,既直观对应全期望公式,又天然契合 DAG 的拓扑转移顺序。