第一行输入一个正整数,代表小红发布的笔记数量。
第二行输入个非负整数
,代表当且每个笔记的点赞数量。
一个整数,代表最终总赞数的期望对取模的值。可以证明,最终的答案一定是个有理数,你只需要输出其对
取模的结果。
分数取模的定义:假设答案是,那么其对
取模的答案是找到一个整数
满足
且
对
取模等于
。
2 1 2
6
有的概率总赞数为 4,
概率总赞数为 6,
概率总赞数为 8……以此类推,最终的期望为
,这个无穷级数收敛于 6。
这道题是期望动态规划(Expectation DP)的经典应用,其精髓在于将一个复杂的问题层层简化,最终转化为一个可以编程解决的模式。
题目的目标是“所有笔记点赞数量均为偶数”。这说明:
所以,我们可以将整个系统无限复杂的状态(每个笔记的具体赞数)压缩成一个非常简单的状态:当前有多少个笔记的点赞数是奇数。
我们设 k 为系统中点赞数为奇数的笔记数量。
假设当前系统处于状态 k(有 k 个奇数笔记,n-k 个偶数笔记)。下一次点赞会发生什么?
我们需要求的是“最终总赞数的期望”。可以分解为:
最终期望总赞数 = 初始总赞数 + 达到目标状态期望需要的额外点赞数
我们定义 E[k] 为:从当前有 k 个奇数笔记的状态,到第一次达到 k=0(全偶)状态,期望还需要多少次点赞。
根据全期望公式,我们可以写出 E[k] 的递推关系:
E[k] = 1 + (k/n) * E[k-1] + ((n-k)/n) * E[k+1]
我们还有边界条件:
我们现在有了一个关于 E[k]、E[k-1]、E[k+1] 的方程组。直接求解非常困难,因为每个 E[k] 都依赖于它两边的值。
这里是第二个“Aha!”时刻,也是解题的突破口:我们不直接求 E[k],而是去研究相邻状态期望的差值。
我们定义 d[k] = E[k] - E[k-1],表示从 k-1 状态到 k 状态,期望步数增加了多少。
通过一系列代数变形(将 E[k-1] = E[k] - d[k] 和 E[k+1] = E[k] + d[k+1] 代入原始方程),可以得出一个关于 d[k] 的、更简单的递推关系:
d[k] = ( (n - k) * d[k+1] + n ) / k
这个新关系的美妙之处在于,d[k] 只依赖于 d[k+1],这是一个单向的依赖关系!我们可以从 k 大的一端向小的一端进行计算,这是一个完美的动态规划(DP)结构。
现在,我们可以把整个求解过程变成一个清晰的算法:
初始化:
DP计算差值 d[k]:
重构期望值 E[k]:
得出最终答案:
因为答案要对 10^9 + 7 取模,所有计算都要在模意义下进行:
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;
long long power(long long base, long long exp) {
long long res = 1;
long long mod = 1e9 + 7;
base %= mod;
while (exp > 0) {
if (exp % 2 == 1) res = (res * base) % mod;
base = (base * base) % mod;
exp /= 2;
}
return res;
}
long long modInverse(long long n) {
long long mod = 1e9 + 7;
return power(n, mod - 2);
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
long long initial_sum = 0;
int odd_count = 0;
for (int i = 0; i < n; ++i) {
long long a;
cin >> a;
initial_sum += a;
if (a % 2 != 0) {
odd_count++;
}
}
if (odd_count == 0) {
cout << initial_sum << endl;
return 0;
}
long long mod = 1e9 + 7;
// d[k] = E[k] - E[k-1], 期望的差值
vector<long long> d(n + 1, 0);
d[n] = 1; // E[n] - E[n-1] = 1
// 从后向前计算所有的 d[k]
// d[k] = ((n-k)*d[k+1] + n) / k
for (int k = n - 1; k >= 1; --k) {
long long inv_k = modInverse(k);
long long term1 = ((long long)(n - k) * d[k + 1]) % mod;
long long numerator = (term1 + n) % mod;
d[k] = (numerator * inv_k) % mod;
}
// E[k] 是 d 数组的前缀和
// E_dp[k] 存储 E[k] 的值
vector<long long> E_dp(n + 1, 0);
E_dp[0] = 0;
for (int k = 1; k <= n; ++k) {
E_dp[k] = (E_dp[k - 1] + d[k]) % mod;
}
long long expected_additional_likes = E_dp[odd_count];
// 初始总和可能很大,也要取模
long long final_expected_sum = (initial_sum % mod + expected_additional_likes) % mod;
cout << final_expected_sum << endl;
return 0;
} 显然,这道题具体的数是多少并不重要
我们只关心奇数与偶数的个数
设为奇数个数为
时的还原为偶数的期望
我们有
对于
或
有与
此处不依赖数学直觉,将其转换为
使得而是选择高斯消元
对于每一个表达式
有
$$
可列出行列式
| 1 | 0 | 0 | ... | ... | ... | ... | 0 |
|---|---|---|---|---|---|---|---|
| 1 | ... | ... | ... | ... | 1 | ||
| 0 | 1 | ... | ... | ... | 1 | ||
| ... | ... | ... | ... | ... | ... | ... | 1 |
| ... | ... | ... | ... | 1 | 1 | ||
| ... | ... | ... | ... | ... | -1 | 1 | 1 |
显然,只需要解出该行列式对应的方程组就可求出对应的
但高斯消元的时间复杂度为不可能通过
的数据
注意到该行列式及其特殊,每行最多只有三个值
此处使用TDMA(Thomas Algorithm)托马斯算法进行计算
本质其实是高斯消元剪枝,先只使用上一行的进行消元,变成上三角矩阵
再只使用下一行数据获取答案,具体做法请自行搜索
def thomas_algorithm(n, a, b, c, d):
"""
求解方程组:
b[0] * x[0] + c[0] * x[1] = d[0]
a[i] * x[i-1] + b[i] * x[i] + c[i] * x[i+1] = d[i], for i=1 to n-2
a[n-1] * x[n-2] + b[n-1] * x[n-1] = d[n-1]
"""
# 前向消元
for i in range(1, n):
w = a[i] / b[i-1]
b[i] = b[i] - w * c[i-1]
d[i] = d[i] - w * d[i-1]
# 回代
x = [0] * n
x[n-1] = d[n-1] / b[n-1]
for i in range(n-2, -1, -1):
x[i] = (d[i] - c[i] * x[i+1]) / b[i]
return x 如此便解决该题,附通过代码
#include <bits/stdc++.h>
using namespace std;
constexpr int mod = 1e9 + 7;
int pow(int a, int b, const int &m)
{
long long res = 1, po = a;
for (; b; b >>= 1)
{
if (b & 1)
res = res * po % m;
po = po * po % m;
}
return res;
}
// Returns the modular inverse of a prime modulo p.
int64_t inverse(int a, int p = mod)
{
return pow(a, p - 2, p);
}
int main()
{
int n;
cin >> n;
int64_t sum = 0;
int cnt = 0;
for (int i = 0; i < n; i++)
{
int x;
cin >> x;
sum += x;
cnt += x & 1;
}
vector<int64_t> c(n + 1, 1);
c[0] = 0;
vector<array<int64_t, 3>> a(n + 1);
a[0] = {0, 1, 0};
int64_t inv_n = inverse(n);
for (int i = 1; i < n; i++)
{
a[i][0] = (mod - i) * inv_n % mod;
a[i][1] = 1;
a[i][2] = (mod - (n - i)) * inv_n % mod;
}
a[n][0] = mod - 1;
a[n][1] = 1;
a[n][2] = 0;
for (int i = 1; i <= n; i++)
{
int t = a[i][0] * inverse(a[i - 1][1]) % mod;
for (int j = 1; j < 3; j++)
{
a[i][j - 1] -= t * a[i - 1][j] % mod;
a[i][j - 1] = (a[i][j - 1] + mod) % mod;
}
c[i] -= (c[i - 1] * t) % mod;
c[i] = (c[i] + mod) % mod;
}
for (int i = n - 1; i >= 0; i--)
{
int t = a[i][2] * inverse(a[i + 1][1]) % mod;
for (int j = 1; j <= 2; j++)
{
a[i][j] -= t * a[i + 1][j - 1] % mod;
a[i][j] = (a[i][j] + mod) % mod;
}
c[i] -= (c[i + 1] * t) % mod;
c[i] = (c[i] + mod) % mod;
}
cout << (sum + c[cnt] * inverse(a[cnt][1]) % mod) % mod;
return 0;
}