洛谷 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;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值