引言
最近在刷题的时候偶然见到这样一个题目,见下图
大致的意思是,让我们计算a的b次方取模p的结果,再我了解了关于快速幂的内容之后,很快便解决了这道题,每次乘完a后取模最后就可以得到结果。但是在这之后,我又碰到了这样一道题,见下图
这个魔性的题目竟然让我求后五百位?这种题用C语言所给的基础内置类型就行不通了 ,可能聪明的你会让人想到高精度,高精+和-我在之前博客中已经讲过,这道题是要求掌握高精度的乘法才能进一步将此题求解,我在求解完这道题后,便想将之前遗留的坑填上,便写了此篇博客。
快速幂
但是看完题先别急,想让我们看看什么是快速幂。
1.a的平方是a^2,a^2的平方是a^4,以此类推a^8,a^16,a^32。。。
2.a^x * a^y = a^(x+y) 这个也好理解
3.如果我们将b(幂的大小)写成二进制呢?
假设b⑩ = 13 那么b② = 1101
由此我们便可以将a^13,转化成a^8 * a^4 * a^0,而所乘的位数刚好与二进制的一相对应
也就是说,我们可以先自增一个幂次方,每当b出现一位等于1时,向结果乘上一个事先自 增好的幂次方 当遍历完所有b二进制中1的位,便能得到最终的结果。
如果上面的话还没理解,可以结合下面的代码辅助理解,大体的意思就是,不需要一个一个往上乘a,根据幂的二进制直接乘a的更高幂次,减少计算。
while (b > 0) {//代码中用到了&(按位与)和>>(移位符) if (b & 1) { //&的作用是比对两个数二进制的每一位,同为1为1,存在0则0 //b = 1 1 0 1 //1 = 0 0 0 1 //result 0 0 0 1 ans = ans * a;//当b&1为真时乘上事先准备的幂次方a,存入结果中 } a = a * a; b = b >> 1; //将二进制位同时向右移动一位,高位补符号位 //b = 1 1 0 1 //b>>1 = 0 1 1 0 //高位补符号位,详情可以去专门看一下我之前的博客或者别人的讲解 //b = b>>1 将结果赋给b }
这样算下来,最终存在ans的结果便是a的b次方了
总的来说,如果 b 在二进制上的某一位是 1,我们就把答案乘上对应的 a^2^n。如果还是不太理解可以看看下面的代码实现,也是第一道题的答案
#include<stdio.h> int main() { long long int a, b, p, ans = 1; scanf("%lld%lld%lld", &a, &b, &p); long long int ar = a, br = b; while (b > 0) { if (b & 1) { ans = ans * a % p;//每次注意取模防止数字过大内置类型存不下 } a = a * a % p; b = b >> 1; } ans = ans % p; printf("%lld^%lld mod %lld=%lld", ar, br, p, ans); return 0; }
看完代码后,是不是就清晰多了?
但我们要考虑一件事,指数的增长速度那么快,用内置类型肯定,放不下,我们用高精度如何?
高精度乘
在了解这种乘法之前,我们可以回忆一下咱们小学的时候是怎样学习乘法运算的。
举个例子——12*19
怎么理解呢?我们可以在这个式子中找找规律,比如右边个位的2和9都是在第0位(由于将数字放入数组中时是从第0位放起),它们相乘也是从第0位开始,19的1和12的2一个是第1位一个是第0位,它们相乘的结果便从第一位开始,而19的1和12的1两个都是第1位,它们在算式计算相加的时候便从第2位也就是百位开始。以上反应的规律总结以下就是
for (int q = 0; q < la; q++) { for (int p = 0; p < lb; p++) { sum[q + p] += a[q] * b[p]; } } for(int q=0;q<la>lb?la + 3:lb + 3;q++){ sum[q + p + 1] += sum[q + p] / 10; sum[q + p] %= 10; }
在运用高精乘的过程中,也需要注意关于输入过大放不下的问题,这时候就需要用到字符串的处理,可以看看下方我专门写的一份用于高精乘的代码。
#include <stdio.h> #include <string.h> int main() { char a0[5008]; char b0[5008]; int a[5008] = { 0 }; int b[5008] = { 0 }; int sum[5008] = { 0 }; int i = 0; scanf("%s", a0); scanf("%s", b0); int v = 0; for (v = 5001; a0[v] != '\0'; v--); v--; for (int c = 0; v >= 0; c++, v--) { a[c] = a0[v] - '0'; } for (v = 5002; b0[v] != '\0'; v--); v--; for (int c = 0; v >= 0; c++, v--) { b[c] = b0[v] - '0'; } //以上代码将字符串转为数字放入数组 int la = strlen(a0); int lb = strlen(b0); int lc = la + lb; for (int q = 0; q < la; q++) { for (int p = 0; p < lb; p++) { sum[q + p] += a[q] * b[p]; } } for(int q=0;q<la>lb?la + 3:lb + 3;q++){ sum[q + p + 1] += sum[q + p] / 10; sum[q + p] %= 10; } //以上代码进行高精乘 int j = 0; for (j = 5005; sum[j] == 0; j--); for (; j >= 0; j--) printf("%d", sum[j]); //以上代码打印结果 return 0; }
当快速幂遇见高精乘
这时候再将开始那道题搬运下来看看
这道题说白了最后要求求后500位其实是之前两者知识点的结合体,不过先讲讲第一问,问你位数,由于2^P-1的结果与2^P是相同的,所以可以列方程
m刚刚好就是最终位数,在math.h中有计算lg的函数我们直接调用即可。
最终代码放到下面,配上注释
#include<stdio.h> #include<math.h> int arr[505];//充当计算2的幂次方的一个过程 int sign[505];//存放2的幂次方 int res[505];//充当最终结果的一个过程 int now[505];//存放最终结果 int main() { int P; int ws; int i, j; scanf("%d", &P); ws = P * log10(2) + 1; sign[0] = 2; res[0] = 0; now[0] = 1; while (P > 0) { if (P & 1) {//当P&1为真,结果乘上2的幂次方 for (i = 0; i <= 500; i++) { for (j = 0; j <= 500; j++) { if (i + j <= 500)//防止计算越界 res[i + j] += now[i] * sign[j]; } } for (i = 0; i <= 500; i++) { if (res[i] > 9) { res[i + 1] += res[i] / 10; res[i] %= 10; } } for (i = 0; i <= 500; i++) { now[i] = res[i]; res[i] = 0;//用完res后要清零 } } //下面是2的幂次方的自乘和自增 for (i = 0; i <= 500; i++) { for (j = 0; j <= 500; j++) { if (i + j <= 500)//防止计算越界 arr[i + j] += sign[i] * sign[j]; } } for (i = 0; i <= 500; i++) { if (arr[i] > 9) { arr[i + 1] += arr[i] / 10; arr[i] %= 10; } } for (i = 0; i <= 500; i++) { sign[i] = arr[i]; arr[i] = 0;//用完arr计算2的幂次方后清零 } P >>= 1;//二进制P右移一位 } now[0]--;//最终结果减一 printf("%d\n", ws); for (i = 499,j=0; i >= 0; i--) {//华丽打印结果 printf("%d", now[i]); j++; if (j >= 50) { printf("\n"); j = 0; } } return 0; }
结语
以上便是本次快速幂以及高精度乘法的所有内容,如果感觉对你有帮助的话,还请点个小小的赞,留下关注收藏再走啊!比心-----♥