深入理解非递归快速幂

深入理解非递归快速幂

网络上很多博客都只有板子,并没有严谨详细探讨其思想。本篇文章将从二进制角度来深入理解非递归快速幂

原理

先举个栗子,求 (3^{15}) 。对于 (3^{15}) 我们可以通过数学幂的运算法则得到下面的式子

[3^{17}=3^{2^{4}+2^{0}}=3^{2^{4}}*3^{2^{0}} ]

[3^{2^{i}}=3^{2^{i-1}}*3^{2^{i-1}} ]

我们会发现将其指数化为以2为底数的数之和后,计算 (3^{15}) 时,仅需算出 (3^{2^{4}})(3^{2^{0}}) 便可求得 (3^{15}) 的值,相比于遍历指数次,每次乘以底数的传统做法,效率更高。而我们又会发现 (3^{2^{i}}) 是由 (3^{2^{i-1}}) 转换过来的,所以,我们只需求出之和构成指数的2为底数的各个数(即问是哪些以2为底数的数累加起来为指数的),便可求解。

[17=2^{4}+2^{0} ]

而后,我们再通过观察发现,上面这个式子中的4和0其实就是17的二进制上不为0的二进制位

(废话,二进制本身不就是将任意十进制数拆解为以2为底数的数之和的过程吗)

找出之和构成指数的以2为底数的各个数后,我们只需要从 (3^{2^{0}}) 开始,每次平方本身,推出第i位的 (3^{2^{i}}) ,并根据上面的分解规则(规律),逐位判断二进制从而解决最后的问题从而最终求出快读幂。

总而言之,我觉得快速幂算法其实是利用二进制将数分解为很多个递归平方(词穷了),每次都以平方级增长,从而实现快速求幂。

实现

先上个非递归快速幂板子

LL pow_mod(LL a, LL b, LL mo){
    LL ret = 1;
    while(b){
        if(b & 1) ret = (ret * a) % mo;
        a = (a * a) % mo;
        b >>= 1;
    }
    return ret;
}

首先需要明白的是,b&1在二进制中可以理解为取b的二进制最后一位,如果b&1返回1(即true)则末尾为1,反之为0(所以根据这个性质也可以很快地判断一个数是否为奇数),所以在这里只有当b的二进制最后一位为1时,才执行其后语句ret = (ret * a) % mo;;另外b >>= 1是表示将b右移1位,即将b的二进制整体向右移动一格。

既然理解了原理以及实现要点,希望读者尽可能自己琢磨一下这个程序,可以模拟感受一下,因为那样来得更快

在这个程序中,每一次,无论当前二进制最后一位是否为1,a都要平方一下(因为 (3^{2^{i}}=3^{2^{i-1}}*3^{2^{i-1}}) ,前一个值会影响后一个值,所以必须维护),然后将当前二进制右移一位,去掉已经处理过的二进制数位以处理下一位,但当当前二进制最后一位为1时,根据之前的规律,需将a加入到答案中,最终当所有二进制数位都被处理掉后,b自然变为0,while循环结束。

以上。

参考

本文采用 知识共享 署名-非商业性使用-相同方式共享 3.0 中国大陆 许可协议进行许可。欢迎转载,请注明出处: 转载自:Santiego的博客

原文地址:https://www.cnblogs.com/santiego/p/9521181.html