【算法学习笔记】几率与指望DP

本文学习自 Sengxian 学长的博客php

以前也在CF上写了一些几率DP的题并作过总结html

建议阅读完本文再去接着阅读这篇文章:Herenode

前言

单纯只用到几率的题并非不少,从现有的 OI/ACM 比赛中来看,大多数题目须要几率与指望结合起来(指望就是用几率定义的),因此本文主要讲述指望 DP。git

指望 DP 有一些固定的方法,这里分多种方法来说述。学习


讲解

例一

#3036. 绿豆蛙的归宿spa

题意:code

给定一个起点为 \(1\),终点为 \(n\) 的有向无环图。到达每个顶点时,若是有 \(K\) 条离开该点的道路,能够选择任意一条道路离开该点,而且走向每条路的几率为 \(\frac 1 K\)。问你从 \(1\) 出发走到 \(n\) 的路径指望总长度是多少。htm

方法一:直接定义指望状态

这道题的终点很明确,那就是走到 \(n\) 即中止。对于指望 DP,咱们通常采用逆序的方式来定义状态,即考虑从当前状态到达终点的指望代价。由于在大多数状况下,终点不惟一,而起点是惟一的。
咱们定义 \(dp(i)\)为从 \(i\) 出发走到终点 \(n\) 的路径指望总长度,根据全指望公式,获得(设​ \(G_i\)为从 \(i\) 的边的集合):blog

\[dp(i) = \sum\limits_{e\in G_i}\frac{dp(e_{to}) + e_{const}}{|G_i|} \]

由于这是一个有向无环图,每一个点须要其能到达的点的状态,故咱们采用拓扑序的逆序进行计算便可。ip

【AC Code】

const int N = 100000, M = 2 * N;
int n, m;
struct node { int v, w;};
vector<node>e[N];
int d[N];    // 出度
double f[N]; // dp

double dfs(int u) {
    if (f[u] >= 0)return f[u];
    f[u] = 0;
    for (auto [v, w] : e[u]) { // tuple 需开启 C++17
        f[u] += (w + dfs(v)) / d[u];
    }
    return f[u];
}

int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    memset(f, -1, sizeof(f));
    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        e[u].push_back(node{v, w});
        d[u]++;//出度++
    }
    cout << fixed << setprecision(2) << dfs(1);
}

方法二:利用指望的线性性质

根据指望的线性性质\(E[aX +bY]=aE[X] + bE[Y]\)。因此另一种求指望的方式是分别求出每一种代价产生的指望贡献,而后相加获得答案。在本题中,路径指望总长度等于每条边产生的指望贡献之和。而每条边产生又等于通过这条边的指望次数乘这条边的代价。因此,咱们只须要算每条边的指望通过次数便可。

\((u,v,w)\) 的指望通过次数是很差直接算的,但若是咱们能算得点 \(u\) 的指望通过次数为 \(dp(u,v)\),那么边 \((u,v,w)\) 的指望通过次数是 \(dp(u)*\frac1{|G_u|}\) ,对答案的贡献就是 \(w*dp(u)*\frac1{|G_u|}\)

如何计算点 \(u\) 的指望通过次数 \(dp(u)\)呢?咱们依然考虑 DP 的方式,首先有 \(dp(u) = 1\),转移采起刷表的方式:

\[dp(e_{to})\leftarrow dp(u)*\frac1{|G_u|},e\in G_u \]

在用边 \(e\) 刷表的同时,边 \(e\) 的贡献就能够计算了,十分简洁。由于这种方法计算答案十分的便捷,并且适用范围广,因此这种『利用指望的线性性质,单独计算贡献的方法』是咱们计算指望首选的方法。

【AC Code】这里贴 Sengxian 学长的代码

typedef long long ll;
inline int readInt() {
    static int n, ch;
    n = 0, ch = getchar();
    while (!isdigit(ch)) ch = getchar();
    while (isdigit(ch)) n = n * 10 + ch - '0', ch = getchar();
    return n;
}

const int MAX_N = 100000 + 3, MAX_M = MAX_N * 2;
struct edge {
    edge *next;
    int to, cost;
    edge(edge *next = NULL, int to = 0, int cost = 0): next(next), to(to), cost(cost) {}
} pool[MAX_M], *pit = pool, *first[MAX_N];
int n, m, deg[MAX_N], outDeg[MAX_N];
double f[MAX_N];

