快速幂第二章(C++循环迭代版本)

快速幂第二章(C++循环迭代版本)

既然是第二章,那当然有第一章啦!详见快速幂第一章(C++递归版本)

如果您想要更加透彻地学习快速幂的知识,请务必将这篇文章细细地,一字不落地看完。

前言

当题目要我们求类如 a b = ? a^b=? ab=?的题目时,最直接,最暴力的写法如下:

#include <bits/stdc++.h>
using namespace std;
#define LL long long
int main() {
    LL a,b,res=1;
    cin>>a>>b;
    while(b--) {
        res*=a;
    }
    cout<<res;
}

算法简单明了,循环 b b b次,每次答案 × a ×a ×a。而这类代码,往往是TLE。

TLE很好理解。这段代码的时间复杂度是 O ( b ) O(b) O(b)。当b的数据范围很大时自然会超时。

(时间复杂度如何计算可以参考编者的另一篇博客 O O O时间复杂度详解)


关于幂的知识

  1. a b = a × a × . . . × a × a a^b=a×a×...×a×a ab=a×a×...×a×a b b b a a a相乘)
  2. a x × a y a^x×a^y ax×ay
    = a × a × . . . × a \red a\red ×\red a\red×\red.\red.\red.\red×\red a a×a×...×a ( x (x (x a ) a) a) × × × a × a × . . . × a × a \blue a\blue ×\blue a\blue ×\blue .\blue .\blue .\blue ×\blue a\blue ×\blue a a×a×...×a×a ( y (y (y a ) a) a)
    = ( x + y ) (x+y) (x+y) a a a相乘的积
    = a x + y a^{x+y} ax+y
    例如 2 3 × 2 5 = 2 8 2^3×2^5=2^8 23×25=28

进制转换

对于 10 10 10进制转成 x x x进制,我们可以采用除以 x x x取余法:

这是一种常见的手工计算方法,适用于整数。具体步骤如下:
将十进制数不断除以 x x x,直到商为 0 0 0为止,取得的余数从下往上依次排列,即为 x x x进制数的结果。
例如:将十进制数 11 11 11转换为二进制:
第一步: 11 ÷ 2 = 5...1 11÷2=5...1 11÷2=5...1
第二步: 5 ÷ 2 = 2...1 5÷2=2...1 5÷2=2...1
第三步: 2 ÷ 2 = 1...0 2÷2=1...0 2÷2=1...0
第四步: 1 ÷ 2 = 0...1 1÷2=0...1 1÷2=0...1
将得到的余数从上往下排列,得到的结果为 1101 1101 1101

1101 1101 1101 11 11 11的二进制形式的逆序数(倒过来)。

1101 1101 1101反过来, 1011 1011 1011即为所求十进制数 11 11 11的二进制形式。


正文

为了直观简洁,这里用字母 D \scriptsize D D B \scriptsize B B表示十进制标识和二进制表标识

比如下面的 5 ( D ) 5\scriptsize(D) 5(D)表示十进制数 5 5 5 101 ( B ) 101\scriptsize(B) 101(B)表示二进制数 101 101 101

5 ( D ) 5\scriptsize(D) 5(D) = 101 =101 =101 ( B ) \scriptsize(B) (B)
   = 100 ( B ) =100\scriptsize(B) =100(B) + 1 +1 +1 ( B ) \scriptsize(B) (B)
   = 4 ( D ) =4\scriptsize(D) =4(D) + 1 +1 +1 ( D ) \scriptsize(D) (D)

很容易理解吧?

用这种方式,我们可以将任一个数字拆分成若干个 2 2 2的幂次方数相加的形式。

11 = 8 + 2 + 1 , 21 = 16 + 4 + 1 , 15 = 8 + 4 + 2 + 1 11=8+2+1,21=16+4+1,15=8+4+2+1 11=8+2+121=16+4+115=8+4+2+1

