快速幂 & 大数取模及例题(极其详细)

首先,以杭电OJ的一道题引入话题

人见人爱A ^ B

接着,先给出取模的几个重要结论

  • (a * b) % p = (a % p * b) % p
  • (a + b) % p = (a % p + b) % p
  • (a + b) % p = (a % p + b % p) % p
  • (a - b)% p = (a % p - b % p) % p
  • (a * b)% p = (a % p * b % p) % p

对于最后一条性质,即多个因子连续的乘积取模的结果等于每个因子取模后的乘积再取模的结果

上题的要求也简洁明了。即A的B次方结果的最后三位数所表示的整数。之所以把所表示的整数加粗,是由于要考虑到最后三位数字的首位为0的情况,如最后三位是024,那么程序输出的整数应该为24。

很多人看到这道题,第一反应是这还不简单,直接一个pow函数求出A的B次方,再对结果取最后三位数字不久好了?下面附上代码

#include <iostream>
#include <cstdio>
#include <cmath>

using namespace std;

int main()
{
    int a,b; //a表示底数,b表示指数
    cin >> a >> b;
    int p = pow(a,b); //pow(a,b)可以求出a的b次方
    cout << p % 1000 << endl;
    return 0;
}

测试一下代码,2的10次方是大家熟悉的1024,结果也得到了24

在这里插入图片描述
但如果输入的数字非常大呢?比如题目所给样例的最后一个。答案会得到0或者是一个负数,因为数据太大发生了溢出。换成long long也不行,大家可以阅读文章最后链接给出的博客。
在这里插入图片描述
那么,问题该如何解决?
需要注意:解决这类问题首先就不能使用pow函数,这是没有算法基础的同学求a的b次方时首先会想到的方法,但其实没啥用,大数据过不去。下面将代码改进如下:

#include <iostream>
#include <cstdio>
#include <cmath>

using namespace std;

int main()
{
    //底数和指数在int的范围之内
    int base,power;
    cin >> base >> power;
    
    long long res = 1;
    
    //执行power次循环,每次结果都乘一个base,即base的power次方
    for (int i = 1;i <= power;i++)
    {
        res = res * base;
    }
    long long ans = res % 1000;
    cout << ans << endl;
    return 0;
}

但是测试代码会发现,如果要求2的100次方,结果输出还是0
在这里插入图片描述
此时,就应该想到上面提到的取模的性质了。本题只需用到最后一个性质 (a * b)% p = (a % p * b % p) % p

解析公式的使用

我们在上面的代码中定义了
long long ans = res % 1000
需要注意,此时的res其实是for循环执行完毕得到的结果res
变形一下
long long ans = ( res * 1 ) % 1000
再利用取模性质
long long ans = ( res % 1000 * 1 % 1000 ) % 1000
而1 % 1000 = 1,所以
long long ans = ( res % 1000 ) % 1000
比较①和②,会发现多取一次模对于结果没有影响,那么为何不提前取模呢??(减少数据运算的次数,降低数据的范围,这是提前取模的两个优点)
下面给出杭电OJ练习的AC代码,其中为防止底数和指数大于 int ,故将所有的数据都改为了long long,并且加入了循环输入以测试多组数据

#include <iostream>
#include <cstdio>

using namespace std;

int main()
{
    long long base,power;
    while (cin >> base >> power) //多组数据
    {
        if (base == 0 && power == 0) break;
        long long res = 1;
    
        //执行power次循环,每次结果都乘一个base,即base的power次方
        for (long long i = 1;i <= power;i++)
        {
            res = res * base;
            res = res % 1000;
        }
        long long ans = res % 1000;
        cout << ans << endl;
    }
    return 0;
}

下面是大数取模的另一道练习,需要用到另一个公式,有兴趣和时间的读者可以see see
练习
到此大数取模告一段落。

那么,如果继续思考。虽然大数取模方法可以很快求出A的B次方的后几位数,但如果考虑算法的时间复杂度,假设我们求2的100次方,计算机则需要执行100次循环,分析得这个算法的时间复杂度为O(n)。当n不大时没有影响,但如果我们要求2的1000000000次方,显而易见,OJ会提示TLE(超时)

