快速幂算法 超详细教程

求幂运算

求幂运算大家都不陌生,幂是指数运算的结果,当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
根据数学指数运算法则可以推出下列等式 :
n 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 n^{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)=n231+220+211+201=n123n022n121n120
我们会发现n11 的结果和下面四个项有关 n 1 ∗ 2 3 , n 0 ∗ 2 2 , n 1 ∗ 2 1 , n 1 ∗ 2 0 n^{1*2^3},n^{0*2^2},n^{1*2^1},n^{1*2^0} n123,n022,n121,n120我们知道任何数的0次方都是1所以下面的项结果等于一 n 0 ∗ 2 2 = 1 n^{0*2^2}=1 n022=1那么说明n11的结果只和下面这些项有关 n 1 ∗ 2 3 , n 1 ∗ 2 1 , n 1 ∗ 2 0 n^{1*2^3},n^{1*2^1},n^{1*2^0} n123n121,n120观察发现这些项刚好对应二进制1011(2)的非零位的项。所以我们只需要计算非零位的项的累乘就好了。也就是计算以下等式 n 1 1 = n 2 3 ∗ n 2 1 ∗ n 2 0 n^11=n^{2^3}*n^{2^1}*n^{2^0} n11=n23n21n20根据指数运算特性我们可以得知下面的等式 n 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 n^{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} n20n20=n21,n21n21=n22,n22n22=n23,n23n23=n24
到这里快速幂的思想出来了,上面的描述我们可以用代码实现

long long int quik_power(int base, int power)
{
    long long int result = 1;   //用于存储项累乘与返回最终结果,由于要存储累乘所以要初始化为1
    while (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)
接下来我们可以得到下面的等式
3 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 3^{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)=3251+240+231+221+210+201=325132403231322132103201因为 3 2 4 ∗ 0 = 3 2 1 ∗ 0 = 3 0 = 1 3^{2^4 * 0}=3^{2^1 * 0}=3^0 = 1 3240=3210=30=1所以我们只需要计算 3 2 5 ∗ 1 ∗ 1 ∗ 3 2 3 ∗ 1 ∗ 3 2 2 ∗ 1 ∗ 1 ∗ 3 2 0 ∗ 1 3^{2^5*1}*1*3^{2^3*1}*3^{2^2*1}*1*3^{2^0*1} 325113231322113201根据指数运算性质可以得到 n 2 m − 1 ∗ n 2 m − 1 = n 2 m n^{2^{m-1}}* n^{2^{m-1}}= n^{2^m} n2m1n2m1=n2m所以根据上述公式递推计算即可得以下项的值 3 2 5 , 3 2 4 , 3 2 3 , 3 2 2 , 3 2 1 , 3 2 0 3^{2^5},3^{2^4},3^{2^3},3^{2^2},3^{2^1},3^{2^0} 325324323322321320
最后累乘即可得到结果。

快速幂 指数折半

核心思想:每一次运算都把指数折半,底数变其平方
每次的指数都折半可以把很大的指数不断减小,这样减少循环次数,但还能保持最终结果不变达到快速求幂次方的效果。因为是每一轮指数都折半所以这相当于用二的m次方(m是折半次数)去除于指数,因为指数的”大爆炸“特性所以不管多大的指都能被快速缩小。例如:410第一次折半是45,第二次折半是42,第三次折半是41,第四次折半是40。也就是说若nm的m被除以2k次方k为折半次数。在2k(k=1,2,3,4……)形成指数"大爆炸"。
接下来我们具体看例子。
计算412:
4 12 = 4 ∗ 4 ∗ 4 ∗ 4 ∗ 4 ∗ 4 ∗ 4 ∗ 4 ∗ 4 ∗ 4 ∗ 4 ∗ 4 4^{12}= 4*4*4*4*4*4*4*4*4*4*4*4 412=444444444444我们跟据指数折半底数变其平方来操作 ( 4 ∗ 4 ) 12 / 2 = 1 6 6 = ( 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 (44)12/2=166=(44)(44)(44)(44)(44)(44)=161616161616我们发现412变成了166,循环次数从12变成了6,我们继续操作 ( 16 ∗ 16 ) 6 / 2 = 25 6 3 = ( 16 ∗ 16 ) ∗ ( 16 ∗ 16 ) ∗ ( 16 ∗ 16 ) = 256 ∗ 256 ∗ 256 (16*16)^{6/2} = 256^3 = (16*16)*(16*16)*(16*16) = 256*256*256 (1616)6/2=2563=(1616)(1616)(1616)=256256256现在166次方变成了2563,循环次数从6变成了3,我们继续操作,但是会发现现在的指数是3,不能被二整除,那怎么办呢?若指数是奇数那么指数减一再折半。让3变成2再折半。 256 ∗ ( 256 ∗ 256 ) ( 3 − 1 ) / 2 = 256 ∗ 6553 6 1 = 256 ∗ ( 256 ∗ 256 ) = 256 ∗ 65536 256*(256 * 256)^{(3-1)/2}=256*65536^1= 256*(256*256)=256*65536 256(256256)(31)/2=256655361=256(256256)=25665536现在2563变成了256 * 655361 因为现在的指数是1指数是奇数所以我们还是继续指数减一再折半的操作 256 ∗ 65536 ∗ 6553 6 0 / 2 = 256 ∗ 65536 256*65536*65536^{0/2} = 256*65536 25665536655360/2=25665536现在指数已经被除以没了,指数为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 == 1
			result *= 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 == 1
			result *= 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 题解

  • 100
    点赞
  • 247
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
快速模幂乘算法可以用于计算 a^b mod m 的值,其中 a, b 和 m 都是正整数。该算法通过将指数 b 转换为二进制形式,利用幂的性质将幂的乘积转化为幂的平方的乘积降低计算复杂度,同时利用模运算的性质降低中间计算的数值大小,从而避免溢出和提高计算效率。 具体步骤如下: 1. 将指数 b 转换为二进制形式,记作 b0b1b2...bn,其中 bi 表示二进制展开中第 i 位的数值; 2. 初始设置计算结果 res = 1,和幂的底数 base = a; 3. 从二进制展开的最高位 n 开始遍历,若该位的值为 1,则将 res 乘上当前的底数 base 并对乘积取模,即 res = (res * base) % m; 4. 将当前底数 base 的值平方并对其进行模运算,即 base = (base * base) % m; 5. 重复步骤 3 和 4 直到遍历完整个二进制展开,此时 res 的值即为 a^b mod m 的结果。 具体理解可以看下面的示例: 假设需要计算 6^13 mod 11 的值,首先将指数 13 转换为二进制形式为 1101,计算过程如下: b3 = 1,res = 6,base = 6,6 * 6 = 36 = 3 * 11 + 3,res = 6 * 6 = 36 % 11 = 3,base = 6 * 6 = 36 % 11 = 3; b2 = 0,res = 3,base = 3,3 * 3 = 9,res = 3 * 3 = 9 % 11 = 9,base = 3 * 3 = 9 % 11 = 9; b1 = 1,res = 9,base = 9,9 * 3 = 27 = 2 * 11 + 5,res = 9 * 9 = 81 % 11 = 4,base = 9 * 9 = 81 % 11 = 4; b0 = 1,res = 4,base = 4,4 * 4 = 16 = 1 * 11 + 5,res = 4 * 6 = 24 % 11 = 2,base = 4 * 4 = 16 % 11 = 5; 因此,6^13 mod 11 的值为 2。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值