根据上面的内容,可以这样将 11 11 11拆分成 8 + 2 + 1 8+2+1 8+2+1的和。
再根据板块二幂的知识可以得到 2 11 = 2 8 × 2 2 × 2 1 2^{11}=2^8×2^2×2^1 211=28×22×21

现在我们的问题就变成了怎样求 2 8 × 2 2 × 2 1 2^8×2^2×2^1 28×22×21

我们看一下 11 11 11的二进制形式 1011 1011 1011

好像发现了什么, 2 11 = 2 8 × 2 2 × 2 1 2^{11}=2^8×2^2×2^1 211=28×22×21,反过来写成另外一种形式就是:

         \space\space\space\space\space\space\space\space          2 11 = 1 × 2 1 2^{11}=\blue1×\red2^{\red1} 211=1×21    \space\space    × × ×    \space\space    1 × 2 2 \blue1×\red2 ^{\red2} 1×22    \space\space       \space\space    × × ×    \space\space    0 × 2 4 \blue0×\red2^{\red4} 0×24    \space\space       \space\space    × × ×    \space\space    1 × 2 8 \blue1×\red2^{\red8} 1×28

注:    \space\space    0 × 2 4 0×2^4 0×24    \space\space   不是真正的让答案 × 0 ×0 ×0,而仅仅表示答案在运算这一步时不变。

我们先观察蓝色部分,发现这些数分别是 1   0   1   1 1\space 0\space1\space1 1 0 1 1 11 ( D ) 11\scriptsize(D) 11(D)= 1101 ( B ) 1101\scriptsize(B) 1101(B) 1101 1101 1101倒过来是不是与上面算式中的 1011 1011 1011恰好吻合?也就是说,蓝色部分就是 11 ( D ) 11\scriptsize(D) 11(D) 2 2 2进制的逆序数(反过来)。

我们怎么才能得到一个数 2 2 2进制的逆序数(反过来)形式呢?很简单,只需要不断保存 m o d   2 mod\space2 mod 2的结果,然后不断除以 2 2 2。前面转换二进制的部分提到过。

11 11 11 11 % 2 = 1 , 11 / 2 = 5 ; 5 % 2 = 1 , 5 / 2 = 2 ; 2 % 2 = 0 , 2 / 2 = 1 ; 1 % 2 = 1 , 1 / 2 = 0 11\%2=\red1,11/2=5;5\%2=\red1,5/2=2;2\%2=\red0,2/2=1;1\%2=\red1,1/2=0 11%2=1,11/2=5;5%2=1,5/2=2;2%2=0,2/2=1;1%2=1,1/2=0
红色部分 1 1 0 1 \red1\red1\red0\red1 1101即为所求的 11 11 11 2 2 2进制形式逆序数。

我们再看每一个部分的后一个数,很明显,它们都是 2 2 2的幂次方,且之间互相存在着关联。 2 8 = 2 4 × 2 4 , 2 4 = 2 2 × 2 2 , 2 2 = 2 1 × 2 1 2^8=2^4×2^4,2^4=2^2×2^2,2^2=2^1×2^1 28=24×24,24=22×22,22=21×21,即每一项都是前一项的平方。

代码:

#include <bits/stdc++.h>
#define LL long long
using namespace std;
int main() {
    LL a,b,ans=1,res;
    cin>>a>>b;
    res=a;
    while(b) {
        int k=b&1;
        if(k) ans*=res;
        res=res*res;
        b>>=1;
    }
    cout<<ans;
}

