白树台学园 (easy version) - 题解
标签与难度
标签: 期望, 概率, 数学, 线性DP, 前缀和优化, 逆元 难度: 2100
题目大意喵~
哈喵~ 各位看官,今天我们来解决一道关于期望的有趣问题,喵!
事情是这样的:我们有一个长度为 的队伍,每个位置上的人都有一个擅长的方面(可以看作是颜色),总共有 种不同的方面。有些位置上的人选是固定的,他们的颜色 是一个 到 之间的确定值。但其他位置的人选还没定,用 表示,这些位置的颜色会从 到 中等概率随机选择一个。
红方会随机攻击我们队伍的一个连续区间 。一个区间的“牢固度”被定义为这个区间内不同颜色的数量。
我们的任务是,计算在所有可能的随机颜色选择和所有可能的随机区间选择下,区间牢固度的期望值是多少。结果需要对 取模,呐。
举个栗子:如果队伍是 [1, 0, 2],。位置 的颜色可能是 或 (概率各 )。
- 如果是
[1, 1, 2],随机区间的牢固度期望是... - 如果是
[1, 2, 2],随机区间的牢固度期望是... 我们要计算的是这两个情况的平均值,喵~
解题思路分析
这道题看起来有点复杂,因为它混合了两种随机性:颜色的随机选择和区间的随机选择。不过别怕,我带你一步步拆解它,的说!
核心思想:期望的线性性质
首先,我们要计算的是期望值。对付期望问题,有一个超级好用的法宝,那就是期望的线性性质!
一个区间的牢固度(不同颜色数)可以看作是“颜色1是否出现”+“颜色2是否出现”+...+“颜色m是否出现”的总和。其中,如果颜色 出现,值为1,否则为0。
根据期望的线性性质,一个区间的期望牢固度就是:
我们的目标是求在所有 个区间上的期望牢固度的平均值。设 为我们要求的最终答案。
把上面的公式代入,并交换求和顺序,我们得到:
这个式子告诉我们,可以对每一种颜色 单独计算它对总期望的贡献,最后加起来!是不是清晰多啦?
转换思路:正难则反
直接计算“出现”的概率有点麻烦,因为它可能在多个位置出现。我们不如换个角度,计算它“不出现”的概率,然后用 去减。这可是我的得意技之一哦,喵~
代入上面的总和式:
所以,最终的期望值 就是:
现在,问题转化为了计算所有颜色在所有区间内不出现的概率之和。
计算“不出现”的概率
对于一个固定的颜色 和一个区间 :
- 如果区间内某个位置 的颜色已经确定为 (即 ),那么 必然出现,不出现的概率为 。
- 如果区间内没有确定为 的位置,那么对于每个 且 的位置,它肯定不是 。对于每个 的位置,它不是 的概率是 。
所以,如果一个区间 内没有固定的颜色 ,并且有 个 ,那么 在这个区间不出现的概率就是 。
分段计算与前缀和优化
对于每种颜色 ,那些 的位置就像一道道屏障,把整个序列 1...n 分割成了好几个独立的段。一个区间 如果想要 不出现,它必须完整地落在一个由这些屏障分割出的段里。
例如,如果 ,颜色 出现在位置 和 ,那么有效的段就是 , , 。我们需要对这些段分别计算贡献。
设一个段为 [start, end]。我们要计算:
其中 是区间 中 的数量。
直接暴力计算这个和是 的,对于所有颜色和所有段,总复杂度会是 左右,太慢了喵!必须优化!
我们可以用前缀和来加速这个计算。设 。 令 如果 ,否则 。 那么 。
令 为 的前缀积。那么 。 我们要计算的和变成了:
这个形式依然需要 。但我们可以进一步优化! 我们可以在 的时间内预处理出以下三个前缀和数组:
prod_g[i] = g_i(前缀积)sum_inv_g[i] = \sum_{k=0}^{i} (g_k)^{-1}(前缀积的逆元的前缀和)sum_g_times_sum_inv_g[i] = \sum_{k=1}^{i} g_k \cdot \text{sum\_inv\_g}[k-1]
有了这些,对于任何一个段 [start, end],我们想求的 就可以通过差分在 内算出啦!
第一个部分是 sum_g_times_sum_inv_g 的区间和,第二个部分是 prod_g 的区间和(需要另一个前缀和数组)乘以一个常数。这样,每个段的计算就是 的了!
最终算法流程
预处理:
- 计算模逆元,特别是 和 。
- 遍历数组 ,计算出我们需要的几个前缀和数组。总时间 。
- 记录下每种颜色 出现的所有位置。时间 。
计算总和:
- 初始化总的不出现概率和
total_complement_prob_sum = 0。 - 对每种颜色 从 到 :
- 取出 的所有固定位置,在列表前后加上 和 作为哨兵。
- 遍历这些位置,得到一个个不包含固定 的段
[start, end]。 - 利用预处理好的前缀和数组,在 时间内计算出这个段的贡献,累加到
total_complement_prob_sum。
- 初始化总的不出现概率和
得出答案:
- 总的不出现概率和已经得到。
- 根据公式 计算最终答案。
总时间复杂度是 ,空间复杂度是 。这下就可以轻松跑过啦,喵~
代码实现
这是我根据上面的思路,精心重构的一份代码,注释超详细的哦!
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;
// 定义一个好用的长整型别名
using ll = long long;
// 模数
const int MOD = 998244353;
// 快速幂函数,用来计算 a^b % MOD
ll power(ll base, ll exp) {
ll res = 1;
base %= MOD;
while (exp > 0) {
if (exp % 2 == 1) res = (res * base) % MOD;
base = (base * base) % MOD;
exp /= 2;
}
return res;
}
// 模块逆元函数,a^{-1} mod m = a^{m-2} mod m
ll modInverse(ll n) {
return power(n, MOD - 2);
}
// 封装一个加法,防止溢出
void add(ll &a, ll b) {
a += b;
if (a >= MOD) a -= MOD;
}
// 封装一个减法,防止变负数
void sub(ll &a, ll b) {
a -= b;
if (a < 0) a += MOD;
}
int main() {
// 加速输入输出,喵~
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
vector<int> a(n + 1);
vector<vector<int>> color_positions(m + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
if (a[i] != 0) {
color_positions[a[i]].push_back(i);
}
}
// P 是随机位置的颜色不是某个特定颜色的概率
ll P = (m - 1) * modInverse(m) % MOD;
// --- 开始预处理四个关键的前缀和数组 ---
// g[i] = prefix product of p_k, where p_k=P if a[k]=0, else 1
vector<ll> g(n + 1, 1);
// sum_g[i] = prefix sum of g_k
vector<ll> sum_g(n + 1, 0);
// inv_g[i] = g[i]的逆元
vector<ll> inv_g(n + 1, 1);
// sum_inv_g[i] = prefix sum of inv_g[k]
vector<ll> sum_inv_g(n + 2, 0); // size n+2 for easier indexing sum_inv_g[l-2]
// sum_g_x_sum_inv_g[i] = prefix sum of g[k] * sum_inv_g[k-1]
vector<ll> sum_g_x_sum_inv_g(n + 1, 0);
for (int i = 1; i <= n; ++i) {
g[i] = g[i - 1];
if (a[i] == 0) {
g[i] = (g[i] * P) % MOD;
}
sum_g[i] = (sum_g[i - 1] + g[i]) % MOD;
inv_g[i] = modInverse(g[i]);
sum_inv_g[i+1] = (sum_inv_g[i] + inv_g[i]) % MOD; // sum_inv_g[i] stores sum up to i-1
}
sum_inv_g[0] = 1; // g[0] = 1, inv_g[0] = 1
for (int i = 1; i <= n; ++i) {
ll term = (g[i] * sum_inv_g[i]) % MOD; // g[i] * sum(inv_g[k] for k=0..i-1)
sum_g_x_sum_inv_g[i] = (sum_g_x_sum_inv_g[i-1] + term) % MOD;
}
ll total_complement_sum = 0;
// 对每种颜色计算贡献
for (int c = 1; c <= m; ++c) {
int last_pos = 0;
// 把 N+1 也作为一个屏障
color_positions[c].push_back(n + 1);
for (int pos : color_positions[c]) {
int start = last_pos + 1;
int end = pos - 1;
if (start <= end) {
// 利用预处理的前缀和 O(1) 计算段贡献
// sum_{r=start to end} g[r] * (sum_inv_g[r-1] - sum_inv_g[start-2])
ll term1 = sum_g_x_sum_inv_g[end];
sub(term1, sum_g_x_sum_inv_g[start - 1]);
ll term2_factor = sum_inv_g[start - 1 + 1]; // sum up to start-2
ll term2_sum = sum_g[end];
sub(term2_sum, sum_g[start - 1]);
ll term2 = (term2_factor * term2_sum) % MOD;
ll segment_sum = term1;
sub(segment_sum, term2);
add(total_complement_sum, segment_sum);
}
last_pos = pos;
}
}
// 总区间数
ll total_intervals = (ll)n * (n + 1) / 2 % MOD;
ll inv_total_intervals = modInverse(total_intervals);
// 计算补集的期望贡献
ll complement_expectation = (total_complement_sum * inv_total_intervals) % MOD;
// 最终答案 E = m - E_complement
ll final_ans = m;
sub(final_ans, complement_expectation);
cout << final_ans << endl;
return 0;
}复杂度分析
时间复杂度:
- 预处理所有前缀和数组需要遍历一次序列,复杂度为 。
- 记录每种颜色的位置需要遍历一次序列,复杂度为 。
- 对每种颜色 ,我们遍历其固定的位置。由于每个位置最多属于一种颜色,所有颜色固定位置的总数最多为 。所以遍历所有段的总时间是 。
- 因此,总的时间复杂度是 ,非常高效!
空间复杂度:
- 我们需要几个长度为 的前缀和数组,空间为 。
color_positions向量存储所有固定颜色的位置,总大小为 。如果 很大且固定位置很少,也需要 的空间来初始化这个向量。所以总空间是 。
知识点总结
这道题是一道非常棒的期望题,它教会了我们几件重要的事情,喵~
- 期望的线性性质: 这是解决复杂期望问题的最有力武器!将整体期望拆解为各个独立事件的概率之和,能极大地简化问题。
- 正难则反 (补集思想): 当直接计算“至少一个”很困难时,尝试计算“一个都没有”的概率,然后用总概率 去减,往往能柳暗花明。
- 前缀和优化: 对于需要反复计算区间和的问题,预处理前缀和(或前缀积、更复杂的前缀信息)是王道!本题中,通过巧妙地设计和预处理多个前缀和数组,我们将 的区间计算降至 ,是解题的关键。
- 分段处理: 当序列中某些特殊点(本题中的固定颜色位置)会影响计算时,可以将序列按这些点分段,在段内进行计算,化整为零。
希望这篇题解能帮助你更好地理解这道题目!如果还有问题,随时可以再来问我哦,喵~