← 目录 第 39 / 60 节 全部 60 节

39 · 概率与期望入门

从“数情况”走到“算期望”:先把总分拆成 0/1 指示变量之和,用线性性逐项算概率;拆不动时改成倒推 DP;最后分清 double 与除法取模两条路。
提高-预计 65 分钟概率期望线性性
本节出现的新函数 / 新语法(先看懂再用)

概率的第一性原理仍然是计数:把所有等可能的情况数清,分子是符合条件的那部分。期望在此之上多一步——把每个结果乘上它的概率,再全部相加。竞赛里这两件事都不用“模拟”,只用一个公式:期望的线性性。

别去算整体的分布,去算每一个最小单位的贡献。它通常只依赖常数个数据,于是 2ⁿ 种情况被压成 n 个乘法。

三个公式打底。P(Ā) = 1 − P(A):补集,恒成立,是“至少一个”类问题的首选。P(A∪B) = P(A) + P(B) − P(A∩B):一般并,恒成立。P(A∩B) = P(A)·P(B):只在 A、B 独立时成立。

“独立”的意思是:一个发生与否,不改变另一个的概率。两道题的正确答案分别是什么,是独立的;同一道题的两个不同选项,不独立。判断独立性不能靠感觉,要看两个事件的信息是否互相影响。

运算公式前提典型场景
补集P(Ā) = 1 − P(A)永远成立至少中一次、全部装错
互斥相加P(A∪B) = P(A) + P(B)A、B 不能同时发生掷出 1 或 2
一般并P(A∪B) = P(A)+P(B)−P(A∩B)永远成立两张牌至少一张是红桃
独立相乘P(A∩B) = P(A)·P(B)A、B 独立两次射击都命中
条件概率P(A|B) = P(A∩B)/P(B)P(B) > 0已知第一次是正面

两个可以立刻心算的例子:掷两枚硬币至少一个正面,1 − (1/2)(1/2) = 3/4;掷两枚骰子和为 7,只有 (1,6)(2,5)(3,4)(4,3)(5,2)(6,1) 六种,6/36 = 1/6。所有“和为几”的问题都是数点对,不要背公式。

新函数拆解 · double 存概率:除法、精度与输出
double p = 1.0 * cnt / tot; // 先变成浮点再除 printf("%.3f\n", ans); // 保留 3 位小数
组成含义 / 格式要求
1.0 * ...把整数强行转成浮点再除。写成 cnt / tot 是整数除法,7/6 得到 1,1/6 得到 0,概率题会整片变成 0 和 1。
double 的精度53 位尾数,约 15~16 位十进制有效数字。累加 10⁷ 项的相对误差约 10⁻⁹,输出 3 位小数安全。
为什么不用 floatfloat 只有 24 位尾数,约 7 位有效数字。累加 10⁷ 项之后误差到 10⁻² 量级,答案会错在最后两位。
判等不要写 a == b,写 fabs(a - b) < 1e-9。二进制无法精确表示 0.1,所以 0.1 + 0.2 != 0.3。
输出格式printf("%.3f", x) 与 cout << fixed << setprecision(3) << x 等价,都是四舍五入到 3 位。题目写“保留三位”就输出三位,多一位少一位都 WA。
什么时候不用它题目要求“答案 mod 998244353”时,浮点毫无用处,要换成逆元(见第三张卡)。
最小可运行示例
示例
double p = 1.0 * 3 / 6;          // 0.5;写成 3 / 6 得到 0
double q = 7.0 / 6;

printf("%.3f\n", q);             // 1.167
printf("%.6f\n", 1.0 / 3);       // 0.333333

if (fabs(p - 0.5) < 1e-9) puts("equal");   // 浮点判等要带容差
注意

分子分母都是整数,但一旦做除法,就必须保证至少一边是浮点。最省事的写法是分子写 1.0 * ...。

double 能精确表示 0.5、0.25、0.125,不能精确表示 0.1、1/3、1/7。所以“理论上是 0.5”的答案可能算成 0.49999999999999994——不影响三位小数,但会破坏判等。

