Skip to content

233的字符串 - 题解

标签与难度

标签: 组合数学, 数学, 费马小定理, 快速幂, 模块化算术, 求和公式, 动态规划 难度: 1600

题目大意喵~

主人你好呀,喵~ ฅ(●'ω'●)ฅ

这道题是说,我们有一个正整数 nn,然后我们要用它来生成一个字符串。方法就是把 "abc" 这个小片段重复 nn 次。比如说,如果 n=3n=3,我们得到的字符串就是 "abcabcabc"。

我们的任务,就是在这个长长的字符串里,找出所有 "acb" 子序列的数量。

什么是子序列呢?就是从原来的字符串里,按顺序挑出几个字符组成的新序列。比如说,在 "abcdefg" 里,"bdf" 就是一个子序列哦!

所以,我们要找的就是,从字符串里先找一个 'a',在它后面再找一个 'c',最后在这个 'c' 后面再找一个 'b'。把所有可能的组合都数出来,就是答案啦!

因为答案可能会非常大,所以要记得对 109+710^9 + 7 取模哦,喵~

解题思路分析

这道题看起来是在字符串里数数,但如果真的去生成那个长长的字符串,当 nn 很大的时候,肯定会超时和超内存的,对吧?所以我们得找一个更聪明的数学方法,喵!

这种数子序列个数的问题,有一个经典的小技巧哦!对于目标子序列 "acb",我们可以遍历字符串,固定中间那个字符 'c' 的位置,然后数一数它前面有多少个 'a',后面有多少个 'b'。

让我们来分析一下字符串的结构吧! 这个字符串是 abcabc...abc,一共重复了 nn 次。我们可以把它看成是 nn 个 "abc" 块。 为了方便,我们给这些块编号,从第 0 块到第 n1n-1 块。

  • 'a' 出现在第 ii 块的开头。
  • 'b' 出现在第 ii 块的中间。
  • 'c' 出现在第 ii 块的末尾。

现在,我们来固定 'c' 的位置。假设我们选择了第 ii 个块(ii00n1n-1)里的那个 'c'。

  1. 寻找 'a': 我们需要在这个 'c' 的前面找一个 'a'。这个 'c' 在第 ii 块,所以它前面的所有 'a' 都在第 0,1,,i0, 1, \dots, i 这些块里。每个块里有一个 'a',所以总共有 i+1i+1 个 'a' 可以选择,喵~

  2. 寻找 'b': 我们需要在这个 'c' 的后面找一个 'b'。这个 'c' 在第 ii 块,所以它后面的所有 'b' 都在第 i+1,i+2,,n1i+1, i+2, \dots, n-1 这些块里。一共有 (n1)(i+1)+1=n1i(n-1) - (i+1) + 1 = n-1-i 个块,所以就有 n1in-1-i 个 'b' 可以选择。

根据乘法原理,如果我们固定了第 ii 块的 'c',那么能构成的 "acb" 子序列就有 (i+1)×(n1i)(i+1) \times (n-1-i) 个。

为了得到总数,我们只需要把所有可能的 'c' 的情况加起来就好啦!'c' 可以取自第 0 块,第 1 块,...,一直到第 n1n-1 块。所以,总数就是:

Total=i=0n1(i+1)(n1i)\text{Total} = \sum_{i=0}^{n-1} (i+1)(n-1-i)

现在,我们来化简这个求和公式,准备好小爪子来推导吧! 为了方便计算,我们做一个变量替换,令 k=i+1k = i+1。当 ii00 遍历到 n1n-1 时,kk 就从 11 遍历到 nn。 原式中的 (n1i)(n-1-i) 就变成了 (n1(k1))=nk(n-1-(k-1)) = n-k。 所以求和式就变成了:

Total=k=1nk(nk)=k=1n(nkk2)\text{Total} = \sum_{k=1}^{n} k(n-k) = \sum_{k=1}^{n} (nk - k^2)

利用求和的性质,我们可以把它拆开:

Total=k=1nnkk=1nk2=nk=1nkk=1nk2\text{Total} = \sum_{k=1}^{n} nk - \sum_{k=1}^{n} k^2 = n \sum_{k=1}^{n} k - \sum_{k=1}^{n} k^2

这里用到了两个非常著名的求和公式,主人可要记住哦!

  • 自然数求和公式: k=1Nk=N(N+1)2\sum_{k=1}^{N} k = \frac{N(N+1)}{2}
  • 平方和公式: k=1Nk2=N(N+1)(2N+1)6\sum_{k=1}^{N} k^2 = \frac{N(N+1)(2N+1)}{6}

N=nN=n 代入我们的式子:

Total=n(n(n+1)2)(n(n+1)(2n+1)6)\text{Total} = n \left( \frac{n(n+1)}{2} \right) - \left( \frac{n(n+1)(2n+1)}{6} \right)

通分一下,把分母都变成 6:

Total=3n2(n+1)6n(n+1)(2n+1)6\text{Total} = \frac{3n^2(n+1)}{6} - \frac{n(n+1)(2n+1)}{6}

提取公因式 n(n+1)6\frac{n(n+1)}{6}

Total=n(n+1)6[3n(2n+1)]\text{Total} = \frac{n(n+1)}{6} [3n - (2n+1)]

