我们先来理解一下这个简化版osu游戏的规则。游戏由n次操作组成,每次操作有成功(记为1)和失败(记为0)两种结果。每个操作的成功概率是给定的,我们需要计算所有可能的01串对应的分数期望值。
关键计分规则是:对于极长的连续1串(即不被其他1包含的最长连续1串),长度为X的串贡献X³分。例如:
直接计算所有可能的01串并求期望显然不可行,因为时间复杂度是O(2ⁿ)。我们需要找到一种更高效的方法来计算期望值。
动态规划是解决这类期望计算问题的有效方法。我们可以定义几个辅助数组来记录不同阶的期望值:
对于第i次操作,有两种可能:
因此,递推公式为:
code复制b[i] = (b[i-1] + 1) * a[i]
c[i] = (c[i-1] + 2*b[i-1] + 1) * a[i]
d[i] = d[i-1] + (3*c[i-1] + 3*b[i-1] + 1) * a[i]
这个算法只需要遍历一次所有操作,每个操作执行固定数量的计算,因此时间复杂度是O(n),空间复杂度也是O(n)(可以优化到O(1))。
c复制#include<stdio.h>
#define R register
#define N 100001
int m; // 操作次数
double a[N], b[N], c[N], d[N]; // 概率和三个辅助数组
c复制int main(void)
{
scanf("%d",&m); // 读取操作次数
// 遍历每个操作
for(R int i=1; i<=m; i++)
{
scanf("%lf",&a[i]); // 读取当前操作的成功概率
// 计算以i结尾的连续1的期望长度
b[i]=(b[i-1]+1)*a[i];
// 计算以i结尾的连续1的平方期望长度
c[i]=(c[i-1]+b[i-1]*2+1)*a[i];
// 计算前i次操作的总期望分数
d[i]=d[i-1]+(c[i-1]*3+b[i-1]*3+1)*a[i];
}
// 输出结果,保留1位小数
printf("%.1lf",d[m]);
return 0;
}
虽然这个实现已经足够高效,但我们还可以进行一些优化:
让我们用题目中的样例来验证这个算法:
输入:
code复制3
0.5
0.5
0.5
计算过程:
最终输出6.0,与题目给出的样例结果一致。
这个解法利用了期望的线性性质:E[X³]可以通过分解为E[X]、E[X²]等低阶期望来计算。具体来说:
对于连续1的长度X,当新增一个1时:
因此,我们可以分别维护X、X²和X³的期望值。
让我们更严格地证明递推关系的正确性:
定义:
对于第i次操作:
因此,期望值为:
E[l[i]] = (E[l[i-1]] + 1) * a[i]
E[s[i]] = (E[s[i-1]] + 2E[l[i-1]] + 1) * a[i]
E[f[i]] = E[f[i-1]] + (3E[s[i-1]] + 3E[l[i-1]] + 1) * a[i]
这与我们的递推公式完全一致。
由于题目要求保留1位小数,而概率是实数,需要注意:
需要特别注意的边界情况包括:
如果实现结果不正确,可以:
这个问题的解法可以推广到更一般的情况:
如果分数计算规则改为连续X个1贡献Xᵏ分,我们可以类似地维护k个辅助数组,分别表示X、X²,...,Xᵏ⁻¹的期望值。
对于更复杂的计分规则(如分段函数),可能需要调整动态规划的状态定义和转移方程。
如前所述,我们可以只保存前一个状态的值,将空间复杂度优化到O(1):
c复制double a, b = 0, c = 0, d = 0;
for(int i=1; i<=m; i++) {
scanf("%lf",&a);
double new_b = (b + 1) * a;
double new_c = (c + 2*b + 1) * a;
d = d + (3*c + 3*b + 1) * a;
b = new_b;
c = new_c;
}
printf("%.1lf",d);
这种动态规划方法在概率和期望计算中非常常见,类似的题目包括:
理解这个问题的解法有助于解决一大类期望计算和动态规划问题。关键在于如何定义合适的状态变量,并建立它们之间的递推关系。