新函数拆解 · 期望的线性性
E[X + Y] = E[X] + E[Y] // 恒成立,不需要独立 E[a·X + b] = a·E[X] + b
组成含义 / 格式要求
X、Y 是什么同一个随机试验上的两个量。可以是任意两个量,甚至互相依赖。
为什么不需要独立证明只用到“联合概率求和”和分配律,独立二字根本没进推导(见本节数学补给)。
最强的用法把总分拆成一堆 0/1 指示变量之和:X = X₁ + X₂ + … + Xₙ,Xᵢ 只标记“第 i 件事发生了”。于是 E[X] = Σ P(第 i 件事发生),整体期望变成 n 个局部概率相加。
不能乱用乘法没有线性性:一般 E[XY] ≠ E[X]·E[Y](只有独立时才相等);E[X²] ≠ (E[X])²。
常见误解“期望”不是“最可能的值”。一枚均匀骰子的期望是 3.5,而 3.5 永远不会掷出来。
最小可运行示例
示例
// n 个人随机坐 n 个座位,期望有多少人坐对?
// 第 i 个人坐对的概率是 1/n,n 个人相加 → 答案恒为 1,与 n 无关
double ans = 0;
for (int i = 1; i <= n; ++i) ans += 1.0 / n;      // ans = 1

// 掷一枚均匀骰子:E = Σ k·(1/6) = 21/6 = 3.5
double e = 0;
for (int k = 1; k <= 6; ++k) e += k * (1.0 / 6);
注意

指示变量是期望题的第一武器:把“做对几题”“出现几次”写成 0/1 之和,整体期望自动拆成 n 个局部概率。

局部概率通常只依赖常数个数据(例如相邻两道题的选项数),于是整题从 O(2ⁿ) 降到 O(n)。

新函数拆解 · 除法取模:逆元与费马小定理
long long inv(long long a) { return qpow(a, MOD - 2); } // MOD 是质数 ans = p % MOD * inv(q) % MOD; // p/q mod MOD
组成含义 / 格式要求
为什么不能直接除(a/b) mod MOD ≠ (a mod MOD) / (b mod MOD)。例如 1/2 mod 7:直接除得 0,正确答案是 4,因为 2×4 = 8 ≡ 1 (mod 7)。
逆元的定义a·a⁻¹ ≡ 1 (mod MOD)。把“除以 a”换成“乘以 a⁻¹”,除法就消失了。
费马小定理MOD 是质数且 a 不是 MOD 的倍数时,a^(MOD−1) ≡ 1,于是 a⁻¹ ≡ a^(MOD−2)。一次快速幂,O(log MOD)。
两个前提① MOD 必须是质数(10⁹+7、998244353 都行);② 分母 q 不能是 MOD 的倍数,否则逆元不存在。
常用值mod 10⁹+7 下:1/2 = 500000004,1/3 = 333333336,1/6 = 166666668,1/7 = 142857144。
最小可运行示例
示例
const long long MOD = 1000000007;

long long qpow(long long a, long long b) {
    long long res = 1 % MOD; a %= MOD;
    while (b) { if (b & 1) res = res * a % MOD; a = a * a % MOD; b >>= 1; }
    return res;
}

// 掷一次骰子出现 6 的概率是 1/6,写成模意义下的数:
cout << 1 * qpow(6, MOD - 2) % MOD << "\n";       // 166666668
// 验算:6 × 166666668 = 1000000008 = MOD + 1 ≡ 1
注意

什么时候用哪套:要求输出实数 → double;要求输出“答案 mod 998244353” → 逆元。后者越来越常见,因为它彻底没有精度问题。

多个分数相加时,逐项换成逆元再加,不要先把分母乘起来——分母的乘积极容易变成 MOD 的倍数,那时逆元就不存在了。

例题 · 单选错位洛谷 P1297提高+/省选-去洛谷做这题 ↗

n 道单选题,第 i 道题有 aᵢ 个选项,每个选项成为正确答案的概率相等。gx 全部做对了,但把第 i 题的答案填到了第 i+1 题的位置上(第 n 题的答案填到第 1 题)。求他期望做对几道题。

