Brotato

Brotato

概率dp,边际效益,方程求解未知数,寻找临界值

1. 道具数量很大时的近似

题目保证 \(n \times p \le 20\),且 \(0 < p \le 0.5\)。

1.1 单关的期望尝试次数

只看某一关,每次尝试成功概率 \(q = 1-p\),失败概率 \(p\)。若有无限道具,失败后从当前关继续,则这一关就是反复尝试直到成功。

设 \(E\) 为这一关需要的期望尝试次数。第一次尝试:

  • 成功概率 \(q\),尝试次数为 1,结束;
  • 失败概率 \(p\),浪费 1 次,状态完全回到原点,还需 \(E\) 次。

所以:

\[E = q \cdot 1 + p \cdot (1 + E) \]

展开:

\[E = q + p + pE = 1 + pE \]

移项:

\[E(1-p) = 1 \implies E = \frac{1}{1-p} \]

所以单关期望尝试次数为 \(\dfrac{1}{1-p}\)。

1.2 单关的失败次数期望

设 \(X\) 为单关尝试次数,\(Y\) 为单关失败次数。因为最后一次一定成功,所以:

\[Y = X - 1 \]

于是:

\[E[Y] = E[X] - 1 = \frac{1}{1-p} - 1 = \frac{p}{1-p} \]

1.3 全部 \(n\) 关的总期望

\(n\) 关独立,总尝试次数 \(T = X_1 + \cdots + X_n\),由期望线性性质:

\[E[T] = n \cdot \frac{1}{1-p} = \frac{n}{1-p} \]

总成功次数恒为 \(n\),所以总失败次数 \(F = T - n\):

\[E[F] = E[T] - n = \frac{n}{1-p} - n = \frac{n p}{1-p} \]

1.4 为什么道具超过 200 就可以近似

由 \(n \times p \le 20\) 且 \(p \le 0.5\):

\[E[F] = \frac{n p}{1-p} \le \frac{20}{1-p} \le \frac{20}{0.5} = 40 \]

也就是说,即使有无限道具,整个通关过程平均只会失败不超过 40 次。道具用完的概率随道具数量指数衰减,当 \(k > 200\) 时,道具几乎不可能用完。

因此当 \(k > 200\) 时,直接输出无限道具的期望:

\[\boxed{\frac{n}{1-p}} \]

只有当 \(k \le 200\) 时,才需要 DP 精确计算。

2. 状态定义与转移

设 \(dp[i][j]\) 表示:已经通过了 \(i\) 关,还剩 \(j\) 个道具,从第 \(i+1\) 关开始,通关剩余所有关卡所需的最小期望尝试次数。

目标:\(dp[0][k]\)。

边界:\(dp[n][j] = 0\)(已经全部通过)。

转移:当前挑战第 \(i+1\) 关(即状态 \(i\))。

  • 成功概率 \(q = 1-p\):进入 \(dp[i+1][j]\)。
  • 失败概率 \(p\):有两种选择:
    1. 使用道具:消耗 1 个道具,从当前关继续,状态变为 \(dp[i][j-1]\)。
    2. 不使用道具:回到第一关重新开始,状态变为 \(dp[0][j]\)(道具不消耗)。

因此:

\[dp[i][j] = 1 + q \cdot dp[i+1][j] + p \cdot \min(dp[i][j-1],\ dp[0][j]) \]

3. 处理未知数 \(x = dp[0][j]\)

固定道具数 \(j\),令 \(x = dp[0][j]\),它是当前轮唯一未知数。上一轮的值 \(f(i) = dp[i][j-1]\) 已知,且关于 \(i\) 单调递减(\(i\) 越大,剩余关卡越少,期望越小)。

3.1 阈值性质

因为 \(f(i)\) 单调递减,\(x\) 是常数,所以存在阈值 \(i_0\):

  • 当 \(i < i_0\) 时:\(f(i) \ge x\),失败后从头开始更优,\(\min = x\)。
  • 当 \(i \ge i_0\) 时:\(f(i) < x\),失败后使用道具更优,\(\min = f(i)\)。

3.2 反向辅助数组 \(suf\)

在 \(i \ge i_0\) 的区域,转移不涉及未知数 \(x\):

\[dp[i][j] = 1 + q \cdot dp[i+1][j] + p \cdot f(i) \]

边界 \(dp[n][j] = 0\)。从 \(n\) 往下计算:

\[suf[n] = 0 \]

\[suf[i] = 1 + q \cdot suf[i+1] + p \cdot f(i), \quad i = n-1, n-2, \dots, 0 \]

于是对于 \(i \ge i_0\),有 \(dp[i][j] = suf[i]\)。特别地,\(dp[i_0][j] = suf[i_0]\)。

3.3 正向线性表示 \(pre\)

在 \(i < i_0\) 的区域,转移为:

\[dp[i][j] = 1 + q \cdot dp[i+1][j] + p \cdot x \]

设 \(dp[i][j] = A_i + B_i \cdot x\)。由 \(dp[0][j] = x\) 得 \(A_0 = 0,\ B_0 = 1\)。递推:

\[dp[i+1][j] = \frac{dp[i][j] - 1 - p x}{q} \]

所以:

\[A_{i+1} = \frac{A_i - 1}{q}, \qquad B_{i+1} = \frac{B_i - p}{q} \]

