主人你好喵~ 这里是小猫娘的题解教室啦!
今天我们要看一道非常有趣的题目哦,它看起来像组合数,但又因为一个小小笔误,变得完全不一样了呢!让我们一起来看看这个可爱的错误会带来什么神奇的结果吧,喵~
B. Binomial Coefficients, Kind Of
题目大意
这道题是说,有一个叫做 akshiM 的同学,他想写一个程序来计算组合数 C(n, k)。
我们都知道,正确的组合数递推公式是 C(n, k) = C(n-1, k) + C(n-1, k-1),对吧?
但是呢,akshiM 同学迷迷糊糊地把公式写错了,写成了:
C(n, k) = C(n, k-1) + C(n-1, k-1)
他还定义了两个边界条件:C(n, 0) = 1 和 C(n, n) = 1。
现在,我们的任务就是,根据这个 错误 的公式,来计算出一系列 (ni, ki) 对应的 C(ni, ki) 的值。因为结果可能会很大,所以我们需要对 10^9 + 7 取模哦。
输入格式有点小调皮,它会先给你一个 t,表示有 t 组查询。然后,第二行是所有的 n,第三行是所有的 k。我们要一一对应地计算出结果~
题解方法
看到这个奇怪的递推公式,第一反应是不是有点懵呀?没关系,小猫娘最喜欢找规律了,喵!我们来手动算几个值看看~
根据题目的约束,1 <= k < n,并且 C(n, 0) = 1。
当
k = 1时:C(n, 1) = C(n, 0) + C(n-1, 0) = 1 + 1 = 2哦?C(n, 1)的值似乎不依赖于n耶!当
k = 2时:C(n, 2) = C(n, 1) + C(n-1, 1)因为我们已经知道,对于任何大于1的n,C(n, 1)的值都是2。所以C(n, 1) = 2,C(n-1, 1)也等于2。 那么C(n, 2) = 2 + 2 = 4。当
k = 3时:C(n, 3) = C(n, 2) + C(n-1, 2)同理,C(n, 2) = 4,C(n-1, 2)也等于4。 所以C(n, 3) = 4 + 4 = 8。
喵喵~ 主人你看! C(n, 1) = 2 = 2^1C(n, 2) = 4 = 2^2C(n, 3) = 8 = 2^3 一个大胆的猜想出现了:C(n, k) = 2^k!
我们可以用数学归纳法来证明这个猜想是不是正确的~
- 基础情况 (Base Case): 当
k = 1时,我们已经算过了,C(n, 1) = 2 = 2^1。成立! - 归纳假设 (Inductive Hypothesis): 假设当
k = m时,对于所有n > m,都有C(n, m) = 2^m成立。 - 归纳步骤 (Inductive Step): 我们来证明当
k = m+1时也成立。C(n, m+1) = C(n, m) + C(n-1, m)根据我们的归纳假设,因为n > m+1和n-1 > m,所以C(n, m) = 2^m并且C(n-1, m) = 2^m。 所以,C(n, m+1) = 2^m + 2^m = 2 * 2^m = 2^(m+1)。 证明成功啦!喵~
所以,这个看似复杂的递推关系,其实结果非常简单!C(n, k) 的值只和 k 有关,n 只要满足 n > k 就行了(题目已经保证了这一点),它就是 2^k!
那么问题就转化成了:对于每个输入的 k,计算 2^k mod (10^9 + 7)。 因为 k 最大可以到 10^5,直接用循环去乘 k 次会超时,所以我们需要一个更快的计算幂的方法,那就是——快速幂!
题解
下面我们来一起看看这份优雅的代码是怎么实现的吧~
#include <iostream>
#include <vector>
// 这是一个快速幂函数,用来计算 (base^exp) % mod
// 对于大指数来说非常高效哦
long long power(long long base, long long exp) {
long long res = 1;
long long mod = 1000000007;
base %= mod;
while (exp > 0) {
// 如果 exp 是奇数,就说明当前二进制位是 1
if (exp % 2 == 1) {
res = (res * base) % mod;
}
// base 自乘,为下一位做准备
base = (base * base) % mod;
// exp 右移一位,相当于除以 2
exp /= 2;
}
return res;
}
int main() {
// 这两行是为了让输入输出更快一点的小魔法~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
// 根据题目,所有的 n 在一行,所有的 k 在另一行
// n 的值其实我们用不到,但必须读进来才能读到 k
std::vector<int> n(t);
std::vector<int> k(t);
for (int i = 0; i < t; ++i) {
std::cin >> n[i];
}
for (int i = 0; i < t; ++i) {
std::cin >> k[i];
}
// 循环处理每一组查询
for (int i = 0; i < t; ++i) {
// 根据我们推导出的结论,C(n, k) 就等于 2^k
// 调用快速幂函数计算结果
long long result = power(2, k[i]);
// 按格式输出,最后一位后面没有空格哦
std::cout << result << (i == t - 1 ? "" : " ");
}
std::cout << "\n";
return 0;
}代码的逻辑非常清晰,对吧?
power函数是核心,它用快速幂算法在O(log k)的时间里计算出2^k。main函数里,先是读入数据。这里有个小陷阱,就是n和k是分开两行读的。虽然我们推导出n没用,但还是要把它读掉,这样才能正确读到k。- 然后对每个
k,调用power(2, k)计算结果并输出。整个过程又快又准确,喵~
知识点介绍
这道题虽然简单,但涉及到的知识点可是非常重要的哦!
1. 递推关系与数学归纳法 (Recurrence & Induction)
- 递推关系:就是一个数列中的项可以用它前面的项来表示。这道题里的
C(n, k) = C(n, k-1) + C(n-1, k-1)就是一个递推关系。 - 数学归纳法:是一种证明方法,特别适合用来证明和自然数有关的命题。当我们通过观察找到了一个规律(比如
C(n, k) = 2^k),就可以用数学归纳法来严格地证明它。先证明基础情况,再假设k=m成立,然后推导出k=m+1也成立。这是我们解决递推问题的强大武器!
2. 快速幂 (Binary Exponentiation)
- 用途:快速计算
(base^exp) % mod。 - 核心思想:利用指数
exp的二进制表示。比如我们想算2^13,13的二进制是1101,也就是8 + 4 + 1。所以2^13 = 2^8 * 2^4 * 2^1。我们可以通过不断地将底数平方(2^1 -> 2^2 -> 2^4 -> 2^8 ...)来得到这些项,然后只把指数二进制位为1对应的项乘起来。 - 复杂度:时间复杂度是
O(log exp),比普通的O(exp)循环快得多啦!在算法竞赛中是必备技能哦。
3. 模运算 (Modular Arithmetic)
- 为什么需要:因为
2^k会变得非常大,会超出计算机整数类型的表示范围(比如long long)。 - 核心性质:
(a + b) % m = ((a % m) + (b % m)) % m和(a * b) % m = ((a % m) * (b % m)) % m。这个性质允许我们在计算的每一步都取模,防止中间结果溢出,同时保证最终结果的正确性。在快速幂的每一步乘法后都取模,就是这个道理啦。
好啦,这次的题解就到这里了喵~ 主人有没有觉得,一个可爱的小错误,反而让问题变得更简单有趣了呢?下次再见啦,喵~