Total=n(n+1)6[3n2n1]\text{Total} = \frac{n(n+1)}{6} [3n - 2n - 1]

Total=n(n+1)(n1)6\text{Total} = \frac{n(n+1)(n-1)}{6}

哇!公式变得好简洁呀,喵!就是 (n1)n(n+1)6\frac{(n-1)n(n+1)}{6}。这其实就是组合数 C(n+1,3)C(n+1, 3) 哦!

最后一步:处理模运算

我们的最终公式是 (n1)n(n+1)6\frac{(n-1)n(n+1)}{6}。在程序里计算时,要进行模运算。除以 6 怎么办呢? 我们不能直接 / 6,因为这会丢失精度。在模算术里,除以一个数等于乘以它的模逆元。 因为模数 109+710^9+7 是一个质数,我们可以用费马小定理来求 6 的逆元。 费马小定理说,如果 pp 是质数,那么对于任意整数 aa,有 apa(modp)a^p \equiv a \pmod{p}。如果 aa 不是 pp 的倍数,则有 ap11(modp)a^{p-1} \equiv 1 \pmod{p}。 从 aap21(modp)a \cdot a^{p-2} \equiv 1 \pmod{p} 可以看出,aa 的模 pp 逆元就是 ap2(modp)a^{p-2} \pmod{p}。 所以,6 的逆元就是 6109+72(mod109+7)6^{10^9+7-2} \pmod{10^9+7}。我们可以用快速幂算法来高效地计算它。

于是,最终的计算步骤是:

  1. 计算 n1,n,n+1n-1, n, n+1
  2. 计算 66 的模逆元,即 power(6, MOD - 2)
  3. 将这四项相乘,每一步都取模,就得到最终答案啦!

代码实现

这是我根据上面的思路,精心为你准备的代码哦,喵~

cpp
#include <iostream>

// 使用 long long 来防止计算 n*(n-1)*(n+1) 时溢出
using ll = long long;

// 题目要求的模数
const int MOD = 1e9 + 7;

// 快速幂函数,用于计算 (base^exp) % mod
// 这是计算模逆元的核心工具喵~
ll power(ll base, ll exp) {
    ll res = 1;
    base %= MOD;
    while (exp > 0) {
        // 如果 exp 是奇数,需要把当前的 base 乘到结果里
        if (exp % 2 == 1) {
            res = (res * base) % MOD;
        }
        // base 自乘,exp 折半
        base = (base * base) % MOD;
        exp /= 2;
    }
    return res;
}

// 计算模逆元的函数
// 根据费马小定理,a 的模 p 逆元是 a^(p-2)
ll modInverse(ll n) {
    return power(n, MOD - 2);
}

int main() {
    // 为了更快的输入输出,喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    ll n;
    std::cin >> n;

    // 根据我们的推导,公式是 (n * (n-1) * (n+1)) / 6
    // 我们需要计算 n, n-1, n+1 对 MOD 取模后的值

    // (n-1) % MOD,使用 (n - 1 + MOD) % MOD 来防止 n=1 时出现负数
    ll term1 = (n - 1 + MOD) % MOD;
    ll term2 = n % MOD;
    ll term3 = (n + 1) % MOD;

    // 计算 6 的模逆元
    ll inv6 = modInverse(6);

    // 把所有项乘起来,每一步都取模
    ll ans = 1;
    ans = (ans * term1) % MOD;
    ans = (ans * term2) % MOD;
    ans = (ans * term3) % MOD;
    ans = (ans * inv6) % MOD;

    std::cout << ans << std::endl;

    return 0;
}

复杂度分析

  • 时间复杂度: O(log(MOD))O(\log(\text{MOD})) 我们的计算主要耗时在快速幂求模逆元上。快速幂算法的时间复杂度是对指数取对数,所以是 O(log(MOD))O(\log(\text{MOD}))。其他的乘法和取模都是常数时间操作。所以总的时间复杂度就是 O(log(MOD))O(\log(\text{MOD})),非常快哦!

  • 空间复杂度: O(1)O(1) 我们只用了几个变量来存 n、模数和中间计算结果,没有使用额外的、随 n 增长的存储空间,所以空间复杂度是常数级别的,喵~

知识点总结

这道题虽然简单,但涉及到的知识点可是非常核心的呢!

  1. 组合计数思想: 解决计数问题时,"固定一部分,计算另一部分" 是一个非常强大的思想。在这里我们固定了子序列的中间元素 'c',大大简化了问题。
  2. 求和公式: 熟练掌握常见的求和公式(如自然数和、平方和)是解决许多数学问题的利器。
  3. 模块化算术: 在处理可能溢出的大数问题时,全程使用模运算是基本操作。要记住 (a+b)%m = ((a%m)+(b%m))%m(a*b)%m = ((a%m)*(b%m))%m
  4. 模逆元: 在模算术中实现除法,就需要用到模逆元。当模数是质数时,费马小定理是求逆元的最佳伙伴。
  5. 快速幂: 它是计算 ab(modp)a^b \pmod p 的标准算法,也是求模逆元的基础。每个会算法的我都应该掌握它!

希望这篇题解能帮到你,喵~ 如果还有不懂的地方,随时可以再来问我哦!( ´ ▽ ` )ノ