可以验证 \(B_i \equiv 1\)。并且 \(A_i\) 有闭式解:

\[A_i = \frac{1 - q^{-i}}{p} \]

因此可以预处理:

\[preA[i] = \frac{1 - q^{-i}}{p}, \qquad preB[i] = 1 \]

3.4 衔接解出 \(x\)

假设阈值是 \(i_0\)。在 \(i = i_0 - 1\) 处,既属于 \(i < i_0\) 区域,又可以用 \(i_0\) 处的值表示:

\[dp[i_0-1][j] = 1 + q \cdot dp[i_0][j] + p \cdot x = 1 + q \cdot suf[i_0] + p \cdot x \]

同时由 \(pre\) 表示:

\[dp[i_0-1][j] = preA[i_0-1] + x \]

令两者相等:

\[preA[i_0-1] + x = 1 + q \cdot suf[i_0] + p \cdot x \]

解出:

\[x = \frac{1 + q \cdot suf[i_0] - preA[i_0-1]}{1 - p} = \frac{1 + q \cdot suf[i_0] - preA[i_0-1]}{q} \]

在代码中,我们用 \(id = i_0 - 1\) 作为循环变量,则:

\[x = \frac{1 + q \cdot suf[id+1] - preA[id]}{q} \]

然后验证 \(x\) 是否满足阈值条件:

\[f(id+1) \le x \le f(id) \]

即:

\[ndp[id+1] \le x \le ndp[id] \]

第一个满足的 \(id\) 对应的 \(i_0 = id+1\) 就是真正的阈值。

4. 算法流程

  1. 如果 \(k > 200\),直接输出 \(n / (1-p)\),结束。
  2. 否则令 \(k = \min(k, 200)\)。
  3. 预处理 \(pow\_inv[i] = q^{-i}\) 和 \(preA[i] = (1 - q^{-i}) / p\)。
  4. 初始化 \(j=0\) 的 \(dp\) 数组:

    \[dp[i][0] = \frac{q^{-n} - q^{-i}}{p} \]

  5. 对于 \(j = 1, 2, \dots, k\):
    • 交换 \(dp\) 和 \(ndp\),使 \(ndp\) 保存上一层 \(dp[i][j-1]\)。
    • 反向计算 \(suf\):

      \[suf[n] = 0,\quad suf[i] = 1 + q \cdot suf[i+1] + p \cdot ndp[i] \]

    • 寻找阈值 \(id\)(\(id = i_0 - 1\)):
      遍历 \(id = 0, 1, \dots, n-1\),计算

      \[tx = \frac{1 + q \cdot suf[id+1] - preA[id]}{q} \]

      若 \(ndp[id+1] \le tx \le ndp[id]\),则 \(x = tx\),跳出。
    • 填充当前层:
      • 对于 \(i \le id\):\(dp[i] = preA[i] + x\)。
      • 对于 \(i > id\):\(dp[i] = suf[i]\)。
  6. 输出 \(dp[0]\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;

void solve() {
    int n; int k; cin >> n >> k;
    ld p, q; cin >> p;
    q = 1.0 - p;
    cout << fixed << setprecision(10);
    if(k > 200) {
        ld ans = (ld)n / (1 - p);
        cout << ans << "\n";
        return;
    }

    k = min(200, k);
    vector<pair<ld, ld>> dp(n + 1, {0.0, 0.0}), ndp(n + 1, {0.0, 0.0});
    //预处理 k = 0的情况
    dp[0].second = 1.0;
    for(int i = 1; i <= n; i ++) {
        dp[i].first = (dp[i - 1].first - 1) / q;
        dp[i].second = (dp[i - 1].second - p) / q;
    }
    ld y = (ld)-1 * dp[n].first / dp[n].second;
    for(int i = 0; i <= n; i ++) {
        dp[i].first += dp[i].second * y; dp[i].second = 0;
    }
    //pre 从前往后推
    vector<pair<ld, ld>> pre(n + 1, {0.0, 0.0});
    pre[0].second = 1.0;
    for(int i = 1; i <= n; i ++) {
        pre[i].first = (pre[i - 1].first - 1) / q;
        pre[i].second = (pre[i - 1].second - p) / q;
    }
    for(int j = 1; j <= k; j ++) {
        swap(dp, ndp);
        vector<pair<ld, ld>> suf(n + 1, {0.0, 0.0});
        for(int i = n - 1; i >= 0; i --) suf[i].first = 1.0 + q * suf[i + 1].first + p * ndp[i].first; 
        
        int id = 0; ld x = 0.0;
        for(;id < n; id ++) {
            pair<ld, ld> tim;
            tim.first = 1.0 + q * suf[id + 1].first;
            tim.second = q * suf[id + 1].second + p;
            ld tx = (tim.first - pre[id].first) / (pre[id].second - tim.second);
            if(tx <= ndp[id].first && tx >= ndp[id + 1].first) {x = tx; break;}
        }
        for(int i = 0; i <= id; i ++) dp[i].first = pre[i].first + pre[i].second * x;
        for(int i = id + 1; i <= n; i ++) dp[i].first = suf[i].first + suf[i].second * x;
    }

    cout << dp[0].first << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int _ = 1; //cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-11-03 21:56  thenrify  阅读(2)  评论(0)    收藏  举报