首页 > 试题广场 >

小红的点赞

[编程题]小红的点赞
  • 热度指数:285 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 256M,其他语言512M
  • 算法知识视频讲解
小红在整理自己小红书上发布的笔记时,会发现,每过一段时间,都会随机有一个笔记点赞数量加 1(每个笔记被点赞的概率是相同的)。
现在小红想知道,当第一次出现所有笔记点赞数量均为偶数时,所有笔记的总赞数之和的期望是多少?

输入描述:
第一行输入一个正整数n,代表小红发布的笔记数量。
第二行输入n个非负整数a_i,代表当且每个笔记的点赞数量。
1\leq n \leq 10^5
0\leq a_i \leq 10^9


输出描述:
一个整数,代表最终总赞数的期望对10^9+7取模的值。可以证明,最终的答案一定是个有理数,你只需要输出其对10^9+7取模的结果。
分数取模的定义:假设答案是x/y,那么其对p取模的答案是找到一个整数a满足a∈[0,p-1]a*yp取模等于x
示例1

输入

2
1 2

输出

6

说明

1/2的概率总赞数为 4,1/4概率总赞数为 6,1/8概率总赞数为 8……以此类推,最终的期望为1/2*4+1/4*6+……,这个无穷级数收敛于 6。

这道题是期望动态规划(Expectation DP)的经典应用,其精髓在于将一个复杂的问题层层简化,最终转化为一个可以编程解决的模式。

第一步:抓住问题的本质,简化状态

题目的目标是“所有笔记点赞数量均为偶数”。这说明:

  • 一个笔记具体的点赞数(是2还是100)并不重要,重要的是它的奇偶性 (Parity)
  • 点赞一个偶数笔记 -> 变为奇数。
  • 点赞一个奇数笔记 -> 变为偶数。

所以,我们可以将整个系统无限复杂的状态(每个笔记的具体赞数)压缩成一个非常简单的状态:当前有多少个笔记的点赞数是奇数

我们设 k 为系统中点赞数为奇数的笔记数量。

  • 初始状态:k = 输入中 a_i 为奇数的个数。
  • 目标状态:k = 0。

第二步:分析状态如何转移

假设当前系统处于状态 k(有 k 个奇数笔记,n-k 个偶数笔记)。下一次点赞会发生什么?

  1. 点中一个奇数笔记:该笔记变为偶数,系统中的奇数笔记总数从 k 变为 k-1。
    • 发生的概率为 k / n。
  2. 点中一个偶数笔记:该笔记变为奇数,系统中的奇数笔记总数从 k 变为 k+1。
    • 发生的概率为 (n - k) / n。

第三步:定义期望,建立递推关系

我们需要求的是“最终总赞数的期望”。可以分解为:
最终期望总赞数 = 初始总赞数 + 达到目标状态期望需要的额外点赞数

我们定义 E[k] 为:从当前有 k 个奇数笔记的状态,到第一次达到 k=0(全偶)状态,期望还需要多少次点赞。

根据全期望公式,我们可以写出 E[k] 的递推关系:
E[k] = 1 + (k/n) * E[k-1] + ((n-k)/n) * E[k+1]

  • 1: 代表无论如何,我们都要先点赞一次。这次点赞是确定发生的,贡献为1。
  • (k/n) * E[k-1]: 有 k/n 的概率,状态变为 k-1,之后还需要 E[k-1] 步。
  • ((n-k)/n) * E[k+1]: 有 (n-k)/n 的概率,状态变为 k+1,之后还需要 E[k+1] 步。

我们还有边界条件:

  • E[0] = 0 (已经到达目标,无需额外点赞)。
  • E[n] = 1 + E[n-1] (当所有 n 个笔记都为奇数时,下一次点赞必然会点中一个奇数笔记,使其变为偶数,状态百分百转移到 n-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)

现在,我们可以把整个求解过程变成一个清晰的算法:

  1. 初始化

    • 读入 n 和所有 a_i。
    • 计算 initial_sum (初始总赞数) 和 odd_count (初始奇数笔记数)。
  2. DP计算差值 d[k]

    • 确定DP起点: 我们需要一个已知的 d 值。从边界条件 E[n] = 1 + E[n-1] 可知,E[n] - E[n-1] = 1。所以,d[n] = 1。这是我们 DP 的锚点。
    • 执行DP: 从 k = n-1 循环到 1,使用公式 d[k] = ((n - k) * d[k+1] + n) / k 计算出所有的 d[k]。
  3. 重构期望值 E[k]

    • 我们已经有了所有差值 d[k]。E[k] 就是这些差值的累加和(前缀和)。
    • E[0] = 0
    • E[1] = E[0] + d[1] = d[1]
    • E[2] = E[1] + d[2] = d[1] + d[2]
    • E[k] = d[1] + d[2] + ... + d[k]
    • 我们可以用一个循环,计算出所有 E[k] 的值并存储起来。
  4. 得出最终答案

    • 根据初始的 odd_count,从我们计算好的 E 数组中查出期望的额外点赞数 E[odd_count]。
    • 最终答案 = (initial_sum + E[odd_count]) % MOD。

第六步:处理编程细节(模运算)

因为答案要对 10^9 + 7 取模,所有计算都要在模意义下进行:

  • 加法、减法、乘法都直接取模。
  • 除法是关键:除以 k 等价于乘以 k 在模 10^9 + 7 下的逆元。根据费马小定理,k 的逆元是 k^(MOD-2) % MOD。我们需要一个快速幂函数来计算它。
#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;
}


编辑于 2025-08-22 17:49:22 回复(2)
too hard,只能先求都是偶数的情况下混点分,完全求出的话要概率统计非常好才想的出来吧。我感觉我数学学院的舍友也没思路。
发表于 2025-03-28 14:54:52 回复(2)

小红的点赞题解

显然,这道题具体的数是多少并不重要

我们只关心奇数与偶数的个数

为奇数个数为时的还原为偶数的期望
我们有

对于


此处不依赖数学直觉,将其转换为
使得而是选择高斯消元

对于每一个表达式

$$
可列出行列式

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;
}
发表于 2025-09-18 00:52:50 回复(0)
真一点不会, 跪了
发表于 2025-03-27 15:09:15 回复(0)