输入:一行 5 个整数 n, A, B, C, a₁(2 ≤ n ≤ 10⁷,0 ≤ A, B, C ≤ 10⁸)。因为 n 很大,a 由规则生成:q₁ = a₁,qᵢ = (qᵢ₋₁·A + B) mod 100000001(i ≥ 2),最后 aᵢ = qᵢ mod C + 1。

输出:一个实数,期望做对的题数,保留三位小数。

样例说明(n = 3,a = {2, 3, 1}): 6 种正确答案等概率,对应做对 3、1、1、1、1、0 题 期望 = 7/6 ≈ 1.167,输出 1.167
例题怎么想:把“做对几题”拆成 n 个 0/1

先处理输入。这不是“读 n 个数”,而是读 5 个参数再按规则生成 a。规则里有个陷阱:qᵢ 用的是还没 mod C 的 qᵢ₋₁ 迭代,两步必须分开——先把整串 q 生成完,再统一做 aᵢ = qᵢ mod C + 1。混在一起写,从第 2 项起就全错。

概率部分:设 Xᵢ = 1 表示“抄到第 i+1 题位置上的那个答案,恰好等于第 i+1 题的正确答案”。第 n 题的答案填在第 1 题的位置,所以 Xₙ 看的是第 1 题。做对的题数 X = X₁ + X₂ + … + Xₙ,每个 Xᵢ 只取 0 或 1。

由线性性,E[X] = Σ E[Xᵢ] = Σ P(Xᵢ = 1)。这一步把题目之间的所有关联切断了:不需要知道 2ⁿ 种全局情况,也不需要管 X₁ 与 X₂ 是不是独立,每题单独算概率就行。

算 P(Xᵢ = 1)。第 i+1 个位置上写的是“第 i 题的正确答案”,它在 1..aᵢ 上均匀,概率 1/aᵢ;第 i+1 题的正确答案在 1..a_{i+1} 上均匀,概率 1/a_{i+1},而且两者独立。于是

P = Σ_{v=1}^{min(aᵢ,aᵢ₊₁)} (1/aᵢ)(1/aᵢ₊₁) = min/(aᵢ·aᵢ₊₁) = 1/max(aᵢ, aᵢ₊₁)。一个“最小值除以乘积”,化简后就是一个“最大值取倒数”。

所以答案 = Σ_{i=1}^{n} 1/max(aᵢ, aᵢ₊₁),其中 a_{n+1} = a₁。用样例 a = {2,3,1} 验算:1/3 + 1/3 + 1/2 = 7/6 ≈ 1.167,与题面一致。

实现:n ≤ 10⁷,开数组要 40 MB;但每一项只依赖相邻两个 a,边生成边累加即可,额外空间 O(1)。注意第 n 题的答案要填到第 1 题,所以循环结束后还差一项,别漏。

精度:n 项相加,每项 ≤ 1,和 ≤ 10⁷。double 的相对误差 10⁻¹⁶,累加 n 项的绝对误差约 10⁻⁹ 量级,保留三位小数完全安全。

