抛硬币移宝石的期望回返时间:把游戏看成带反射壁的随机游走,用 Kac 定理化简成几何级数,整数通分精确求值。
OJ: roj
题目 ID: 10021
难度:普及+/提高-
标签:
创建: 2026-10-02 17:51
更新: 2026-10-07 11:01
形式化题目
Alice 初始有
- 正面:Alice 给 Bob 一颗宝石(Alice 为 0 时状态不变);
- 反面:Bob 给 Alice 一颗宝石(Bob 为 0 时状态不变)。
若某个回合结束时 Alice 恰有
注意终止条件的含义:Alice 初始就恰有
以样例
digraph G {
rankdir = LR;
node [shape = circle, style = filled, fillcolor = lightblue, fontsize = 12];
s0 [label = "k=0\nAlice空手", fillcolor = lightyellow];
s1 [label = "k=1\n起点=终点", fillcolor = lightgreen];
s2 [label = "k=2\nBob空手", fillcolor = lightyellow];
s0 -> s0 [label = "正面 p(不用给)", constraint = false];
s0 -> s1 [label = "反面 1-p"];
s1 -> s0 [label = "正面 p"];
s1 -> s2 [label = "反面 1-p"];
s2 -> s1 [label = "正面 p"];
s2 -> s2 [label = "反面 1-p(不用给)", constraint = false];
}
观察这张状态图:只要第 1 回合没回到 1,游戏就进入了"从
正解
思路
第一步:把游戏建模成随机游走。
设
- 内部状态
:以概率 走到 ,以概率 走到 ; - 端点反射:
(Alice 空手)时正面"不用给", 概率 ; 时反面 概率 。
游戏终止时刻
第二步:朴素做法及其瓶颈。
设
这是
第三步:用回返定理一步得到封闭形式。
这条随机游走每一步都至少挪动(或原地反射)一次,从任何状态出发都能以正概率回到
其中
于是期望回返时间
它也可以从第二步的方程组硬解出来:特征方程
如果不想直接引用定理,也可以用平稳分布的含义论证:这条链长期运行时,每回返一次就"消耗"一个回返间隔,而长期处于各状态的比例就是
第四步:精确计算,绕开浮点溢出。
令 double/long double 逐项累加会有溢出和精度风险(官方 C++ 题解靠"每项为正、乘除交替"侥幸避开,但 Python 里我们有更好的武器:精确整数)。
Decimal(40 位有效数字)做一次除法,误差远小于
整个算法只有
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 18:00
# update_at: 2026-10-02 18:00
import sys
from decimal import Decimal, getcontext
PRECISION = 40 # 有效数字 40 位,远超题目 1e-6 的相对误差要求
def solve() -> None:
getcontext().prec = PRECISION
it = iter(sys.stdin.read().split())
n, m = int(next(it)), int(next(it))
p_text = next(it)
whole, _, frac = p_text.partition('.') # 有限小数按位数缩放成整数,全程只用整数运算
scale = 10 ** len(frac) # 缩放系数:小数 6 位时 scale = 10^6
p_scaled = int(whole or "0") * scale + int(frac or "0")
# Alice 的宝石数在 [0, n+m] 上随机游走:正面 -1(0 处 Bob 空手,状态不动),
# 反面 +1(n+m 处 Alice 空手,状态不动),两个端点等价于反射壁。
# 游戏在 Alice 的宝石数"回到出发值 n"的那个回合结束,由回返定理 E = 1/π(n),
# 其中 π(k) ∝ (p/q)^k,于是 E = Σ_{k=0}^{n+m} ((1-p)/p)^{k-n}。
# 整数化:令 a/b = (scale-p_scaled)/p_scaled,通分 a^n·b^m 后
# 分子分母都是至多 1200 位的整数,避免浮点误差也避免大分数归约。
a = scale - p_scaled # q = 1-p 的缩放分子
b = p_scaled # p 的缩放分子
total = n + m
num = sum(a ** k * b ** (total - k) for k in range(total + 1)) # Σ a^k·b^{n+m-k}
den = a ** n * b ** m
print(f"{Decimal(num) / Decimal(den):.10f}")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-04 23:00
* update_at: 2026-10-04 23:00
*/
// main.cpp:roj 10021 游戏
// 把 Alice 的宝石数看成 [0, n+m] 上的反射随机游走,游戏结束时它恰好回到出发点 n。
// 由 Kac 回返定理,期望回返时间 E = 1/pi(n),其中平稳分布 pi(k) 正比于 (q/p)^k,
// 约去公共因子后 E = sum_{k=0}^{n+m} ((1-p)/p)^(k-n)。
// 注意 (1-p)/p 最大约 1e6、指数最大 200,直接求幂会溢出到 1e600;
// 所以先定位最大项、把它提到外面,再用 log10 分离指数与尾数,手工拼出定点小数。
#include <cmath>
#include <cstdio>
#include <iostream>
#include <string>
using namespace std;
typedef long long ll;
ll n, m; // Alice、Bob 的初始宝石数
ll scale; // 概率 p 的小数位数对应的 10 的幂,例如 p = 0.125 时 scale = 1000
ll p_scaled; // p 按 scale 缩放后的整数分子,p = p_scaled / scale
ll q_scaled; // q = 1 - p 同分母下的整数分子,q_scaled = scale - p_scaled
// 读入 n、m 和有限小数 p。
// p 用字符串读入并按小数位数缩放成整数,避免直接读浮点带来的表示误差。
void read_input() {
string p_text;
cin >> n >> m;
cin >> p_text;
ll point = p_text.size(); // 小数点位置;没有小数点时保持在末尾
for (ll i = 0; i < (ll)p_text.size(); i++) {
if (p_text[i] == '.') point = i;
}
scale = 1;
for (ll i = point + 1; i < (ll)p_text.size(); i++) scale *= 10;
p_scaled = 0;
for (ll i = 0; i < (ll)p_text.size(); i++) {
if (p_text[i] == '.') continue;
p_scaled = p_scaled * 10 + (p_text[i] - '0');
}
q_scaled = scale - p_scaled;
}
// 输出 value * 10^exponent 的定点小数(保留 8 位小数)。
// value 落在 [1, 10),exponent >= 0,整数部分不足的位用 0 补齐;
// 超出浮点有效位数的低位补 0,相对误差仍可忽略。
void print_scaled(long double value, ll exponent) {
char buffer[64];
snprintf(buffer, sizeof(buffer), "%.17Lf", value);
char digits[64]; // value 的有效数字,去掉小数点后按位存放
ll len = 0;
for (ll i = 0; buffer[i] != '\0' && len < 40; i++) {
if (buffer[i] == '.') continue;
digits[len] = buffer[i];
len++;
}
string out;
for (ll i = 0; i <= exponent; i++) out.push_back(i < len ? digits[i] : '0');
out.push_back('.');
for (ll i = 0; i < 8; i++) {
ll pos = exponent + 1 + i;
out.push_back(pos < len ? digits[pos] : '0');
}
printf("%s\n", out.c_str());
}
int main() {
read_input();
ll total = n + m; // 宝石总数,也是随机游走状态的上界
long double r = (long double)q_scaled / (long double)p_scaled; // 反向概率比 q/p
// 找出答案 sum r^(k-n) 中绝对值最大的一项所在下标 dominant。
// r >= 1 时最大项在 k = total(指数为 m),r < 1 时最大项在 k = 0(指数为 -n)。
ll dominant = total;
if (r < 1.0L) dominant = 0;
// 提出最大项后 sum = r^(dominant-n) * s,其中 s = sum r^(k-dominant) 落在 [1, total+1]。
// 从 dominant 向两侧扩展时每项都不超过 1,因此不会溢出。
long double s = 1.0L;
long double term = 1.0L;
for (ll k = dominant - 1; k >= 0; k--) {
term /= r;
s += term;
}
term = 1.0L;
for (ll k = dominant + 1; k <= total; k++) {
term *= r;
s += term;
}
// log10(答案) = (dominant - n) * log10(r) + log10(s)。
long double log10_answer = (long double)(dominant - n) * log10l(r) + log10l(s);
ll exponent = (ll)floorl(log10_answer);
long double mantissa_part = log10_answer - (long double)exponent; // 答案的小数尾数部分,落在 [0, 1)
while (mantissa_part < 0.0L) {
mantissa_part += 1.0L;
exponent--;
}
while (mantissa_part >= 1.0L) {
mantissa_part -= 1.0L;
exponent++;
}
long double mantissa = powl(10.0L, mantissa_part); // 尾数,落在 [1, 10)
print_scaled(mantissa, exponent);
return 0;
}复杂度
设
- 时间复杂度:
次大整数乘法,参与运算的整数不超过 位,单次乘法 以内,总体在毫秒级。 - 空间复杂度:
,只保存当前的分子累加值与两个幂(不存储整张幂表)。 - 若用 C++ 实现,同样的公式用
long double计算即可(注意 有正有负时改成乘除交替,避免 方向溢出)。
总结
- 关键观察 1:终止条件是"Alice 的宝石数回到初始值
",游戏求的是回返时间而非首达时间——这一步想错了,后面全错。 - 关键观察 2:宝石数变化是一个带反射壁的随机游走,由 Kac 回返定理,期望回返时间等于平稳概率的倒数
,其中 。 - 关键化简:
约去二项系数与 后只剩几何级数 ,不用任何方程组。 - 实现要点:小数按位数缩放成整数、通分
,用大整数 + Decimal精确求值,彻底避开与 很大时的浮点问题。
图示解析
最后把整条推导路线串起来,从朴素方程组走到整数化求值:
digraph G {
rankdir = TB;
node [shape = box, style = rounded, fontsize = 11];
a [label = "朴素:E_k 期望方程组\nn+m+1 个未知数", fillcolor = lightyellow, style = "rounded,filled"];
b [label = "特征方程 pλ²-λ+q=0\n根 λ=1 与 λ=q/p", fillcolor = lightyellow, style = "rounded,filled"];
c [label = "细致平衡 π_k ∝ (p/q)^k\nKac 定理 E[T] = 1/π_n", fillcolor = lightgreen, style = "rounded,filled"];
d [label = "几何级数\nE = Σ ((1-p)/p)^(k-n)", fillcolor = lightgreen, style = "rounded,filled"];
e [label = "p=b/s 整数化,通分 a^n·b^m\n大整数 + Decimal 精确求值", fillcolor = lightblue, style = "rounded,filled"];
a -> b [label = "硬解方程组"];
a -> c [label = "换视角:回返时间"];
c -> d [label = "约去二项系数"];
b -> d [label = "代数验证,结果一致"];
d -> e [label = "绕开浮点溢出"];
}
从图里能看出两条殊途同归的路线:左路(朴素方程组 → 特征根)是纯代数推导,右路(细致平衡 → Kac 定理)是概率论证,两者最终都收敛到同一个几何级数。实现层再补最后一块:利用"有限小数"这个输入约束做整数化,让 Python 的精确运算成为可能。
EOF