洛谷 P1303 A*B Problem高精度乘法 详细题解 原理解释 代码注释
完整代码请扒拉到最底下↓。
解题要求:掌握基础的判断、循环、数组、字符串以及一些计算原理即可
“高精度”是怎么一回事?
简单来说,就是我们计算的数字太大太大了,导致已经超出了我们可以定义的范围,以至于我们不能用正常的定义来定义和计算数字,不然只能计算我们能所容纳的数字部分,无法达到要求。
比如longlong可以定义的范围在 -(2的63次方)到 2的63次方-1
如果此时数据过大,超过了longlong可以定义的范围,那么我们该如何计算它呢?这就需要我们运用到高精度的知识了。
题外话:Python没有这个限制,所以一行代码就解决了。
高精度乘法计算的原理
1.存储数据
你看既然我们数据太大,普通的定义都装不下了,那我们拿什么来装数据呢?没错,就是
数组
有同学可能会问:我们又不知道这个数有多长,我现在又不会用vector,(其实我现在也不会),那我们定义数组的时候应该定义多长的数组呢?
题目给出的数据范围是非负整数,且不超过10的2000次方,那么我们就定义两个长度为2000的数组,分别容纳两个乘数:
int anum[2000] = {0}, bnum[2000] = {0};
另外再定义一个可以容纳乘积的数组:
int pro[40000000] = {0};
接下来一步就是要输入数据了,但是这里有个坑,如果按照正常的顺序输入,算起来会非常麻烦,具体原因是什么呢?我们先看看我们是如何计算的
2.计算原理
其实计算原理我们早在小学就已经学过了,那就是
竖式运算
11
* 22
-------------
22
22
-------------
242
我们发现再计算的时候,下边的乘数的每一位会和上边的乘数每一位相乘,于是我们便利用两个for循环嵌套,外边的for循环对应下边的数,里边的for循环对应上边的数。
接下来就涉及乘积的每一位该怎么表示了:
1.将每一位相乘的结果存入第三个数组
2.错位相加,更新第三个数组对应的元素
3.如果相乘/相加结果大于10就需要存入结果个位的数
4.进位要保存一下,下一位相乘的时候就要加上,注意如果计算到最后一步也出现了进位,那么还要再多加一次循环,往多出来的一位存储进位
5.最终得到第三个数组排列的数值就是计算的结果。
但是这里如果仔细思考,就会发现有两个大麻烦:
1.决定每一位的数不仅仅有乘法,还有加法,所以要保存两次进位,一次是乘法进位,一次是加法进位
2.相加的时候是错位计算的,下标该如何对应?
第一个问题很好解决,定义两个进位即可,不过注意由于计算的顺序是:
乘法,更新位数->计算乘法进位->加法,再更新位数->计算加法进位
我在实现的时候将乘法和加法合并在一起算,直接一步到位,所以我的计算顺序是:
乘法,加法,更新位数->计算加法进位->计算乘法进位
(具体代码后边会一步一步解释)
但是这里在计算加法进位的时候,需要用到上一步未更新的数据和未更新的乘法进位(代码会解释的,别急),所以我把乘法进位的计算放在了后边,然而按照我的计算顺序,此时位数已经更新了,所以我还需要新建一个临时容器去存储未更新的数据。
那么第二个很关键的问题就是错位计算如何解决?
关于这个我还是选择了参考大佬,大佬给出的方法就是:下标相加
例如我定义的是
for (int i = 0;i < bl;i++)//下边的乘数
{
for (int j = 0; j <= al; j++)//上边的乘数
{
pro[?]=bnum[i] * anum[j]
}
}
那么怎么将相乘的结果错位相加呢?这里举个例子:
12
* 34
-------------
48
36
-------------
408
这里第一个乘数12中的“2”对应的就是anum[0],“1”对应的就是anum[1],同理第二个乘数34中“4”对应的就是bnum[0],“3”对应的就是bnum[1].
那么我们来看第一步计算2 * 4 = 8:
两个乘数分别为anum[0],bnum[0],乘积为pro[0]
第二步1 * 4 = 4:
两个乘数分别为anum[1],bnum[0],乘积为pro[1]
第三步2 * 3 = 6:
两个乘数分别为anum[0],bnum[1],乘积为pro[1]
第四步1 * 3 = 3:
两个乘数分别为anum[1],bnum[1],乘积为pro[2]
观察规律你就会发现乘积的下标正好等于两个乘数下标相加之和!这也就是实现错位相加的核心原理。
接下来就是核心的计算部分了:
首先定义我们所需要的各种东东
int anum[2000] = {0}, bnum[2000] = {0};
int pro[4000000] = {0};//乘积
int dig_mul = 0;//乘法进位
int dig_add = 0;//加法进位
int temp = 0;//临时存储当前位数的容器,防止更新数据后,后续计算使用的是更新后的数据
//嵌套循环准备好
for (int i = 0;i < bl;i++)//下边的乘数
{
for (int j = 0; j <= al; j++)//上边的乘数
{
temp = pro[i + j];//先将未更改的数据临时保存一下
}
}
然后就是计算位数,这里一步一步来:
bnum[i] * anum[j]//首先二者相乘
bnum[i] * anum[j] + dig_mul//相乘之后再加上上一步乘法的进位
(bnum[i] * anum[j] + dig_mul) % 10//模10就是除以10取余数,可以取得各位数的数字,现在得到的就是乘法结束后当前位数的数字
//此时乘法计算已经结束,下面继续计算加法
pro[i + j] + (bnum[i] * anum[j] + dig_mul) % 10//乘法计算结束后,加上上一次计算结束的结果
pro[i + j] + (bnum[i] * anum[j] + dig_mul) % 10 + dig_add//再加上上一次加法的进位
(pro[i + j] + (bnum[i] * anum[j] + dig_mul) % 10 + dig_add) % 10//最后整体模10,取的当前位数
//完整过程如下:
pro[i + j] = (pro[i + j] + (bnum[i] * anum[j] + dig_mul) % 10 + dig_add) % 10
接下来就是计算加法进位:
dig_add = (temp + (bnum[i] * anum[j] + dig_mul) % 10 + dig_add) / 10;//计算加法进位
计算乘法进位:
dig_mul = (bnum[i] * anum[j] + dig_mul) / 10;//计算乘法的进位
注意在一次循环完成后,需要将本次计算的进位都清零,不然会干扰下一次计算
完整如下:
int dig_mul = 0;//乘法进位
int dig_add = 0;//加法进位
int temp = 0;//临时存储当前位数的容器,防止更新数据后,后续计算使用的是更新后的数据
for (int i = 0;i < bl;i++)//下边的乘数
{
for (int j = 0; j <= al; j++)//上边的乘数
{
temp = pro[i + j];//先将未更改的数据临时保存一下
pro[i + j] = (pro[i + j] + (bnum[i] * anum[j] + dig_mul) % 10 + dig_add) % 10;//计算当前位的数字
dig_add = (temp + (bnum[i] * anum[j] + dig_mul) % 10 + dig_add) / 10;//计算加法进位
dig_mul = (bnum[i] * anum[j] + dig_mul) / 10;//计算乘法的进位
}
dig_mul = 0;//计算完一位后乘法进位清零
dig_add = 0;//计算完一位后加法进位清零
}
3.输入数据
细心的同学已经发现了问题:
我们输入数据是从高位往低位输入的,换句话说,如果按照输入顺序存储,那我们输入的两个数据是“最高位对齐”的:
123
123456
但是我们竖式计算的要求是“个位对其”:
000123
123456
如果按照输入顺序存储数据,这样对我们计算有着很大的影响,由于每次输入的数据长度不一,想把下标对其非常困难,那我们怎么解决这个问题呢?
聪明的我大佬想到了一种方法,就是将输入的数据倒序存入数组,这样一来,两个数的第一位不就对其了吗?
321000
654321
细心的同学似乎又注意到一点:短一点的数字后边是用0补齐的,为什么不直接空着呢?
我们回顾一下上边关于计算部分的代码,就发现在整个循环中,始终都是要访问anum[i]和bnum[i]这两个元素的,如果其中一个数比另一个数短,那么短的那个数计算完之后,也仍然会访问它剩下的元素,所以我们不能让它空着,就拿0过来填位置。
接下来先讲怎么输入数据
由于int、longlong这些都装不下了,我们又不能直接往数组里边存,所以需要一个中介来暂存一下数据,接下来就轮到伟大的字符串登场了:
//由于int、longlong无法容纳足够多的数字,只能用string来存储:
string a, b;
cin >> a >> b;
然后咱们就开始把string里的字符倒序存入数组
//获取两个字符串的长度:
int al = a.length();
int bl = b.length();
//倒序将两个数字存入一个int数组里
//目的是将两个数从个位对其
for (int i = 0; i < al; i++)
{
//字符串a是可以变成a[i]的,每一个元素就是对应的字符
//-'0'可以将数字字符转换为数字
anum[i] = a[al - i - 1]-'0';//利用al - i - 1实现倒序
}
for (int i = 0; i < bl; i++)
{
bnum[i] = b[bl - i - 1]-'0';
}
4.最后差不多就剩下输出结果了
由于我们是倒序存储的,所以输出的时候就需要倒序输出,这样输出来就是正序了。
不过注意我们还要把数据前边多余的0给删掉,之前我用的方法是设置一个检测系统,在for循环里不断判断是否遇到第一个非零数字,没有遇到就跳过这次循环。不过后来发现还有一种更简单的方法,给大家分享一下:
//若前边几位都是0,且直到最后一位之前没有找到非零数,则减少字符串的长度
//不判断到最后一位的原因是,如果计算结果为0,能保证输出计算结果
while (len > 0 && pro[len] == 0)
{
len--;
}
//因为我们是反向计算,所以最后需要反向输出,才能使数字为正序
for (int i = len; i >= 0 ; i--)
{
cout << pro[i];
}
至此高精度乘法计算就结束了,不过它并不是真正意义上的高精度乘法,因为它并没有涉及小数部分,或者负数,所以这只是一个半成品,有兴趣的小伙伴可以下去实现一下。
完整代码如下
#include <iostream>
using namespace std;
int anum[2000] = {0}, bnum[2000] = {0};
int pro[4000000] = {0};
int main() {
//由于int无法容纳足够多的数字,只能用string来存储
string a, b;
cin >> a >> b;
//获取两个字符串的长度:
int al = a.length();
int bl = b.length();
int len = al + bl;
//倒序将两个数字存入一个int数组里
//目的是将两个数从个位对其,才能计算
for (int i = 0; i < al; i++)
{
//字符串a是可以变成a[i]的,每一个元素就是对应的字符
//-'0'可以将数字字符转换为数字
anum[i] = a[al - i - 1]-'0';//利用al - i - 1实现倒序
}
for (int i = 0; i < bl; i++)
{
bnum[i] = b[bl - i - 1]-'0';
}
int dig_mul = 0;//乘法进位
int dig_add = 0;//加法进位
int temp = 0;//临时存储当前位数的容器,防止更新数据后,后续计算使用的是更新后的数据
for (int i = 0;i < bl;i++)//下边的乘数
{
for (int j = 0; j <= al; j++)//上边的乘数
{
temp = pro[i + j];//先将未更改的数据临时保存一下
pro[i + j] = (pro[i + j] + (bnum[i] * anum[j] + dig_mul) % 10 + dig_add) % 10;//计算当前位的数字
dig_add = (temp + (bnum[i] * anum[j] + dig_mul) % 10 + dig_add) / 10;//计算加法进位
dig_mul = (bnum[i] * anum[j] + dig_mul) / 10;//计算乘法的进位
}
dig_mul = 0;//计算完一位后乘法进位清零
dig_add = 0;//计算完一位后加法进位清零
}
//若前边几位都是0,且直到最后一位之前没有找到非零数,则减少字符串的长度
//不判断到最后一位的原因是,如果计算结果为0,能保证输出计算结果
while (len > 0 && pro[len] == 0)
{
len--;
}
//因为我们是反向计算,所以最后需要反向输出,才能使数字为正序
for (int i = len; i >= 0 ; i--)
{
cout << pro[i];
}
return 0;
}

浙公网安备 33010602011771号