完整代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, A, B, C;
    long long a1;
    cin >> n >> A >> B >> C >> a1;

    // 生成规则分两步:先按递推算出未取模的 q,再统一做 q mod C + 1。
    // 这里用流式写法:raw 保存"未 mod C"的值,first / prev 保存真正用到的 a。
    long long raw = a1;
    int first = (int)(raw % C) + 1;       // 第 1 题的选项数
    int prev  = first;
    double ans = 0;

    for (int i = 2; i <= n; ++i) {
        raw = (raw * A + B) % 100000001;  // raw·A 最大约 10¹⁶,long long 才装得下
        int cur = (int)(raw % C) + 1;
        ans += 1.0 / max(prev, cur);      // E[X_{i-1}] = 1 / max(a_{i-1}, a_i)
        prev = cur;
    }
    ans += 1.0 / max(prev, first);        // 第 n 题的答案填在第 1 题,补上最后一项

    printf("%.3f\n", ans);
    return 0;
}
逐行拆解
代码片段含义与理由
cin >> n >> A >> B >> C >> a1;5 个参数。a1 要 long long:先不取模,它可能到 10⁸,而 raw·A 会到 10¹⁶。
long long raw = a1;raw 是“还没做 mod C + 1”的那串 q。它和真正参与计算的 a 必须分开存,否则从第 2 项起全错。
int first = (int)(raw % C) + 1;a₁ = q₁ mod C + 1。first 留着给最后一项用(第 n 题的答案填到第 1 题)。
raw = (raw * A + B) % 100000001;递推。先乘后加再取模,中间值最大约 10⁸×10⁸ = 10¹⁶,只有 long long 装得下,int 会直接溢出成随机数。
ans += 1.0 / max(prev, cur);★ 全题唯一的公式。1.0 不能省:写成 1 / max(...) 是整数除法,大部分项直接变成 0,答案会小得离谱。
ans += 1.0 / max(prev, first);★ 循环只处理了 i = 2..n,也就是前 n−1 项。第 n 题的答案填在第 1 题,这一项要单独补,漏了答案会偏小。
printf("%.3f\n", ans);题目要求三位小数。用 cout 也行:cout << fixed << setprecision(3) << ans。
double ans10⁷ 项相加,用 float 会丢精度,必须 double。

上面那题能拆,是因为每一小份的概率可以独立算出来。很多题拆不动:下一步去哪由当前状态决定,同一道题里的概率互相纠缠。这时改用 DP,设 dp[s] = 从状态 s 出发到结束的期望代价,终点 dp[终点] = 0。

关键在方向。dp[s] 的值依赖“从 s 一步能到达的那些状态”,所以必须先算离终点近的状态,也就是倒着推。这和上一节“只看 dp 依赖谁”是同一条规矩:依赖更大的下标,就从大到小扫。

为什么不能正推?正推时每个状态要乘上“到达它的概率”,而那个概率本身还得再递推一层;倒推时每个状态只算一次,而且 dp[终点] 是已知的 0,整条链从末端固定下来。

倒推模板 + 一个骰子问题

模板只有五行(示意,真实的边枚举那一行要换成你自己的循环)。看清楚那个“1”是什么:它是走一步本身的代价,不是概率。

倒推模板(示意)
dp[终点] = 0;                                  // 边界:到终点不用再走
for (状态 s,按"离终点从近到远"的顺序) {        // 倒序!通常就是逆拓扑序
    dp[s] = 0;
    for (每个一步可达的 t)                          // 换成你的边枚举
        dp[s] += p(s -> t) * (cost(s -> t) + dp[t]);   // 全期望公式
}
// 答案 = dp[起点]

例:一枚均匀骰子,每次掷出 1~6 中的一个。从位置 0 出发,走到 ≥ n 就停,求步数期望。

设 dp[i] = 从位置 i 出发到结束的期望步数。i ≥ n 时已经结束,dp[i] = 0。i < n 时掷出 k(概率 1/6),花费 1 步,到达 min(i+k, n)。于是

转移方程
dp[i] = 1 + (1/6) · Σ_{k=1..6} dp[ min(i + k, n) ]

这个方程“自己包含自己”,但 dp[i] 只依赖 i+1..i+6,全是更大的下标,所以从 n−1 倒着扫一遍就能一次算完,不需要解方程。

骰子的完整代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;

    vector<double> dp(n + 7, 0.0);        // dp[n..n+6] = 0:已经到达或越过终点
    for (int i = n - 1; i >= 0; --i) {    // 倒着扫:dp[i] 只依赖更大的下标
        double s = 0;
        for (int k = 1; k <= 6; ++k) s += dp[min(i + k, n)];
        dp[i] = 1 + s / 6;                // 那个 1 是"这一步"本身的代价
    }

    printf("%.6f\n", dp[0]);
    return 0;
}
逐行拆解
代码片段含义与理由
vector<double> dp(n + 7, 0.0);下标要取到 n,多开 6 格防越界;初值 0 恰好就是“已经结束”的期望。
for (int i = n - 1; i >= 0; --i)★ 方向。写成 i 从小到大,dp[i] 读到的是还没算的 0,整张表变成 1。
dp[min(i + k, n)]掷出 k 后位置是 i+k;一旦 ≥ n 就结束了,所以折到 n 这一格(它的期望是 0)。写成 dp[i+k] 会越界读垃圾。
dp[i] = 1 + s / 6;全期望公式:E = Σ p·(代价 + 后续期望)。6 种情况概率相等,所以是除以 6。
printf("%.6f\n", dp[0]);dp[0] 是起点的期望。n = 1 时输出 1.000000(掷什么都够一步);n = 6 时是 2.161394。
题目要求用什么存关键操作主要风险
输出实数,保留 k 位double1.0 * 强制浮点;printf 用 %.3f 这类整数除法、累加误差
答案 mod 质数long long乘逆元代替除法分母是 MOD 的倍数、忘取模
输出分数 p/q 的最简形式long long + gcd算完再约分中间量溢出
概率与期望的 5 个典型错法(附症状)

