二分求幂算法:快速的求幂计算方式
二分求幂法是快速计算形如 \(a^b\) 的求幂运算的方法。朴素计算 \(a^b\) 的方式是将 \(a\) 连乘 \(b\) 次,代码如下:
int result = 1;
for (int i = 0; i != b; i++)
result *= a;
这需要计算 \(b\) 次,而实际真的需要运算这么多次吗?答案是不需要,利用二分求幂法,我们可以使运算次数大大小于 \(b\) 次。那么什么是二分求幂法呢?我们先考虑一个具体的计算:\(a^{32} = \; ?\)。
首先,我们需要想到:\(a^{32} = a^{16} \times a^{16}\)。这说明当我们计算出 \(a^{16}\) 的值后,再去计算 \(a^{32}\) 的值并不需要再连乘 \(a\) 十六次,我们只需要自乘一次 \(a^{16}\) 即可,这便大大降低了运算步骤。
如果我们想到了 \(a^{32} = a^{16} \times a^{16}\),那么我们也可以很自然的进一步想到 \(a^{16} = a^{8} \times a^{8}\),这同样大大简化了计算 \(a^{16}\) 的步骤:当我们得到 \(a^8\) 的值时,只需再自乘一次 \(a^8\) 即可得到 \(a^{16}\) 的值,而无需连乘 8 次 \(a\)。继续这样推演下去,我们可以得到以下的式子:
我们可以发现,通过二分求幂的方法,仅需 6 次运算即可得到结果,而在朴素求幂的方法中足足需要 32 次!
但我们发现一个问题:32 刚好是 2 的 5 次方,所以 32 可以一直被二分到 1 为止,如果失去这种特殊性,我们还可以使用二分求幂吗?答案也是可以的,以计算 \(a^{31}\) 为例:
当我们得到上面的拆分结果后,再计算 \(a^{31}\) 就轻松多了。当我们得到 \(a\) 的值时,自乘一次即可得到 \(a^2\),再自乘一次即可得到 \(a^4\),再自乘一次得到 \(a^8\),再自乘一次得到 \(a^{16}\),我们最后将这些中间结果乘到一起就计算出了 \(a^{31}\) 的值。利用二分求幂,计算出 \(a^{31}\) 仅需要 5 次运算,而朴素求幂足足需要 31 次!
现在我们已经充分认识到了二分求幂法的威力,但想要完全掌握这一方法,我们还需要攻克一个核心问题——如何正确的分解指数,使其可以满足二分求幂的运算过程(如上述对 \(a^{31}\) 的分解)。对于一般化的 \(a^b\),我们可以这样考虑:
显然,这样分解出来的指数 \(b_1, b_2, b_3, \cdots\) 满足二分求幂的运算过程。因为,在二分求幂过程中,从 \(a\) 开始不断自乘,我们可以得到:\(a^1, a^2, a^4, a^8, \cdots\),所以,我们分解出来的 \(a^{b_1}, a^{b_2}, a^{b_3}, \cdots\) 必须属于自乘得到的序列,即指数 \(\lbrace b_1, b_2, b_3, \cdots \rbrace \subset \lbrace 1, 2, 4, 8, \cdots \rbrace\),而 \(\lbrace 1, 2, 4, 8, \cdots \rbrace\) 又等于 \(\lbrace 2^0, 2^1, 2^3, 2^4, \cdots \rbrace\),看到这里,我们只需要再迈出最后一步——联想到二进制,就可以完全掌握二分求幂法了。
对于任何一个指数 \(b\),我们可以将其转化为二进制形式,这个二进制串中所有值为 1 的位置所代表的值,就是二分求幂法所需要的分解结果。现在用这样的视角再次回顾之前的 \(a^{31}\),指数 \(31\) 的二进制形式为 \(11111\),这个二进制串从低位到高位每一个 1 的值如下:
我们可以发现,这些值正好是分解后的结果,即 \(a^{31} = a^{16} \times a^8 \times a^4 \times a^2 \times a^1\),然后就可以轻松的利用二分求幂法快速计算出结果了。
再分析一个具体的例子:计算 \(a^{177}\) 的值。指数 \(177\) 的二进制形式是 \(10110001\),所有值为 1 的位置代表的值分别是:
从而可以将 \(a^{177}\) 分解为 \(a^{128} \cdot a^{32} \cdot a^{16} \cdot a^1\),然后利用二分求幂,从 \(a\) 开始,自乘一次得到 \(a^2\),再自乘一次得到 \(a^4\),以此类推,直到得到 \(a^{128}\)。这些中间结果中,对我们有用的是 \(a, a^{16}, a^{32}, a^{128}\),我们把它们乘到一起,就计算出了 \(a^{177}\) 的值。从程序运行的角度来看,利用二分求幂,得到这个结果需要循环 8 次,而如果要在朴素求幂算法中得到这一结果,则需要运行 177 次!显然,要计算的幂指数越大,二分求幂的优势也就愈加明显。
最后简单地用程序语言表达如何计算 \(a^b\):
int fun(int a, int b) {
int result = 1;
while (b) {
if (b % 2 == 1)
result *= a;
a *= a;
b /= 2;
}
return result;
}
参考资料:
- 王道论坛编组.王道论坛计算机考研机试指南[M]. :, 2013. 101-105.