zoukankan      html  css  js  c++  java
  • 二分求幂算法:快速的求幂计算方式

    二分求幂法是快速计算形如 (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} imes a^{16})。这说明当我们计算出 (a^{16}) 的值后,再去计算 (a^{32}) 的值并不需要再连乘 (a) 十六次,我们只需要自乘一次 (a^{16}) 即可,这便大大降低了运算步骤。

    如果我们想到了 (a^{32} = a^{16} imes a^{16}),那么我们也可以很自然的进一步想到 (a^{16} = a^{8} imes a^{8}),这同样大大简化了计算 (a^{16}) 的步骤:当我们得到 (a^8) 的值时,只需再自乘一次 (a^8) 即可得到 (a^{16}) 的值,而无需连乘 8 次 (a)。继续这样推演下去,我们可以得到以下的式子:

    [egin{align*} a^{32} &= a^{16} imes a^{16} \[1ex] a^{16} &= a^{8} imes a^{8} \[1ex] a^8 &= a^4 imes a^4 \[1ex] a^4 &= a^2 imes a^2 \[1ex] a^2 &= a imes a \[1ex] a^1 &= a end{align*} ]

    我们可以发现,通过二分求幂的方法,仅需 6 次运算即可得到结果,而在朴素求幂的方法中足足需要 32 次!

    但我们发现一个问题:32 刚好是 2 的 5 次方,所以 32 可以一直被二分到 1 为止,如果失去这种特殊性,我们还可以使用二分求幂吗?答案也是可以的,以计算 (a^{31}) 为例:

    [egin{align*} a^{31} &= a^{16} imes a^{15} \[1ex] &= a^{16} imes a^8 imes a^7 \[1ex] &= a^{16} imes a^8 imes a^4 imes a^3 \[1ex] &= a^{16} imes a^8 imes a^4 imes a^2 imes a^1 end{align*} ]

    当我们得到上面的拆分结果后,再计算 (a^{31}) 就轻松多了。当我们得到 (a) 的值时,自乘一次即可得到 (a^2),再自乘一次即可得到 (a^4),再自乘一次得到 (a^8),再自乘一次得到 (a^{16}),我们最后将这些中间结果乘到一起就计算出了 (a^{31}) 的值。利用二分求幂,计算出 (a^{31}) 仅需要 5 次运算,而朴素求幂足足需要 31 次!

    现在我们已经充分认识到了二分求幂法的威力,但想要完全掌握这一方法,我们还需要攻克一个核心问题——如何正确的分解指数,使其可以满足二分求幂的运算过程(如上述对 (a^{31}) 的分解)。对于一般化的 (a^b),我们可以这样考虑:

    [a^b = a^{b_1 + b_2 + b_3 + cdots} = a^{b_1} cdot a^{b_2} cdot a^{b_3} cdots \[1ex] b_1 + b_2 + b_3 + cdots = b \[1ex] lbrace b_1, b_2, b_3, cdots brace subset lbrace 1,2,4, 8, cdots brace = lbrace 2^0, 2^1, 2^2, cdots, 2^n brace ]

    显然,这样分解出来的指数 (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 brace subset lbrace 1, 2, 4, 8, cdots brace),而 (lbrace 1, 2, 4, 8, cdots brace) 又等于 (lbrace 2^0, 2^1, 2^3, 2^4, cdots brace),看到这里,我们只需要再迈出最后一步——联想到二进制,就可以完全掌握二分求幂法了。

    对于任何一个指数 (b),我们可以将其转化为二进制形式,这个二进制串中所有值为 1 的位置所代表的值,就是二分求幂法所需要的分解结果。现在用这样的视角再次回顾之前的 (a^{31}),指数 (31) 的二进制形式为 (11111),这个二进制串从低位到高位每一个 1 的值如下:

    [egin{align*} 2^0 &= 1 \[1ex] 2^1 &= 2 \[1ex] 2^2 &= 4 \[1ex] 2^3 &= 8 \[1ex] 2^4 &= 16 \[1ex] end{align*} ]

    我们可以发现,这些值正好是分解后的结果,即 (a^{31} = a^{16} imes a^8 imes a^4 imes a^2 imes a^1),然后就可以轻松的利用二分求幂法快速计算出结果了。

    再分析一个具体的例子:计算 (a^{177}) 的值。指数 (177) 的二进制形式是 (10110001),所有值为 1 的位置代表的值分别是:

    [egin{align*} 2^0 &= 1 \[1ex] 2^4 &= 16 \[1ex] 2^5 &= 32 \[1ex] 2^7 &= 128 end{align*} ]

    从而可以将 (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;
    }
    

    参考资料:

    1. 王道论坛编组.王道论坛计算机考研机试指南[M]. :, 2013. 101-105.
  • 相关阅读:
    hdu 1199 Color the Ball 离散线段树
    poj 2623 Sequence Median 堆的灵活运用
    hdu 2251 Dungeon Master bfs
    HDU 1166 敌兵布阵 线段树
    UVALive 4426 Blast the Enemy! 计算几何求重心
    UVALive 4425 Another Brick in the Wall 暴力
    UVALive 4423 String LD 暴力
    UVALive 4872 Underground Cables 最小生成树
    UVALive 4870 Roller Coaster 01背包
    UVALive 4869 Profits DP
  • 原文地址:https://www.cnblogs.com/faterazer/p/10978074.html
Copyright © 2011-2022 走看看