再把公式放一下:

         \space\space\space\space\space\space\space\space          11 ( D ) 11\scriptsize(D) 11(D) = 1011 ( B ) =1011\scriptsize(B) =1011(B) ,   ,\space , 逆序为 1101 1101 1101
         \space\space\space\space\space\space\space\space          2 11 ( D ) = 1 × 2 1 2^{11\tiny(D)}=\blue1×\red2^{\red1} 211(D)=1×21    \space\space    × × ×    \space\space    1 × 2 2 \blue1×\red2 ^{\red2} 1×22    \space\space       \space\space    × × ×    \space\space    0 × 2 4 \blue0×\red2^{\red4} 0×24    \space\space       \space\space    × × ×    \space\space    1 × 2 8 \blue1×\red2^{\red8} 1×28
         \space\space\space\space\space\space\space\space          3 11 ( D ) = 1 × 3 1 3^{11\tiny(D)}=\blue1×\red3^{\red1} 311(D)=1×31    \space\space    × × ×    \space\space    1 × 3 2 \blue1×\red3 ^{\red2} 1×32    \space\space       \space\space    × × ×    \space\space    0 × 3 4 \blue0×\red3^{\red4} 0×34    \space\space       \space\space    × × ×    \space\space    1 × 3 8 \blue1×\red3^{\red8} 1×38
         \space\space\space\space\space\space\space\space         注:    \space\space    0 × 2 4 \blue0×\red2^{\red4} 0×24    \space\space   不是真正的让答案 × 0 ×0 ×0,而仅仅表示答案在运算这一步时不变。

代码中的 r e s res res模拟红色部分, a n s ans ans记录答案。

代码第 8 8 8 i f ( b & 1 ) if(b\&1) if(b&1)我们解释一下, & \& &是按位与。

& \& &的性质: 1 & 1 = 1 , 1 & 0 = 0 , 0 & 1 = 0 , 0 & 0 = 0 1 \& 1 = 1,1\& 0 = 0,0 \& 1 = 0,0 \& 0 = 0 1&1=1,1&0=0,0&1=0,0&0=0

11 & 1 11\&1 11&1的运算过程:

1011 ( B ) 1011\scriptsize(B) 1011(B)
&     1 ( B ) \&\space\space\space1\scriptsize(B) &   1(B)
− - − - − - − -
0001 ( B ) 0001\scriptsize(B) 0001(B)

当一个数 & 1 = 1 \&1=1 &1=1时,它就是一个奇数,否则是偶数。 & 1 \&1 &1本质上是通过判断它的二进制数的最后一位是否为 1 1 1来判断它的奇偶性。(其实 & 1 \&1 &1等价于 % 2 \%2 %2 & 1 \&1 &1 % 2 \%2 %2更快)

11 11 11行, b > > = 1 b>>=1 b>>=1也可以写成 b = b > > 1 b=b>>1 b=b>>1 > > >> >>是右移,表示将二进制数的最右边一位舍去。如 101 ( B ) 101\scriptsize(B) 101(B) > > 1 >>1 >>1后是 10 ( B ) 10\scriptsize(B) 10(B)。(其实 > > 1 >>1 >>1等价于 ÷ 2 ÷2 ÷2, > > 1 >>1 >>1 ÷ 2 ÷2 ÷2更快)

我们不难发现,每次循环第 8 8 8行的 & 1   ( % 2 ) \&1\space(\%2) &1 (%2) 和第 11 11 11 > > 1   ( ÷ 2 ) >>1\space(÷2) >>1 (÷2) 结合在一起算出的 k k k的其实就是前面提到的 2 2 2进制形式逆序数,为公式的中的蓝色部分。

r e s res res负责模拟红色部分,初始值是 a a a,每次循环它自乘一次,没什么可以多说的。

这样,红色部分和蓝色部分我们都得出来了。

现在,当 b ≠ 0 b≠0 b=0时,在每次循环中,我们只需要在蓝色部分有值的时候将 a n s ans ans改变, × × ×红色部分 r e s res res就行了。

代码能看懂了吗?

我们将第 8 , 9 8,9 8,9行合并一下,就可以得到完整代码:

完整代码:

#include <bits/stdc++.h>
#define LL long long
using namespace std;
int main() {
    LL a,b,ans=1,res;
    cin>>a>>b;
    res=a;
    while(b) {
        if(b&1) ans*=res;
        res*=res;
        b>>=1;
    }
    cout<<ans;
}

模拟

以上这段代码思路正确,没有问题。

