在算法竞赛和后端开发面试中,你可能会遇到这样的问题:如何快速计算出 $2^{100000}$ 对 $10^9+7$ 取模的结果?
乍一看,这似乎是一个简单的循环计算问题。但如果直接计算,数值的大小会迅速超过计算机甚至宇宙中原子的总数。而且,如果 $exp$(指数)的值达到 $10^{18}$,普通的线性循环方法肯定会导致超时(TLE)。
别担心,在这篇文章中,我们将深入探讨一种非常优雅且高效的算法——快速幂,也被称为平方求幂。我们将一起学习如何利用二进制的特性,将时间复杂度从线性的 $O(N)$ 惊艳地降低到对数的 $O(\log N)$。
为什么要引入模运算?
在正式开始之前,我们必须理解题目中通常包含的那个“奇怪的数字”——模数。在大多数算法题中,这个数通常是 $10^9+7$(即 1000000007)。
假设我们需要计算 $x^y$。当 $y$ 稍微大一点时,结果就会大得离谱。例如,$2^{64}$ 已经超过了 $1.8 \times 10^{19}$。如果 $y$ 是 $10^9$,数字的位数将占据大量的内存,甚至无法存储。
因此,题目通常会要求我们计算 $(x^y) \% \text{Mod}$。这不仅仅是为了存储方便,更是为了在计算过程中保持数值在整数范围内(例如 64 位长整型 long long)。这里有一个关键的数学性质我们需要记住:
$$ (a \times b) \% p = ((a \% p) \times (b \% p)) \% p $$
这个性质意味着我们可以在乘法运算的每一步都进行取模,从而保证中间结果永远不会溢出。
朴素方法的局限
最直观的方法是写一个简单的循环:
// 朴素方法 - 代码示例
// 优点:逻辑简单
// 缺点:当指数很大时(例如 10^18),速度极其慢
long long朴素幂
long long res = 1;
for(int i = 0; i < exp; i++) {
res = (res * base) % mod;
}
return res;
}
我们可以看到,这个算法的时间复杂度是 $O(N)$,其中 $N$ 是指数 $exp$。如果 $exp$ 是 $10^9$,我们需要循环十亿次。在现代计算机上,每秒处理 $10^8$ 次运算已经是很不错的成绩了,处理 $10^9$ 次通常需要好几秒,这远远超过了竞赛中通常要求的 1 秒时间限制。
那么,我们能做些什么呢?答案就是利用数学上的分治思想,使用平方求幂。
让我们从数学的角度重新审视幂运算。假设我们要计算 $x^n$。我们可以根据 $n$ 的奇偶性将其拆解:
$$ x^n = \begin{cases}
(x^{n/2})^2 \times x, & \text{if } n \text{ is odd (奇数)} \\
(x^{n/2})^2, & \text{if } n \text{ is even (偶数)}.
\end{cases} $$
这个公式的美妙之处在于,每进行一步,指数 $n$ 都会减半。这意味着计算量将以对数速度下降,而不是线性下降。
#### 递归实现分析
实现这个逻辑最直接的方式是递归。让我们看看它是如何工作的:
- 基本情况:如果 $exp$ 为 0,任何数的 0 次方都是 1。如果 $exp$ 为 1,结果就是
base % mod。 - 递归步骤:我们先计算 $t = \text{exponentiation}(base, exp / 2)$。注意这里使用的是整数除法。
- 平方与取模:计算结果为 $t^2 \% \text{mod}$。这一步对应了公式中的平方部分。
- 处理奇数:如果 $exp$ 是奇数,我们在结果上再乘一次 $base$ 并取模。
让我们用具体的代码来感受一下。
#### 代码实现:C++
在 C++ 中,我们可以这样编写递归版本的快速幂:
// C++ 程序:使用递归二进制求幂计算模指数值
#include
using namespace std;
// 定义模数,通常取 10^9+7
const long long N = 1000000007;
/*
* 函数:exponentiation
* 功能:计算 (base^exp) % N
* 参数:base - 底数, exp - 指数
* 返回值:计算结果
*/
long long exponentiation(long long base, long long exp) {
// 基本情况:如果指数为0,结果是1
if (exp == 0)
return 1;
// 基本情况:如果指数为1,结果是 base % N
if (exp == 1)
return base % N;
// 递归调用:计算 base^(exp/2)
long long t = exponentiation(base, exp / 2);
// 平方并取模:对应 (base^(exp/2))^2
t = (t * t) % N;
// 如果指数是偶数
if (exp % 2 == 0)
return t;
// 如果指数是奇数,需要多乘一个 base
// 对应 t^2 * x
else
return ((base % N) * t) % N;
}
// 主函数进行测试
int main() {
long long base = 5;
long long exp = 100000;
long long modulo = exponentiation(base, exp);
cout << "结果是: " << modulo << endl;
// 验证一下小的用例
cout << "2^10 % N = " << exponentiation(2, 10) << endl; // 应该是 1024
return 0;
}
#### 代码实现:Java
Java 的实现逻辑与 C++ 完全一致,但我们需要注意基本数据类型的范围。这里我们使用 long 类型。
// Java 程序:使用递归二进制求幂计算模指数值
import java.util.*;
import java.lang.*;
import java.io.*;
class FastExponentiation {
// 定义模数为 long 类型
static long MOD = 1000000007L;
public static void main(String[] args) {
long base = 5;
long exp = 100000;
long result = exponentiation(base, exp);
System.out.println(result);
}
/**
* 递归计算快速幂
* @param base 底数
* @param exp 指数
* @return (base^exp) % MOD
*/
static long exponentiation(long base, long exp) {
// 递归终止条件
if (exp == 0)
return 1;
if (exp == 1)
return base % MOD;
// 核心递归步骤
long t = exponentiation(base, exp / 2);
t = (t * t) % MOD;
// 如果是偶数,直接返回 t
if (exp % 2 == 0)
return t;
// 如果是奇数,补上一个 base
else
return ((base % MOD) * t) % MOD;
}
}
#### 代码实现:Python3
Python 的整数处理非常强大,不会轻易溢出,这使得我们的代码可以更加简洁,无需过分担心 INLINECODE017e0fc2 和 INLINECODE66a4eed3 的区别。
# Python3 程序:使用递归二进制求幂计算模指数值
# 定义模数
MOD = 1000000007
def exponentiation(base, exp):
"""
计算 (base^exp) % MOD
"""
if exp == 0:
return 1
if exp == 1:
return base % MOD
# 计算一半的幂
t = exponentiation(base, exp // 2)
t = (t * t) % MOD
# 根据指数奇偶性决定是否补乘 base
if exp % 2 == 0:
return t
else:
return ((base % MOD) * t) % MOD
# 测试代码
if __name__ == "__main__":
base = 5
exp = 100000
result = exponentiation(base, exp)
print(f"{base}^{exp} % {MOD} = {result}")
#### 代码实现:JavaScript
// JavaScript 程序:使用递归二进制求幂计算模指数值
// 定义模数
const MOD = 1000000007;
/**
* 递归快速幂函数
* @param {number} base 底数
* @param {number} exp 指数
* @returns {number} 结果
*/
function exponentiation(base, exp) {
if (exp === 0) return 1;
if (exp === 1) return base % MOD;
let t = exponentiation(base, Math.floor(exp / 2));
t = (t * t) % MOD;
if (exp % 2 === 0) {
return t;
} else {
return ((base % MOD) * t) % MOD;
}
}
// 驱动代码
const base = 5;
const exp = 100000;
const result = exponentiation(base, exp);
console.log(result);
#### 代码实现:C#
C# 的实现与 Java 非常相似,利用 INLINECODE95ac9a69 代码块可以处理一些极端情况(虽然在取模运算中通常不涉及溢出导致的负数问题,但要注意 INLINECODEfb72f15c 的范围)。
// C# 程序:使用递归二进制求幂计算模指数值
using System;
class FastPowerAlgorithm {
// 模数常量
static long N = 1000000007L;
public static void Main(String[] args) {
long baseVal = 5; // base 是关键字,所以用 baseVal
long exp = 100000;
long result = exponentiation(baseVal, exp);
Console.WriteLine(result);
}
static long exponentiation(long baseVal, long exp) {
if (exp == 0)
return 1;
if (exp == 1)
return baseVal % N;
// 递归计算 base^(exp/2)
long t = exponentiation(baseVal, exp / 2);
t = (t * t) % N;
// 如果指数是偶数
if (exp % 2 == 0)
return t;
// 如果指数是奇数
else
return ((baseVal % N) * t) % N;
}
}
算法背后的原理:为什么它这么快?
你可能想知道,为什么仅仅把指数减半就能带来如此巨大的性能提升?让我们来分析一下时间复杂度。
假设指数 $exp$ 是 1024 ($2^{10}$)。
- 朴素方法:我们需要循环 1024 次。复杂度 $O(2^{10})$。
- 快速幂(递归):
* 第 1 层:$1024 \rightarrow 512$
* 第 2 层:$512 \rightarrow 256$
* …
* 第 10 层:$2 \rightarrow 1$
* 第 11 层:返回
你看,我们只需要 10-11 步就能完成计算!对于任意 $N$,递归的深度是 $\log2 N$。这就是我们将复杂度称为 $O(\log N)$ 的原因。即使 $N$ 是 $10^{18}$,$\log2 N$ 大约也只是在 60 左右。相比于 $10^{18}$ 次循环,这简直是瞬间完成。
实战建议与最佳实践
在编写代码时,有几个细节需要你特别注意,以避免在竞赛或实际开发中踩坑:
- 始终使用长整型:在 Java、C# 或 C++ 中,千万不要使用 INLINECODE3dec408c。即使是取模操作,两个接近模数的 INLINECODEef5b9093 相乘(例如 $10^9 \times 10^9$)也会超过 INLINECODE606c752b 的上限(约 $2 \times 10^9$),导致溢出并产生错误的结果。请务必使用 INLINECODE8ba3abeb (Java/C#) 或
long long(C++)。
- 在乘法后立即取模:不要等到递归返回了再取模,也不要在中间步骤省略取模。为了防止溢出并保证速度,每次产生乘法操作后都要立刻取模。
- 注意参数传递:在递归函数中,尽量按值传递基本类型(如 C++ 中的
long long)。虽然这会增加一点栈开销,但避免了意外的副作用。如果是传递大数组或对象,请使用引用或指针。
- 处理 $exp=0$ 的情况:虽然数学上 $0^0$ 是有争议的,但在算法题中通常定义 $0^0 = 1$ 或者输入保证 $base$ 和 $exp$ 不会同时为 0。不过在函数中判断
if (exp == 0) return 1是标准的做法。
总结
通过这篇文章,我们不仅学习了如何使用二进制求幂来解决大数取模的问题,还深入理解了其背后的数学原理。我们从朴素的 $O(N)$ 算法出发,发现了它在面对大指数时的无力,进而利用分治的思想将其优化为 $O(\log N)$ 的高效算法。
关键要点总结:
- 问题:直接计算 $x^y$ 或简单的循环取模在 $y$ 很大时效率低下且容易溢出。
- 解决方案:使用平方求幂递归地将问题规模减半。
- 公式:$x^n = (x^{n/2})^2$ (偶数)或 $x^n = (x^{n/2})^2 \times x$ (奇数)。
- 复杂度:时间复杂度从 $O(N)$ 降低到了 $O(\log N)$,空间复杂度为 $O(\log N)$(由于递归调用栈)。
- 实践:务必使用
long类型,并在每次乘法后进行取模运算。
掌握这个算法是每个算法工程师和程序员的必修课。下次当你遇到指数爆炸的问题时,别忘了祭出这个强有力的武器!
如果你觉得这篇文章对你有帮助,不妨自己动手写一遍代码,尝试修改一下 $base$ 和 $exp$ 的值,看看结果是否符合预期。祝你编码愉快!