WA,答案全是 0 或 1:cnt / tot 是整数除法。7/6 得 1,1/6 得 0。凡是概率出现,分子上先写 1.0 *。

WA,只错最后一位 0.001:输出位数不对,或用 float 累加了 10⁷ 项。float 只有 7 位有效数字。

WA,答案恰好是正确值的 1/6 或 6 倍:倒推公式里的“这一步代价 1”漏了,或者把 s / 6 写成了 s * 6。那个 1 是步数不是概率,量和概率混在一起乘除最容易被忽略。

WA,且小数据看起来全对:输入生成顺序错。必须先按递推把未取模的 q 全部算出来,再统一 mod C + 1。两步混写,从第 2 项起就全错;更糟的是生成的 a 仍然落在 [1, C] 里,肉眼查不出异常。

RE 或输出 nan:除以 0。概率题的 0/0 多来自“不可达状态也参与了平均”,要先把不可达的分支剔掉。

倒推 DP 的三个专属坑

WA,答案偏小且接近 1:循环方向写反(从小到大)。dp[i] 读到的是还没算的 0。

看起来一样,其实靠运气:dp[min(i+k, n)] 写成 dp[i+k]。本题多开了 6 格、它们又都是 0,答案居然不变;一旦数组只开到 n,就是越界读垃圾。min 这一句是在保证语义,不是保险。

栈溢出 / 死循环:图里有环却写记忆化搜索。倒推的前提是状态依赖有拓扑序;有环时要么列方程解,要么迭代到收敛(高斯消元或不断代入)。

自己动手复现

关掉页面,把 P1297 敲一遍并提交。n 到 10⁷,记得 ios::sync_with_stdio(false)。

改一改 ①:把 1.0 / max(prev, cur) 改成 1.0 / min(prev, cur),用手算的 a = {2,3,1} 对比:正确 7/6 ≈ 1.167,改后是 2.000。

改一改 ②:把生成写成“先生成到 a 里再 mod C”,但把 raw % C + 1 提前到递推之前(即用取过模的值去迭代)。答案会变,而生成的每个 a 仍在 [1, C] 内——肉眼查不出异常,这就是最危险的一类错。

改一改 ③:把 double ans 换成 float ans,自己造 n = 10⁷ 的数据,看最后三位小数是否还稳。

改一改 ④:把骰子程序的循环改成 for (int i = 0; i < n; ++i)(正序),n = 6 时输出 1.000000,而正确值是 2.161394。

数学补给 · 线性性为什么恒成立;几何分布;误差怎么算

先证线性性。设 X、Y 定义在同一试验上,联合分布为 P(X=x, Y=y)。期望的定义是“值 × 概率”求和,于是

线性性的证明
E[X+Y] = Σ_x Σ_y (x+y)·P(X=x, Y=y)
       = Σ_x x·Σ_y P(X=x, Y=y)  +  Σ_y y·Σ_x P(X=x, Y=y)
       = Σ_x x·P(X=x)  +  Σ_y y·P(Y=y)
       = E[X] + E[Y]

中间那步只用了“边缘概率 = 联合概率求和”和分配律,独立二字从未出现。这就是它可以无条件使用的原因。而 E[XY] = E[X]E[Y] 需要把 P(X=x,Y=y) 拆成 P(X=x)P(Y=y),那一步只有独立时才成立。

