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.
  • 相关阅读:
    Windows下Subversion配置管理员指南
    asp.net HttpHand和HttpModule的详细解释,包括Asp.Net对Http请求的处理流程。
    SQL Server 2000不能远程连接的问题
    ASP通用分页类[强]
    不错的站长工具网址(不断收集中....)
    函数的四种调用模式
    HDOJ 3943 Kth Nya Number(数位DP)
    HDOJ 2196 Computer
    HDOJ 3695 Computer Virus on Planet Pandora (AC自动机)
    HDOJ 4277 USACO ORZ (搜索+剪枝)
  • 原文地址:https://www.cnblogs.com/faterazer/p/10978074.html
Copyright © 2011-2022 走看看