在计算机科学中,幂运算是一种非常常见且基础的操作,尤其是在涉及到大数运算时,幂运算的效率对整个计算过程至关重要。设想以下场景:
如果我们采用朴素的计算方法,例如计算 a b a^b ab 时,通过不断相乘 a a a 来获得结果,其时间复杂度为 O ( b ) O(b) O(b)。当 b b b非常大时,这种方法显然是不可行的。为此,我们需要一种更加高效的算法来解决这个问题,快速幂算法就是这样一种高效的算法。
快速幂算法的核心思想是通过将幂指数拆分为二进制形式,并通过一系列的乘法和平方操作,来高效地计算幂次。它主要依赖两个关键概念:
任何一个整数 ( b ) 都可以表示为一系列二进制位的组合,即:
b = b k × 2 k + b k − 1 × 2 k − 1 + ⋯ + b 1 × 2 1 + b 0 × 2 0 b = b_k \times 2^k + b_{k-1} \times 2^{k-1} + \cdots + b_1 \times 2^1 + b_0 \times 2^0 b=bk×2k+bk−1×2k−1+⋯+b1×21+b0×20其中 b i b_i bi为二进制位(0或1)。通过这一表示,可以将幂运算转化为多个乘法和平方操作。
假设我们要计算 ( 3^{13} ),首先我们将 13 表示为二进制形式,即 ( 13 = 1101_2 )。根据二进制拆分的原则,我们可以将幂运算拆解为:
3 13 = 3 ( 1 × 2 3 + 1 × 2 2 + 0 × 2 1 + 1 × 2 0 ) = 3 8 × 3 4 × 3 0 × 3 1 3^{13} = 3^{(1 \times 2^3 + 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0)} = 3^{8} \times 3^{4} \times 3^{0} \times 3^{1} 313=3(1×23+1×22+0×21+1×20)=38×34×30×31这种拆分将幂运算转化为多次乘法和平方操作,虽然差拆分后的项也是幂,但是这些项彼此之间也是有关系的,可以用较小的计算量算出,因此有助于提高计算效率。
霍纳法则是一种高效计算多项式的方法,能够有效减少乘法运算次数。在快速幂算法中,霍纳法则可以帮助我们进一步简化幂运算的过程。我们可以将幂指数的二进制表示进行逐步计算,而不是一次性完成。根据霍纳法则,了可以进一步变换成
b = ( ( … ( b k × 2 1 + b k − 1 ) × 2 + ⋯ ) × 2 + b 1 ) × 2 + b 0 b =(( \dots (b_k \times 2^1 + b_{k-1}) \times 2 + \cdots )\times 2 + b_1) \times 2 + b_0 b=((…(bk×21+bk−1)×2+⋯)×2+b1)×2+b0那么就有
a b = a ( ( … ( b k × 2 1 + b k − 1 ) × 2 + ⋯ ) × 2 + b 1 ) × 2 + b 0 = ( ⋯ ( ( a b k ) 2 × a b k − 1 ) 2 × ⋯ × a b 1 ) 2 × a b 0 a^b =a^{(( \dots (b_k \times 2^1 + b_{k-1}) \times 2 + \cdots )\times 2 + b_1) \times 2 + b_0}=( \cdots((a^{b_k})^2 \times a^{b_{k-1}})^2 \times \cdots \times a^{b_1})^2 \times a^{b_0} ab=a((…(bk×21+bk−1)×2+⋯)×2+b1)×2+b0=(⋯((abk)2×abk−1)2×⋯×ab1)2×ab0
从左到右的快速幂算法也就是刚才通过霍纳法则导出来的公式
通过依次读取幂指数 b b b的二进制表示,从高位到低位逐步构建幂运算的结果。在每读取一位时,首先将当前的结果进行平方,然后根据该位是否为1来决定是否乘以基数 a a a。这种方式避免了重复计算,使得时间复杂度降低到 O ( log b ) O(\log b) O(logb)。
def quick_pow_from_left_to_right(a, b):
b = bin(b)[2:] # 将整数b转换为二进制字符串,并去除前缀'0b'
ans = 1 # 初始化答案为1,因为幂次最低为0,任何数的0次幂都是1
for b_i in b: # 遍历b的二进制表示中的每一位
ans *= ans # 每遇到一个二进制位,都需要平方之前的结果
if b_i == '1': # 如果当前二进制位是1,则需要将a累乘到答案中
ans *= a
return ans # 返回最终的计算结果
假设 ( b = 13 )(即二进制表示为 1101),我们想计算 ( 3^{13} )。从左到右读取其二进制表示:
bit | 1 | 1 | 0 | 1 |
---|---|---|---|---|
结果 | a 1 a^1 a1 | a 3 a^3 a3 | a 6 a^6 a6 | a 13 a^{13} a13 |
与从左到右的算法不同,从右到左的算法从幂指数的最低位开始读取二进制位。每次读取一位后,决定是否将当前的基数乘到结果上,然后将基数平方,为下一次迭代做准备。这个过程是直接利用了二进制拆分的公式,所以理解起来非常的直观
初始化结果为1,当前基数为 a a a。
从右到左扫描 b b b的二进制表示(从最低位到最高位)。
对于每一位:
最终的结果即为 a b a^b ab。
def quick_pow_from_right_to_left(a, b):
b = reversed(bin(b)[2:]) # 将整数b转换为反转二进制字符串,并去除前缀'0b'
ans = 1 # 初始化答案为1,即a的0次方
for b_i in b: # 遍历b的二进制每一位
if b_i == '1': # 如果当前位为1,将当前结果与a的平方累乘
ans *= a
a *= a # 每遍历一位,将底数a平方
return ans # 返回计算结果
同样以 b = 13 b = 13 b=13(二进制为1101)为例,从右到左的计算步骤如下:
bit | 1 | 1 | 0 | 1 |
---|---|---|---|---|
结果 | a 13 a^{13} a13 | a 5 a^5 a5 | a 1 a^1 a1 | a 1 a^1 a1 |
快速幂算法的时间复杂度为 O ( log b ) O(\log b) O(logb),无论是从左到右还是从右到左。相比于朴素的 O ( b ) O(b) O(b)复杂度,这种方法在处理大幂次计算时显著提高了效率。
我将几种方法放在一起进行一个对比
a = 2
b = 1000000
import time
print('从左到右快速幂')
t = time.time()
quick_pow_from_left_to_right(a, b)
print(time.time() - t)
print('从右到左快速幂')
t = time.time()
quick_pow_from_right_to_left(a, b)
print(time.time() - t)
print('朴素幂')
t = time.time()
a ** b # 假设有一个pow_函数实现朴素幂运算
print(time.time() - t)
print('库函数')
t = time.time()
pow(a, b)
print(time.time() - t)
运行结果如下:
从左到右快速幂
0.0029942989349365234
从右到左快速幂
0.005053997039794922
朴素幂
18.3477623462677
库函数
0.0029935836791992188
可以发现在大指数时,快速幂的效果非常明显,并且根据运行时间推断,python内置的 pow
函数应该也是快速幂算法的变种。
快速幂算法通过将幂次的二进制表示与逐步的平方、乘法操作结合起来,实现了高效的幂运算。无论是从左到右还是从右到左的实现,都大大减少了计算所需的时间,特别是在需要处理大数幂运算的场景下。掌握这种算法,对提升计算效率具有重要意义。