快速幂算法 超详细教程
快速幂
- 求幂运算
- 快速幂引入
- 快速幂 二进制
- 快速幂 指数折半
- 快速幂的应用
求幂运算
求幂运算大家都不陌生,幂是指数运算的结果,当m是正整数时nᵐ的意义为m个n相乘,n的m次幂也就是n的m次方。用代码实现求幂运算可以这样写:
long long int mypower(int base, int power)
{long long int result = 1;for (int i = 1; i <= power; i++)result *= base;return result;
}
运行测试结果也是正确的
但我们可以分析一下他的时间复杂度,时间复杂度为O(power),power指数越大程序循环次数更多,2的3次方需要循环3次,2的100次方要循环一百次,如果是2的1000000次方那就得循环一百万次,显然在时间上要花费更多。有没有什么方法可以有效减少循环次数从而避免大量时间的开支呢?为了解决这个问题我们引入了新算法,快速幂。
快速幂引入
为了方便大家理解,我会用两种角度去讲解快速幂算法,二进制与指数折半,其实这两个的本质是一样的,只是分析的角度有些许不同,大家可以按照适合自己的角度去理解快速幂算法,当然两种角度都了解了,那更是最好了。
快速幂 二进制
核心思想:利用二进制来加速运算
例如:nᵐ中m = 11(10)。十进制的11转换为为二进制是1011(2)。
十进制转二进制的转换关系 : 11 = 23*1+22*0+21*1+20*1
根据数学指数运算法则可以推出下列等式 :
n11=n1011(2)=n23∗1+22∗0+21∗1+20∗1=n1∗23∗n0∗22∗n1∗21∗n1∗20n^{11} = n^{1011(2)} = n^{2^3*1+2^2*0+2^1*1+2^0*1}=n^{1*2^3}*n^{0*2^2}*n^{1*2^1}*n^{1*2^0}n11=n1011(2)=n23∗1+22∗0+21∗1+20∗1=n1∗23∗n0∗22∗n1∗21∗n1∗20
我们会发现n11 的结果和下面四个项有关 n1∗23,n0∗22,n1∗21,n1∗20n^{1*2^3},n^{0*2^2},n^{1*2^1},n^{1*2^0}n1∗23,n0∗22,n1∗21,n1∗20我们知道任何数的0次方都是1所以下面的项结果等于一n0∗22=1n^{0*2^2}=1n0∗22=1那么说明n11的结果只和下面这些项有关n1∗23,n1∗21,n1∗20n^{1*2^3},n^{1*2^1},n^{1*2^0}n1∗23,n1∗21,n1∗20观察发现这些项刚好对应二进制1011(2)的非零位的项。所以我们只需要计算非零位的项的累乘就好了。也就是计算以下等式n11=n23∗n21∗n20n^11=n^{2^3}*n^{2^1}*n^{2^0}n11=n23∗n21∗n20根据指数运算特性我们可以得知下面的等式n20∗n20=n21,n21∗n21=n22,n22∗n22=n23,n23∗n23=n24n^{2^0}* n^{2^0}= n^{2^1},n^{2^1}* n^{2^1}= n^{2^2},n^{2^2}* n^{2^2}= n^{2^3},n^{2^3}* n^{2^3}= n^{2^4}n20∗n20=n21,n21∗n21=n22,n22∗n22=n23,n23∗n23=n24
到这里快速幂的思想出来了,上面的描述我们可以用代码实现
long long int quik_power(int base, int power)
{long long int result = 1; //用于存储项累乘与返回最终结果,由于要存储累乘所以要初始化为1while (power > 0) //指数大于0说明指数的二进制位并没有被左移舍弃完毕{if (power & 1) //指数的当前计算二进制位也就是最末尾的位是非零位也就是1的时候//例如1001的当前计算位就是1, 100*1* 星号中的1就是当前计算使用的位result *= base; //累乘当前项并存储base *= base; //计算下一个项,例如当前是n^2的话计算下一项n^2的值//n^4 = n^2 * n^2;power >>= 1; //指数位左移,为下一次运算做准备//一次的左移将舍弃一个位例如1011(2)一次左移后变成101(2)}return result; //返回最终结果
}
为了便于理解我们分析一个实例
计算 345
首先把指数45转换为二进制 45(10)=101101(2)45(10) = 101101(2)45(10)=101101(2)
接下来我们可以得到下面的等式
345(10)=3101101(2)=325∗1+24∗0+23∗1+22∗1+21∗0+20∗1=325∗1∗324∗0∗323∗1∗322∗1∗321∗0∗320∗13^{45(10)} = 3^{101101(2)}=3^{2^5*1 + 2^4*0+2^3*1+2^2*1+2^1*0+2^0*1}=3^{2^5*1}*3^{2^4*0}*3^{2^3*1}*3^{2^2*1}*3^{2^1*0}*3^{2^0*1}345(10)=3101101(2)=325∗1+24∗0+23∗1+22∗1+21∗0+20∗1=325∗1∗324∗0∗323∗1∗322∗1∗321∗0∗320∗1因为324∗0=321∗0=30=13^{2^4 * 0}=3^{2^1 * 0}=3^0 = 1324∗0=321∗0=30=1所以我们只需要计算325∗1∗1∗323∗1∗322∗1∗1∗320∗13^{2^5*1}*1*3^{2^3*1}*3^{2^2*1}*1*3^{2^0*1}325∗1∗1∗323∗1∗322∗1∗1∗320∗1根据指数运算性质可以得到n2m−1∗n2m−1=n2mn^{2^{m-1}}* n^{2^{m-1}}= n^{2^m}n2m−1∗n2m−1=n2m所以根据上述公式递推计算即可得以下项的值325,324,323,322,321,3203^{2^5},3^{2^4},3^{2^3},3^{2^2},3^{2^1},3^{2^0}325,324,323,322,321,320
最后累乘即可得到结果。
快速幂 指数折半
核心思想:每一次运算都把指数折半,底数变其平方
每次的指数都折半可以把很大的指数不断减小,这样减少循环次数,但还能保持最终结果不变达到快速求幂次方的效果。因为是每一轮指数都折半所以这相当于用二的m次方(m是折半次数)去除于指数,因为指数的”大爆炸“特性所以不管多大的指都能被快速缩小。例如:410第一次折半是45,第二次折半是42,第三次折半是41,第四次折半是40。也就是说若nm的m被除以2k次方k为折半次数。在2k(k=1,2,3,4……)形成指数"大爆炸"。
接下来我们具体看例子。
计算412:
412=4∗4∗4∗4∗4∗4∗4∗4∗4∗4∗4∗44^{12}= 4*4*4*4*4*4*4*4*4*4*4*4412=4∗4∗4∗4∗4∗4∗4∗4∗4∗4∗4∗4我们跟据指数折半底数变其平方来操作(4∗4)12/2=166=(4∗4)∗(4∗4)∗(4∗4)∗(4∗4)∗(4∗4)∗(4∗4)=16∗16∗16∗16∗16∗16(4*4)^{12/2}=16^{6}= (4*4)*(4*4)*(4*4)*(4*4)*(4*4)*(4*4) = 16 * 16 * 16 *16*16*16(4∗4)12/2=166=(4∗4)∗(4∗4)∗(4∗4)∗(4∗4)∗(4∗4)∗(4∗4)=16∗16∗16∗16∗16∗16我们发现412变成了166,循环次数从12变成了6,我们继续操作(16∗16)6/2=2563=(16∗16)∗(16∗16)∗(16∗16)=256∗256∗256(16*16)^{6/2} = 256^3 = (16*16)*(16*16)*(16*16) = 256*256*256(16∗16)6/2=2563=(16∗16)∗(16∗16)∗(16∗16)=256∗256∗256现在166次方变成了2563,循环次数从6变成了3,我们继续操作,但是会发现现在的指数是3,不能被二整除,那怎么办呢?若指数是奇数那么指数减一再折半。让3变成2再折半。256∗(256∗256)(3−1)/2=256∗655361=256∗(256∗256)=256∗65536256*(256 * 256)^{(3-1)/2}=256*65536^1= 256*(256*256)=256*65536256∗(256∗256)(3−1)/2=256∗655361=256∗(256∗256)=256∗65536现在2563变成了256 * 655361 因为现在的指数是1指数是奇数所以我们还是继续指数减一再折半的操作256∗65536∗655360/2=256∗65536256*65536*65536^{0/2} = 256*65536256∗65536∗655360/2=256∗65536现在指数已经被除以没了,指数为0了,我们不能再做指数折半底数变其平方的操作了。最后我们得到412=256*65536。原本需要循环12次的如今四次循环完成了。这种折半效率在大指数的时候效果显著。例如410000需要循环10000次但是折半计算后只需要14次循环就能解决因为214=16384这已经超过10000了。回忆一下412=256 * 65536的256和65536是从哪里来的呢。2563与655361指数是奇数的时候被指数减一而分离出来的项。
现在我们尝试用代码去实现上面的指数折半,底数变其平方的操作。
long long int quik_power(int base, int power)
{long long int result = 1;while (power > 0) //指数大于0进行指数折半,底数变其平方的操作{if (power % 2 == 1) //指数为奇数{power -= 1; //指数减一power /= 2; //指数折半result *= base; //分离出当前项并累乘后保存base *= base; //底数变其平方}else //指数为偶数{power /= 2; //指数折半base *= base; //底数变其平方}}return result; //返回最终结果
}
我们不难发现代码中有重复的语句所以我们可以稍加优化整理。
power /= 2; //指数折半base *= base; //底数变其平方
不管指数是否为奇数偶数,指数折半和底数变其平方是都需要进行的必要操作我们可以直接写在判断语句外面
long long int quik_power(int base, int power)
{long long int result = 1;while (power > 0) //指数大于0进行指数折半,底数变其平方的操作{if (power % 2 == 1) //指数为奇数{power -= 1; //指数减一result *= base; //分离出当前项并累乘后保存}power /= 2; //指数折半base *= base; //底数变其平方}return result; //返回最终结果
}
还有一些多余代码我们也能删除。
power -= 1; //指数减一
如果指数是奇数的话有个减一操作,但在代码中我们可以省略这个,因为代码中指数是整数int类型,当power是奇数时小数点会被舍弃,这相当于power减一后再除二的操作。例如:power= 3的时候power / 2 = 相当于power-1后power=2后power / 2 = 1。所以代码可以写成这样。
long long int quik_power(int base, int power)
{long long int result = 1;while (power > 0) //指数大于0进行指数折半,底数变其平方的操作{if (power % 2 == 1) //指数为奇数result *= base; //分离出当前项并累乘后保存power /= 2; //指数折半base *= base; //底数变其平方}return result; //返回最终结果
}
判断一个数的奇数偶数时,除了判断能否被二整除以外我们还有一个方法就是看那个数的二进制。15(10)=1111(2)15(10)=1111(2)15(10)=1111(2)26(10)=11010(2)26(10)=11010(2)26(10)=11010(2)17(10)=10001(2)17(10)=10001(2)17(10)=10001(2)36(10)=100100(2)36(10)=100100(2)36(10)=100100(2)我们可以发现一个规律,奇数的二进制位末尾是1,偶数的二进制位是0.位运算要比取余运算要快所以我们可以把代码写成这样。
long long int quik_power(int base, int power)
{long long int result = 1;while (power > 0) //指数大于0进行指数折半,底数变其平方的操作{if (power & 1) //指数为奇数,power & 1这相当于power % 2 == 1result *= base; //分离出当前项并累乘后保存power /= 2; //指数折半base *= base; //底数变其平方}return result; //返回最终结果
}
我们还可以替换一些代码使程序运算效率更高。
power /= 2; //指数折半
这里的指数折半操作我们也可以使用位运算的左移运算来完成。位运算比除于运算要快。所以我们可以这样写代码。
long long int quik_power(int base, int power)
{long long int result = 1;while (power > 0) //指数大于0进行指数折半,底数变其平方的操作{if (power & 1) //指数为奇数,power & 1这相当于power % 2 == 1result *= base; //分离出当前项并累乘后保存power >>= 1; //指数折半,power >>= 1这相当于power /= 2;base *= base; //底数变其平方}return result; //返回最终结果
}
这就是我们最终的快速幂代码了。大家可能发现了,在快速幂算法中二进制角度还是指数折半角度最终写出的代码其实是一样的只是理解角度略有不同罢了。
快速幂的应用
快速幂一般不会独立使用,常常配合取余运算法则来使用。
看下面这两个题:
HDOJ 2035 人见人爱A^B
HDOJ 1061 Rightmost Digit
做这两个题前首先我们需要了解一下关于取余的公式
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p ) % p
(a * b) % p = (a % p * b % p) % p
我们可以把公式三加入我们的快速幂代码中
long long int quik_power(int base, int power, int p)
{long long int result = 1; while (power > 0) {if (power & 1) result = result * base % p; //根据公式每个项都取余数后在再做累乘base = base * base % p ; //根据公式每个项都取余数后在再做平方操作 power >>= 1; }//根据公式在最后的的结果上再来一次取余数return result % p;
}
那两个题的题解我就不在这写了。特别出两个博客了。感兴趣的朋友可以去看看。
HDOJ 1061 Rightmost Digit 题解
HDOJ 2035 人见人爱A^B 题解
快速幂算法 超详细教程相关推荐
- 点云处理算法整理(超详细教程)
点云处理算法整理(超详细教程) 目录 一. 线性回归_最小二乘法.梯度下降法 二. 线性回归_最小二乘法.RANSAC算法 三. 最近点迭代_ICP算法 四. 常见三角网格划分_voronoi图和De ...
- Linux系统双网卡聚合超详细教程
Linux系统双网卡聚合超详细教程 将多个物理网卡聚合在一起,从而实现冗错和提高吞吐量 网络组不同于旧版中bonding技术,提供更好的性能和扩展性 网络组由内核驱动和teamd守护进程实现. 主要分 ...
- 极验验证码破解—超详细教程(一)
极验验证码破解-超详细教程(一) Gayhub:FanhuaandLuomu/geetest_break 2017.8.21 代码以上传,可供参考 目录 一.网站http://www.gsxt.gov ...
- 快速幂 (转载,详细)
本文转载的,觉得比较详细 快速幂取模算法 在网站上一直没有找到有关于快速幂算法的一个详细的描述和解释,这里,我给出快速幂算法的完整解释,用的是C语言,不同语言的读者只好换个位啦,毕竟读C的人较多~ 所 ...
- java位运算求幂,程序员必学:快速幂算法
前阵子,有小伙伴在我B站的算法教程底下留言 小伙伴们有任何疑问或者希望我解说任何内容,都可以在我的小我私家B站或民众号(xmg_mj)留言哦,我会尽我最大能力.只管抽时间去写文章\录视频来回应人人. ...
- AD从原理图到PCB超详细教程
AD超详细教程 前言 一.建立一个工程模板 二.原理图 1.设计原理图. 2.使用AD自带库和网上开源原理图库 3.画原理图库 4.编译原理图 三.PCB 1.确定元器件尺寸大小 2.绘制PCB Li ...
- IC工程师入门必学《Verilog超详细教程》(附下载)
Verilog HDL(简称 Verilog )是一种硬件描述语言,用于数字电路的系统设计.可对算法级.门级.开关级等多种抽象设计层次进行建模. Verilog 继承了 C 语言的多种操作符和结构,与 ...
- Git安装教程(Windows安装超详细教程)
Git安装教程(Windows安装超详细教程) weir_will 关注 0.9 2017.10.08 00:59* 字数 728 阅读 17955评论 8喜欢 41 2018年4月14日更新 更 ...
- 大数据平台Ambari2.7.4+HDP3.1.4安装(超详细教程)
大数据平台Ambari2.7.4+HDP3.1.4安装(超详细教程) Ambari2.7.4+HDP3.1.4是最后一个免费版本,新版本需要授权. 因工作需要,安装大数据集群,了解到Ambari属于A ...
最新文章
- 视频+课件| 视觉引导系列课程
- tomcat文件目录结构及功能介绍
- 【spring boot】 mybatis配置双数据源/多数据源
- 多个project[项目]共享session
- 03、MySQL—数据表操作
- z370支持pcie信号拆分吗_定了!AMD B550主板确认将支持PCIE4.0,多项能力接近X570
- Midway Serverless 发布 2.0,一体化让前端研发再次提效
- widows下nignx的使用
- 新代系统plc梯形图说明书_东莞自动化PLC编程需要多少钱
- dedecms sphinx 配置
- 利用python进行游戏编程 pdf
- syntaxhighlighter高亮动态代码
- 天骄辅助外挂制作,寻求合作
- Junit4 initializationError初始化错误 终于解决了!!!
- 如何制作网络视频投票?
- 大型计算机网络主机通常采用什么型,全国计算机一级选择题真题集(1)
- Java组各任务工作流程
- 《陰陽師·付喪神卷·迷神》原作:夢枕貘
- 爬取酷狗音乐TOP榜所有的歌曲信息
- 推荐8个良心安全测试工具,快来取走