233的字符串 - 题解
标签与难度
标签: 组合数学, 数学, 费马小定理, 快速幂, 模块化算术, 求和公式, 动态规划 难度: 1600
题目大意喵~
主人你好呀,喵~ ฅ(●'ω'●)ฅ
这道题是说,我们有一个正整数 ,然后我们要用它来生成一个字符串。方法就是把 "abc" 这个小片段重复 次。比如说,如果 ,我们得到的字符串就是 "abcabcabc"。
我们的任务,就是在这个长长的字符串里,找出所有 "acb" 子序列的数量。
什么是子序列呢?就是从原来的字符串里,按顺序挑出几个字符组成的新序列。比如说,在 "abcdefg" 里,"bdf" 就是一个子序列哦!
所以,我们要找的就是,从字符串里先找一个 'a',在它后面再找一个 'c',最后在这个 'c' 后面再找一个 'b'。把所有可能的组合都数出来,就是答案啦!
因为答案可能会非常大,所以要记得对 取模哦,喵~
解题思路分析
这道题看起来是在字符串里数数,但如果真的去生成那个长长的字符串,当 很大的时候,肯定会超时和超内存的,对吧?所以我们得找一个更聪明的数学方法,喵!
这种数子序列个数的问题,有一个经典的小技巧哦!对于目标子序列 "acb",我们可以遍历字符串,固定中间那个字符 'c' 的位置,然后数一数它前面有多少个 'a',后面有多少个 'b'。
让我们来分析一下字符串的结构吧! 这个字符串是 abcabc...abc,一共重复了 次。我们可以把它看成是 个 "abc" 块。 为了方便,我们给这些块编号,从第 0 块到第 块。
- 'a' 出现在第 块的开头。
- 'b' 出现在第 块的中间。
- 'c' 出现在第 块的末尾。
现在,我们来固定 'c' 的位置。假设我们选择了第 个块( 从 到 )里的那个 'c'。
寻找 'a': 我们需要在这个 'c' 的前面找一个 'a'。这个 'c' 在第 块,所以它前面的所有 'a' 都在第 这些块里。每个块里有一个 'a',所以总共有 个 'a' 可以选择,喵~
寻找 'b': 我们需要在这个 'c' 的后面找一个 'b'。这个 'c' 在第 块,所以它后面的所有 'b' 都在第 这些块里。一共有 个块,所以就有 个 'b' 可以选择。
根据乘法原理,如果我们固定了第 块的 'c',那么能构成的 "acb" 子序列就有 个。
为了得到总数,我们只需要把所有可能的 'c' 的情况加起来就好啦!'c' 可以取自第 0 块,第 1 块,...,一直到第 块。所以,总数就是:
现在,我们来化简这个求和公式,准备好小爪子来推导吧! 为了方便计算,我们做一个变量替换,令 。当 从 遍历到 时, 就从 遍历到 。 原式中的 就变成了 。 所以求和式就变成了:
利用求和的性质,我们可以把它拆开:
这里用到了两个非常著名的求和公式,主人可要记住哦!
- 自然数求和公式:
- 平方和公式:
把 代入我们的式子:
通分一下,把分母都变成 6:
提取公因式 :
哇!公式变得好简洁呀,喵!就是 。这其实就是组合数 哦!
最后一步:处理模运算
我们的最终公式是 。在程序里计算时,要进行模运算。除以 6 怎么办呢? 我们不能直接 / 6,因为这会丢失精度。在模算术里,除以一个数等于乘以它的模逆元。 因为模数 是一个质数,我们可以用费马小定理来求 6 的逆元。 费马小定理说,如果 是质数,那么对于任意整数 ,有 。如果 不是 的倍数,则有 。 从 可以看出, 的模 逆元就是 。 所以,6 的逆元就是 。我们可以用快速幂算法来高效地计算它。
于是,最终的计算步骤是:
- 计算 。
- 计算 的模逆元,即
power(6, MOD - 2)。 - 将这四项相乘,每一步都取模,就得到最终答案啦!
代码实现
这是我根据上面的思路,精心为你准备的代码哦,喵~
#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;
}复杂度分析
时间复杂度: 我们的计算主要耗时在快速幂求模逆元上。快速幂算法的时间复杂度是对指数取对数,所以是 。其他的乘法和取模都是常数时间操作。所以总的时间复杂度就是 ,非常快哦!
空间复杂度: 我们只用了几个变量来存
n、模数和中间计算结果,没有使用额外的、随n增长的存储空间,所以空间复杂度是常数级别的,喵~
知识点总结
这道题虽然简单,但涉及到的知识点可是非常核心的呢!
- 组合计数思想: 解决计数问题时,"固定一部分,计算另一部分" 是一个非常强大的思想。在这里我们固定了子序列的中间元素 'c',大大简化了问题。
- 求和公式: 熟练掌握常见的求和公式(如自然数和、平方和)是解决许多数学问题的利器。
- 模块化算术: 在处理可能溢出的大数问题时,全程使用模运算是基本操作。要记住
(a+b)%m = ((a%m)+(b%m))%m和(a*b)%m = ((a%m)*(b%m))%m。 - 模逆元: 在模算术中实现除法,就需要用到模逆元。当模数是质数时,费马小定理是求逆元的最佳伙伴。
- 快速幂: 它是计算 的标准算法,也是求模逆元的基础。每个会算法的我都应该掌握它!
希望这篇题解能帮到你,喵~ 如果还有不懂的地方,随时可以再来问我哦!( ´ ▽ ` )ノ