快速幂算法可以解决时间上的问题
前面已经分析出,朴素的的求幂算法时间复杂度之所以高,就是因为当n很大时,执行的循环操作次数也很大。由此得到了快速幂算法的核心思想:指数减半,底数平方。
先举个例子让大家熟悉一下快速幂算法:
如果我们要求3的10次方
3 ^ 10 = 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3
可将其转化为3的平方的5次方 (3 ^ 2 ^ 5)
即3 ^ 10 = (3 * 3) * (3 * 3) * (3 * 3) * (3 * 3) * (3 * 3)
即3 ^ 10 = (3 * 3) ^ 5
即3 ^ 10 = 9 ^ 5
此时指数由10变成了5(减半),而底数从3变成了9(平方) ,虽然结果没变,但原本求3 ^ 10需要执行10次操作,求9 ^ 5只需要执行5次操作,当指数极大时,如把指数从10000变成了5000,一下减少了5000次的操作,极大的减少了操作的次数,故提高了效率,降低了时间复杂度
但是快速幂算法中需要注意指数为奇数的问题
当指数是偶数时,由指数减半,底数平方,可以得到:
2 ^ 16 = 4 ^ 8 = 16 ^ 4 = 256 ^ 2 = 256 * 256 = 65536

但是,对于 9 ^ 5,5是一个奇数,5的一半是2.5,而指数不能为小数,因此不能直接执行5 / 2,故采取另一种方法表示9 ^ 5
即 9 ^ 5 =(9 ^ 4)* 9,然后对9 ^ 4继续执行指数减半,底数平方的操作即可,直至出现一个数的0次方,便可以计算出结果

下面给出快速幂模板

//快速幂,a为底数,n为指数
long long quick_power(long a,long n)
{
    long long ans = 1;
    while(n)
    {
        if (n % 2) ans *= a;//指数为奇数
        a = a * a;//底数平方
        n /= 2;//指数减半
    }
    return ans;
}

读者可以根据以上代码计算一下2的16次方加深板子的理解和记忆,当然也可将数据类型改为int

那么,当快速幂和大数取模相结合,就可以求出超过long long范围的数(long long 最大是2的63次方)的最后几位
例如:

以下代码可以快速求出2的1000000000次方的最后三位数字

#include <iostream>

using namespace std;

//快速幂与大数取模结合
long long quick_power(long long a,long long n)
{
    long long ans = 1;
    while(n)
    {
        if (n % 2) ans = ans * a % 1000;//奇数
        a = a * a % 1000;//底数平方的最后3位数字
        n /= 2;//指数减半
    }
    return ans;
}

int main()
{
    cout << quick_power(2,1000000000) << endl;
    return 0;
}

在这里插入图片描述

最后,可以使用位运算来优化快速幂
power%2==1可以用更快的位运算power&1来代替
理由如下:

  • **将一个数n与1的二进制做“与”运算,得到的就是n的二进制的最后一位的数字。**如:
    5的二进制为00000101
    6的二进制为00000110
    5是奇数,则5&1=1。6是偶数,则6&1=0。因此奇偶数的判断就可以用位运算来替换。

  • 如果一个数n为偶数,则其二进制表示的最后一位一定是0,且该偶数n&1=0

  • 如果一个数n是奇数,则其二进制表示的最后一位一定是1,且该奇数n&1=1

  • 总结:

  • 奇数&1=1

  • 偶数&1=0

最后,总结出大数取模 + 快速幂 + 位运算的最终优化代码

#include <iostream>

using namespace std;

long long quick_power(long long a,long long n)
{
    long long ans = 1;
    while(n)
    {
        if (n & 1) ans = ans * a % 1000;//奇数
        a = a * a % 1000;
        n >>= 1;//n >> 1可将原数减半,n >>= 1等价于n = n / 2
    }
    return ans;
}
int main()
{
    cout << quick_power(2,1000000000) << endl;
    return 0;
}

每次看见别人可以写下优秀的博客,都很羡慕,然后开始嫌弃自己又笨又懒。今天偶然又看见大佬分享的一篇博客,所以决定参考他的博客写一篇很详细的关于快速幂和大数取模的知识点。下面给出参考博客

  • 26
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值