让我们根据程序一步步地模拟 2 25 2^{25} 225的计算:

    \space\space\space     1 1 1步, b b b 25 25 25 , b ! = 0 ,b!=0 ,b!=0,进入循环:
         \space \space\space \space\space \space\space \space         因为 25 & 1 = 1 , 25\&1=1, 25&1=1, 所以执行 a n s = a n s × r e s = 1 ∗ 2 = 2 , ans=ans×res=1*2=2, ans=ans×res=12=2,
         \space \space\space \space\space \space\space \space          r e s = r e s × r e s = 2 ∗ 2 = 4 , b = b > > 1 = 25 / 2 = 12 res=res×res=2*2=4,b=b>>1=25/2=12 res=res×res=22=4,b=b>>1=25/2=12

    \space\space\space     2 2 2步, b b b 12 12 12 , b ! = 0 ,b!=0 ,b!=0,进入循环:
         \space \space\space \space\space \space\space \space         因为 12 & 1 = 0 , 12\&1=0, 12&1=0, 所以 a n s ans ans无变化,
         \space \space\space \space\space \space\space \space          r e s = r e s × r e s = 4 ∗ 4 = 16 , b = b > > 1 = 12 / 2 = 6 res=res×res=4*4=16,b=b>>1=12/2=6 res=res×res=44=16,b=b>>1=12/2=6

    \space\space\space     3 3 3步, b b b 6 6 6 , b ! = 0 ,b!=0 ,b!=0,进入循环:
         \space \space\space \space\space \space\space \space         因为 6 & 1 = 0 , 6\&1=0, 6&1=0, 所以 a n s ans ans无变化,
         \space \space\space \space\space \space\space \space          r e s = r e s × r e s = 16 ∗ 16 = 256 , b = b > > 1 = 6 / 2 = 3 res=res×res=16*16=256,b=b>>1=6/2=3 res=res×res=1616=256,b=b>>1=6/2=3

    \space\space\space     4 4 4步, b b b 3 3 3 , b ! = 0 ,b!=0 ,b!=0,进入循环:
         \space \space\space \space\space \space\space \space         因为 3 & 1 = 1 , 3\&1=1, 3&1=1, 所以执行 a n s = a n s × r e s = 2 ∗ 256 = 512 , ans=ans×res=2*256=512, ans=ans×res=2256=512,
         \space \space\space \space\space \space\space \space          r e s = r e s × r e s = 256 ∗ 256 = 65536 , b = b > > 1 = 3 / 2 = 1 res=res×res=256*256=65536,b=b>>1=3/2=1 res=res×res=256256=65536,b=b>>1=3/2=1

    \space\space\space     5 5 5步, b b b 1 1 1 , b ! = 0 ,b!=0 ,b!=0,进入循环:
         \space \space\space \space\space \space\space \space         因为 1 & 1 = 1 , 1\&1=1, 1&1=1, 所以执行 a n s = a n s × r e s = 512 ∗ 65536 = 33554432 , ans=ans×res=512*65536=33554432, ans=ans×res=51265536=33554432,
         \space \space\space \space\space \space\space \space          r e s = r e s × r e s = 65536 ∗ 65536 = 4294967296 , b = b > > 1 = 1 / 2 = 0 res=res×res=65536*65536=4294967296,b=b>>1=1/2=0 res=res×res=6553665536=4294967296,b=b>>1=1/2=0

    \space\space\space     6 6 6步, b b b 0 0 0 , b = = 0 ,b==0 ,b==0,结束循环,输出 a n s ans ans 33554432 33554432 33554432

整个程序结束。我们用计算器验算也可以得到 2 25 = 33554432 2^{25}=33554432 225=33554432


小结

在计算 a b a^{b} ab时,即使 a a a是一个很小的数,但当 b b b的数据范围较大时, a b a^b ab也极有可能超出 l o n g   l o n g long\space long long long的范围,所以我们要注意题目的说法,注意有没有让你对答案进行取模操作。

您听懂了吗?

写作不易,不喜勿喷,留个赞吧!

  • 34
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值