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\) 次。
所以:
展开:
移项:
所以单关期望尝试次数为 \(\dfrac{1}{1-p}\)。
1.2 单关的失败次数期望
设 \(X\) 为单关尝试次数,\(Y\) 为单关失败次数。因为最后一次一定成功,所以:
于是:
1.3 全部 \(n\) 关的总期望
\(n\) 关独立,总尝试次数 \(T = X_1 + \cdots + X_n\),由期望线性性质:
总成功次数恒为 \(n\),所以总失败次数 \(F = T - n\):
1.4 为什么道具超过 200 就可以近似
由 \(n \times p \le 20\) 且 \(p \le 0.5\):
也就是说,即使有无限道具,整个通关过程平均只会失败不超过 40 次。道具用完的概率随道具数量指数衰减,当 \(k > 200\) 时,道具几乎不可能用完。
因此当 \(k > 200\) 时,直接输出无限道具的期望:
只有当 \(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 个道具,从当前关继续,状态变为 \(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[n][j] = 0\)。从 \(n\) 往下计算:
于是对于 \(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] = A_i + B_i \cdot x\)。由 \(dp[0][j] = x\) 得 \(A_0 = 0,\ B_0 = 1\)。递推:
所以:
可以验证 \(B_i \equiv 1\)。并且 \(A_i\) 有闭式解:
因此可以预处理:
3.4 衔接解出 \(x\)
假设阈值是 \(i_0\)。在 \(i = i_0 - 1\) 处,既属于 \(i < i_0\) 区域,又可以用 \(i_0\) 处的值表示:
同时由 \(pre\) 表示:
令两者相等:
解出:
在代码中,我们用 \(id = i_0 - 1\) 作为循环变量,则:
然后验证 \(x\) 是否满足阈值条件:
即:
第一个满足的 \(id\) 对应的 \(i_0 = id+1\) 就是真正的阈值。
4. 算法流程
- 如果 \(k > 200\),直接输出 \(n / (1-p)\),结束。
- 否则令 \(k = \min(k, 200)\)。
- 预处理 \(pow\_inv[i] = q^{-i}\) 和 \(preA[i] = (1 - q^{-i}) / p\)。
- 初始化 \(j=0\) 的 \(dp\) 数组:\[dp[i][0] = \frac{q^{-n} - q^{-i}}{p} \]
- 对于 \(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]\)。
- 输出 \(dp[0]\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
//使用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;
}

浙公网安备 33010602011771号