void solve() {
    static int q[MAX_N];
    int l = 0, r = 0;
    q[r++] = 0;

    double ans = 0;
    f[0] = 1.0;
    while (r - l >= 1) {
        int u = q[l++];
        for (edge *e = first[u]; e; e = e->next) {
            f[e->to] += f[u] / outDeg[u];
            ans += f[u] * e->cost / outDeg[u];
            if (--deg[e->to] == 0) q[r++] = e->to;
        }
    }

    printf("%.2f\n", ans);
}

int main() {
    n = readInt(), m = readInt();
    for (int i = 0, u, v, w; i < m; ++i) {
        u = readInt() - 1, v = readInt() - 1, w = readInt();
        first[u] = new (pit++) edge(first[u], v, w);
        deg[v]++, outDeg[u]++;
    }
    solve();
}

例二

接着咱们考虑 Codeforces 518D 这道题,以便体会方法二的好处。

题意:\(n\) 我的排成一列,每秒中队伍最前面的人有 \(p\) 的几率走上电梯(一旦走上就不会下电梯),或者有 \(1-p\) 的几率不动。问你 \(T\) 秒事后,在电梯上的人的指望。

方法一

在本题这样一个状况中,方法一是用不了的,由于咱们的结束状态不明确。

方法三:使用指望的定义计算

若是 \(X\) 是离散的随机变量,输出值为 \(x_1,x_2,...\),输出值相应的几率为 \(p_1,p_2,...\),那么指望值是一个无限数列的和(若是不收敛,那么指望不存在):

\[E[x] =\sum\limits_ip_ix_i \]

在本题中,若是设 \(dp(i,j)\)\(i\) 秒事后,电梯上有 \(j\) 我的的几率,那么答案是:

\[\sum\limits_{0\le k\le n}dp(T,K)*K \]

因此咱们只须要求 \(dp(i, j)\) 就能够了,初始值 \(dp(0, 0) = 1\) 就能够了,仍然是采用刷表法:

\[dp(i + 1,j + 1) \leftarrow dp(i,j)*p\\ dp(i + 1,j)\leftarrow dp(i,j) * (1 - p) \]

【AC Code】

const int N = 2e3 + 10;
double p, dp[N][N];
int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    int n, t;
    cin >> n >> p >> t;
    dp[0][0] = 1;
    for (int i = 0; i < t; ++i) {
        dp[i + 1][n] += dp[i][n];
        for (int j = 0; j < n; ++j) if (dp[i][j] > 1e-10) {
                dp[i + 1][j + 1] += dp[i][j] * p;
                dp[i + 1][j] += dp[i][j] * (1 - p);
            }
    }
    double ans = 0;
    for (int i = 0; i <= n; ++i) ans += i * dp[t][i];
    cout << fixed << setprecision(6) << ans;
}

方法二

那么以前提到的适用范围广的方法二,是否能在这里用呢?答案是确定的。

延续方法三的 DP,咱们不妨将状态之间的转移抽象成边,只不过只有 \(dp(i, j)\)\(dp(i + 1, j + 1)\) 的边才有为 \(1\) 的边权,其他都为 \(0\)。由于这个 DP 涵盖了全部可能出现的状况,因此咱们仍然能够利用指望的线性性质,在刷表的过程当中进行计算答案。

本题中,没有直观的边的概念,可是咱们能够将状态之间的转移抽象成边,因为 \(dp(i, j)\)\(dp(i + 1, j + 1)\) 这一个转移是对答案有 \(1\) 的贡献的,因此咱们将它们之间的边权赋为 \(1\)

这一题将方法二抽象化了,实际上大多数题并不是是直观的,而是这种抽象的形式。

const int N = 2e3 + 10;
double p, dp[N][N];
int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    int n, t;
    cin >> n >> p >> t;
    dp[0][0] = 1;
    double ans = 0;
    for (int i = 0; i < t; ++i) {
        dp[i + 1][n] += dp[i][n];
        for (int j = 0; j < n; ++j) if (dp[i][j] > 1e-10) {
                dp[i + 1][j + 1] += dp[i][j] * p;
                dp[i + 1][j] += dp[i][j] * (1 - p);
                ans += dp[i][j] * p;
            }
    }
    cout << fixed << setprecision(6) << ans;
}

例三

BZOJ 3450

题意:给定一个序列,一些位置未肯定(是 \(\texttt{o}\)\(\texttt{x}\) 的概率各占 \(\%50%\))。对于一个 \(\texttt{ox}\) 序列,连续 \(x\) 长度的 \(\texttt{o}\) 会获得 \(x^2\) 的收益,请问最终获得的序列的指望收益是多少?

分析

