NeFut Logo NeFut
EN 管理员登录

掌握概率论与动态规划:图论中的状态转移与贝叶斯推导

发布于:2026-05-27 09:17 最后更新:2026-06-08 08:26
#C++ #Tutorial

核心逻辑与数学原理

概率论是处理竞赛中不确定性决策与期望动态规划(DP)的数学基石。在 NOIP 级别考察中,概率论的边界通常严格限定在离散样本空间内。

1. 样本空间与条件概率

设 $\Omega$ 为离散有限样本空间,$A$ 和 $B$ 为其中的两个事件。

$$P(A \mid B) = \frac{P(A \cap B)}{P(B)} \text{ (如果 } P(B) > 0 \text{)}$$

在算法竞赛中,条件概率常用于状态步进时的转移系数修正

2. 全概率公式(Law of Total Probability)

设 $B_1, B_2, \dots, B_n$ 为样本空间 $\Omega$ 的一个划分(即各事件两两互斥,且 $\bigcup_{i=1}^n B_i = \Omega$,$P(B_i) > 0$)。则对任意事件 $A$,均有:

$$P(A) = \sum_{i=1}^n P(A \mid B_i) P(B_i)$$

竞赛图论边界:全概率公式在图论拓扑网络或 DP 状态机中,对应当前节点的状态概率是由其所有前驱节点转化而来的加权总和

3. 贝叶斯公式(Bayes' Theorem)

基于条件概率与全概率公式,用于实现“执果索因”的反向概率推导:

$$P(B_j \mid A) = \frac{P(A \mid B_j) \times P(B_j)}{P(A)} = \frac{P(A \mid B_j) \times P(B_j)}{\sum_{i=1}^n P(A \mid B_i) P(B_i)}$$

在已知某种结果 $A$ 发生的条件下,贝叶斯公式能够精确计算出是由特定原因 $B_j$ 引起的后验概率。


状态设计与算法推导

在常见的概率动态规划(Probability DP)中,我们通常需要维护某种特定状态发生的绝对概率

拓扑图上的概率状态设计

设 $f[u]$ 表示流程运行到图中有向节点 $u$ 的概率。假设节点 $u$ 有若干个入边来自前驱集合 $pre(u)$。对于每个前驱节点 $v \in pre(u)$,从 $v$ 走到 $u$ 的转移概率(分支系数)为 $w(v, u)$,且满足 $\sum_{k \in post(v)} w(v, k) = 1$(即出边概率归一化)。

根据全概率公式,节点 $u$ 的状态转移方程为:

$$f[u] = \sum_{v \in pre(u)} f[v] \times w(v, u)$$

执行序判定


C++ 标准源码

#include <iostream>
#include <vector>
#include <queue>
#include <cmath>

// 边的结构体定义
struct Edge {
    int to;
    double prob; // 转移条件概率 P(to | from)
};

const int MAXN = 100000;
std::vector<Edge> graph[MAXN + 5];
int in_degree[MAXN + 5];
double f[MAXN + 5]; // 状态数组:存储到达每个节点的绝对概率

// 拓扑排序跑 DAG 概率动态规划
void compute_probability_dp(int n, int start_node) {
    std::queue<int> q;

    // 初始化边界:起点概率为 1,其余默认为 0
    f[start_node] = 1.0;

    for (int i = 1; i <= n; ++i) {
        if (in_degree[i] == 0) {
            q.push(i);
        }
    }

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

        for (const auto &edge : graph[u]) {
            int v = edge.to;
            // 核心转移:全概率公式代数累加
            f[v] += f[u] * edge.prob; 

            if (--in_degree[v] == 0) {
                q.push(v);
            }
        }
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m, start_node;
    if (std::cin >> n >> m >> start_node) {
        for (int i = 0; i < m; ++i) {
            int u, v;
            double p;
            std::cin >> u >> v >> p;
            graph[u].push_back({v, p});
            in_degree[v]++;
        }

        compute_probability_dp(n, start_node);

        // 输出目标终点的概率
        for (int i = 1; i <= n; ++i) {
            std::cout << "Node " << i << " Prob: " << f[i] << "\n";
        }
    }
    return 0;
}

NOIP 实战避坑指南

  1. 概率累加时没有对孤立起点或断开节点进行条件拦截 在进行拓扑排序 DP 时,入度为 0 的节点可能不仅包括设定的起点 start_node,还包括其他与主连通块完全断开的孤立死节点。这些死节点的初始概率 f[u] = 0。如果不加控制地用它们的垃圾状态去更新后继,虽不影响数值正确性,但如果涉及复杂的乘除法运算(如求商)则极易产生 NaN(非数)异常。
  2. 浮点数精度直接比较的逻辑溃败 在判定概率转移的分支边界时(例如判断某个分支概率是否为 0),绝对不能写成 if (edge.prob == 0.0)。由于计算机内部采用 IEEE 754 浮点数二进制近似存储,连续乘法后的 0 可能会变成 $10^{-17}$ 级别的微小值。必须引入极小量 eps(如 $10^{-9}$)进行范围拦截:if (std::abs(edge.prob) < 1e-9)

经典 NOIP/洛谷 真题

1. 洛谷 P4316 绿豆蛙的归宿

2. 洛谷 P2218 [HAOI2007] 覆盖问题 / 变式 洛谷 P5104 红包发发发

原文链接: local://21.1

[h] 返回首页