再看几何分布:每次成功概率 p,独立重复,求首次成功所需次数的期望。设所求为 E。第一次就成功(概率 p)花 1 次;失败(概率 1−p)花 1 次,然后一切重来,还要 E 次。于是

几何分布的期望
E = 1 + (1 − p)·E      →      p·E = 1      →      E = 1/p

用级数验证:E = Σ_{k≥1} k(1−p)^(k−1)·p = p/(1−(1−p))² = 1/p。p = 1/6 时 E = 6:等一个 6 平均要掷 6 次。这个“E = 1 + (1−p)E”的自指写法,正是倒推 DP 的雏形。

方差的三条常用性质:Var(X) = E[X²] − (E[X])²,Var(aX+b) = a²·Var(X),独立时 Var(X+Y) = Var(X) + Var(Y)。注意 E[X²] ≠ (E[X])²——这就是为什么 OSU! 那类题必须单独维护“平方的期望”和“立方的期望”。

最后算一笔精度的账。double 有 53 位尾数,机器精度 ε ≈ 2.2×10⁻¹⁶。累加 n 项的相对误差最坏约 n·ε:n = 10⁷ 时是 2.2×10⁻⁹,n = 10⁹ 时是 2.2×10⁻⁷。

n(累加项数)相对误差 ≈ n·ε和 ≈ n 时的绝对误差3 位小数安全吗
10³2.2×10⁻¹³2.2×10⁻¹⁰安全
10⁵2.2×10⁻¹¹2.2×10⁻⁶安全
10⁷2.2×10⁻⁹0.022和是 10⁷ 量级时危险
10⁹2.2×10⁻⁷220完全不能用 double 直接加

结论:误差要看绝对量级,不能只看项数。P1297 的和不超过 10⁷ 而每项 ≤ 1,实际误差远小于上界,所以 double 稳过。但如果你在累加 10⁹ 个 10⁹ 量级的数,就必须换 long double、Kahan 求和或干脆取模。

取模那一侧没有误差问题,只有“逆元存不存在”的问题:mod 10⁹+7 下 1/2 = 500000004、1/3 = 333333336、1/6 = 166666668、1/7 = 142857144。验算只有一个办法:把逆元乘回分母,结果必须是 1。

举一反三 · 想清楚这些再往下走
  1. 为什么“做对题数”能拆成 n 个概率相加,而“做对题数的平方”不能?把 (X₁+…+Xₙ)² 展开,看看多出了哪些交叉项,它们在什么条件下才能拆开。
  2. 如果 P1297 改成“第 i 题的答案抄到第 i+2 题的位置”,答案会变成什么?为什么公式里还是“取最大值再取倒数”?
  3. 掷骰子直到连续两次都是 6 才停,期望掷几次?状态只有“当前连续 6 的个数 0 / 1 / 2”三种,试着倒推列方程。
  4. 图上有环时为什么不能倒推?举一个环上最简单的例子(两个点互相走),说明“列方程”和“不断代入”分别怎么做。
  5. 什么情况下必须用逆元而不能用 double?如果分母恰好是 MOD 的倍数,逆元不存在,这时题目通常保证了什么?
  6. n 个人随机坐 n 个座位,“恰好 k 个人坐对”的方案数是 C(n,k)·D(n−k)。这个式子里哪一部分是计数、哪一部分是概率?
自测:能全部回答再进下一节
挑战题 · OSU!
洛谷 P1654提高-去洛谷做这题 ↗

一共 n 次操作,每次成功记 1、失败记 0,得到一个 01 串。串中每一段极长的连续 1,若长度为 X,就贡献 X³ 分(不同段分别计分)。给出每次操作的成功率,求最终得分的期望。

输入:第一行一个整数 n(n ≤ 10⁵),接下来 n 行每行一个 [0,1] 之间的实数 pᵢ,表示第 i 次操作的成功率。

输出:一个实数,四舍五入保留 1 位小数。

引导性提示(先只看这里)

不要指望“得分的期望 = 长度的期望的立方”。E[X³] ≠ (E[X])³。