这个题若是一段一段的处理,实际上并非很好作。咱们观察到 \((x + 1) ^ 2 - x ^ 2 = 2x + 1\),那么根据指望的线性性质,咱们能够单独算每个字符的贡献。咱们设 \(dp_i\) 为考虑前 ii 个字符的指望得分,\(l_i\) 为以 \(i\) 为结尾的 comb 的指望长度,\(Comb_i\) 为第 \(i\)个字符,那么有 3 种状况:

  1. \(s_i = o\) ,则 \(dp_i = dp_{i - 1} + l_{i - 1} * 2 + 1,l_i = l_{i - 1} + 1\)
  2. \(s_i = x\) ,则 \(dp_i = dp_{i - 1}\)
  3. \(s_i =\ ?\), 则 \(dP_i = dp_{i - 1} + \frac{l_i*2 + 1}{2},l_i = \frac{l_{i - 1} + 1}{2}\)

对于前两种状况,实际上是很是直观的,对于第三种状况,其实是求了一个平均长度。例如 ?oo,两种状况的长度 \(l_i\) 分别为 \([0,1,2]\)\([1,2,3]\) ,可是求了平均以后,长度 \(l_i\) 变成了 \([0.5,1.5,2.5]\) ,这样因为咱们的贡献是一个关于长度的一次多项式 \((2x + 1)\) ,因此长度平均以后,贡献也至关于求了一个平均,天然可以求得正确的得分指望。

【AC Code】

const int N = 3e5 + 10;
double dp[N], Comb[N];
int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    int n; string s;
    cin >> n >> s;
    for (int i = 0; i < n; ++i) {
        if (s[i] == 'o') {
            dp[i] = dp[i - 1] + Comb[i - 1] * 2 + 1;
            Comb[i] = Comb[i - 1] + 1;
        } else if (s[i] == 'x') {
            dp[i] = dp[i - 1];
            Comb[i] = 0;
        } else {
            dp[i] = dp[i - 1] + (Comb[i - 1] * 2 + 1) / 2;
            Comb[i] = (Comb[i - 1] + 1) / 2;
        }
    }
    cout << setprecision(4) << fixed << dp[n - 1];
}

思考:若是长度为 \(a\) 的 comb 的贡献为 \(a^3\) 时该如何解决?

Tips:因为 \((a + 1)^3 - a^3 = 3a^3 + 3a + 1\) ,因此咱们要维护 \(a^2\)\(a\) 的指望,注意 \(E_{a^2} \not= E^2_a\),因此维护 \(a^2\) 的指望是必要的。


例四

BZOJ 4318

题意:给定一个序列,每一个位置 \(o\) 的概率为 \(p_i\) ,为 \(x\) 的概率为 \(1-p_i\) 。对于一个 \(\texttt{ox}\) 序列,连续 \(x\) 长度的 \(\texttt{o}\) 会获得 \(x^3\) 的收益,请问最终获得的 \(ox\) 序列的指望收益是多少?

分析

延续例三的思路,咱们仍是分别求每个位置的贡献。根据 \((a + 1)^3 - a^3 = 3a^3 + 3a + 1\),咱们只须要维护 \(l(i)\)为以 \(i\) 为结尾的 comb 的指望长度,\(l_2(i)\)为以 \(i\) 为结尾的 comb 的指望长度的平方。注意 \(E[a^2] \not =E^2[a]\),因此维护 \(a^2\) 的指望是必要的。

int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    int n;
    double p, l1 = 0, l2 = 0, ans = 0;
    cin >> n;
    for (int i = 0; i < n; ++i) {
        cin >> p;
        ans += (3 * l2 + 3 * l1 + 1) * p;
        l2 = (l2 + 2 * l1 + 1) * p;
        l1 = (l1 + 1) * p;
    }
    cout << fixed << setprecision(1) << ans;
}

总结

指望 DP 通常来讲有它固定的模式,一种模式是直接 DP,定义状态为到终点指望,采用逆序计算获得答案。一种模式是利用指望的线性性质,对贡献分别计算,这种模式通常要求咱们求出每种代价的指望使用次数,而每种代价每每体如今 DP 的转移之中。最后的两个例题是典型的分离变量,用指望的线性性质计算答案的例子,若是状态过于巨大,那么就得考虑分离随机变量了。

本总结只是解释了几率与指望 DP 的冰山一角,它能够变化无穷,但那些实际上并不仅属于几率与指望 DP,真正核心的内容,仍是逃不出咱们几种方法。

想要深刻了解一些几率的DP的请阅读这篇文章:Here

相关文章
相关标签/搜索