把总分拆成“每一步新增的分数”。如果第 i 步成功,且它左边已经连了 L 个 1,这一步让分数增加多少?

把 (L+1)³ 减 L³ 展开,你会发现只需要维护“L 的期望”和“L² 的期望”两个量。

想看思路了?点开完整思路

只看第 i 位。设它左边紧挨着的连续 1 的长度为 L(第 i 位是 0 时 L 取 0)。若第 i 位成功,这一段从长度 L 变成 L+1,分数增加 (L+1)³ − L³ = 3L² + 3L + 1;若失败,增加 0。把每一步的增量加起来,正好等于最终的 X³ 之和(每一段内部逐项累加,首尾相接)。

于是总分的期望 = Σ E[第 i 步的增量],这一步用线性性,不需要任何独立性假设。

而 E[第 i 步的增量] = pᵢ · (3·E[L²] + 3·E[L] + 1)。这里又用了一次线性性:把 3L² + 3L + 1 的期望拆成三项期望。注意 (L+1)³ 展开里有 L·L 这种项,所以 E[L²] 必须单独维护——它就是“为什么不能只存 E[L]”的全部原因。

维护两个量:e1 = E[L]、e2 = E[L²]。第 i 位之后,若成功则 L 变成 L+1,否则 L 变成 0。于是 e1 = p(e1+1),e2 = p(e2 + 2e1 + 1)。

★ 更新顺序:先用旧的 e1、e2 算出这一步的期望增量,再用旧的 e1 更新 e2,最后更新 e1。顺序错一步,e2 里就会混进新的 e1。

复杂度 O(n),n = 10⁵。答案最大到 n³ = 10¹⁵,用 double(它对 10¹⁵ 的 ulp 约 0.125,本题数据允许);保险起见也可以开 long double。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    scanf("%d", &n);

    double e1 = 0, e2 = 0;       // e1 = E[L],e2 = E[L²],L 是"以当前位置结尾的连续 1 长度"
    double ans = 0;

    for (int i = 1; i <= n; ++i) {
        double p;
        scanf("%lf", &p);

        ans += p * (3 * e2 + 3 * e1 + 1);   // 先用旧的 e1、e2 算这一步的期望增量
        e2 = p * (e2 + 2 * e1 + 1);         // 再用旧的 e1 更新 e2(顺序不能换)
        e1 = p * (e1 + 1);                  // 最后更新 e1
    }

    printf("%.1f\n", ans);
    return 0;
}
关键片段拆解
片段作用
double e1 = 0, e2 = 0;初始时左边没有 1,L = 0,所以 E[L] = E[L²] = 0。
ans += p * (3*e2 + 3*e1 + 1);★ 核心公式。(L+1)³ − L³ = 3L² + 3L + 1,再乘上“这一步成功”的概率 p。失败时增量为 0,所以只乘 p,不用乘 (1−p)。
e2 = p * (e2 + 2*e1 + 1);若成功,L 变成 L+1,于是 L² 变成 L²+2L+1,取期望得 e2+2e1+1;若失败 L 归零。所以在 p 概率下取这个值。
e1 = p * (e1 + 1);同样的道理,成功时长度加一,失败时归零。
更新顺序e2 的更新式里要用旧的 e1。先更新 e1 再更新 e2,就把新的 e1 混进去了,答案会偏大。习惯写法:算 ans → 更新 e2 → 更新 e1。
printf("%.1f\n", ans);题目要求保留 1 位小数。n = 3、三次都是 0.5 时,输出 6.0。
这题最容易错的地方
  • 把 E[X³] 当成 (E[X])³,或者以为“总分的期望 = 期望长度的立方”。三次方不满足线性性。
  • 只维护 e1 不维护 e2。展开 (L+1)³ 时会出现 3L²,缺了这一项答案会明显偏小。
  • 更新顺序反了:先更新 e1 再用它更新 e2,等于用了 L+1 去算 L²,答案偏大。
  • 把“每一段的 X³”理解成“整串 1 的个数的立方”。分段计分是题目的关键设定,样例里 101 得 2 分就是证据。
延伸阅读 / 参考