一.引言
在说高精度加减乘除运算之前,我们先搞明白什么是高精度运算?
高精度算法,属于处理大数字的数学计算方法。在一般的科学计算中,会经常算到小数点后几百位或者更多,当然也可能是几千亿几百亿的大数字。一般这类数字我们统称为高精度数,高精度算法是用计算机对于超大数据的一种模拟加,减,乘,除,乘方,阶乘,开方等运算。对于非常庞大的数字无法在计算机中正常存储,于是,将这个数字拆开,拆成一位一位的,或者是四位四位的存储到一个数组中, 用一个数组去表示一个数字,这样这个数字就被称为是高精度数。高精度算法就是能处理高精度数各种运算的算法,但又因其特殊性,故从普通数的算法中分离,自成一家。
说白了就是有时候你可能遇到100位相乘的运算,但是这个结果显然超过int类型的范围,long long int类型同样远超,所以我们就需要其他的处理方法来实现,而高精度算法就此孕育而生。
二.高精度加法:(不考虑负数)
高精度加法的实现原理:
1.把一个大数一位一位的存储到一个int数组中,数组索引1代表着个位,索引2代表着十位,以此类推。例如:
数组序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
数字A | 8 | 7 | 4 | 7 | 6 | 4 | 3 | 4 | 5 | 1 |
数字B | 0 | 5 | 9 | 6 | 4 | 5 | 5 | 2 | 0 | 4 |
数字A对应着1543467478
同理B是4025546950
确保数组中的每个索引对应的值均为一位数
2.将两数相加:
数组序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
数字A | 8 | 7 | 4 | 7 | 6 | 4 | 3 | 4 | 5 | 1 | |
数字B | 0 | 5 | 9 | 6 | 4 | 5 | 5 | 2 | 0 | 4 | |
和 | 8 | 2 | 4 | 4 | 1 | 0 | 9 | 6 | 5 | 5 |
此例中结果没有到11位,若超过10位则将11位填上
3.输出结果:
从高位到低位依次输出。即逆序遍历一遍数组即可。
4.代码实现如下:
int add(int *a, int *b, int acnt, int bcnt) {//高精度加法,结果存储到a数组中,返回相加结果的位数
int cntt = acnt > bcnt ? acnt : bcnt;//确定加法所到的最高位
for (int i = 1; i <= cntt; i++) {
if (a[i] + b[i] >= 10) {//满10进位
a[i + 1]++;
a[i] = (a[i] + b[i]) % 10;//当前位则相加然后对10取模
}
else a[i] = a[i] + b[i];//不满10故直接相加
}
if (a[cntt + 1] != 0)return cntt + 1;//返回结果的位数
else return cntt;
不用担心一次循环之后会有某个位的数值为两位数,因为两个一位数相加结果最大值为18,再加上前一位的进位只达到19,故一次循环即可,不用再检查一遍
练习题请参考洛谷P1601
加上负数其实就是减法,减法如下。
三.高精度减法:
原理同加法类似,毕竟减法本来就是加上负数而已
注意两点:
1.相减后为负数:
其实这个也很好处理。如果最高位减下来要小于0,那肯定就为负数了。判断完成后只需要执行输出-(b-a)即可。因为-(b-a)=-b+a=a-b,而且b-a>0。
2.a或b为负数:
借助高精度加法即可。
4.代码实现如下:
int cmp(int *a, int *b, int acnt, int bcnt) {//比较a和b谁大
if (acnt > bcnt)return 1;
else if (acnt < bcnt)return -1;
else {
for (int i = acnt; i >= 1; i--) {//从最高位一直比到个位判断谁大
if (a[i] < b[i])return -1;
else if (a[i] > b[i])return 1;
else {
if (a[i] == b[i]) {
if (i == 1)return 0;
else continue;
}
}
}
}
}
int sub(int *a, int *b, int acnt, int bcnt,int aflag,int bflag) {//高精度减法,返回相减结果的位数
if (aflag == -1 && bflag == -1)return add(a, b, acnt, bcnt);
//两数均为负数,则相加在加负号,add函数为前面的高精度加法函数,add函数只进行正整数加法,输出时通过flag标志判断是否加负号
else if (aflag != -1 && bflag == -1)return add(a, b, acnt, bcnt);
//减一个负数结果是加上其绝对值
else if (aflag == -1 && bflag != -1)return sub(b, a, bcnt, acnt, bflag, -aflag);
//负数减正数则对调两数位置即变成b-a,记得把aflag变相反数防止无限递归
else if (cmp(a, b, acnt, bcnt) == 0)return 0;//两数相等,则值为0,表示结果没位数即为0
else if (cmp(a, b, acnt, bcnt) < 0) {//a<b,故调换两数位置
return sub(b, a, bcnt, acnt, bflag, aflag);
}
for (int i = 1; i <= acnt; i++) {//依次从个位开始相减
if (a[i] < b[i]) {//当前位减不了则借位再减
a[i + 1]--;
a[i] += 10;
a[i] = a[i] - b[i];
}
else {//当前位能减则直接减
a[i] = a[i] - b[i];
}
}
for (int i = acnt;; i--) {
if (a[i] != 0)return i;//返回结果的位数
}
}
练习题请参考洛谷P2142
四.高精度乘法:
高精度乘法原理:
由于我们是一位一位存储的,故适合的方法就是我们平时所列的式子算法,如下:
数A | 2 | 5 | ||
---|---|---|---|---|
数B | 3 | 8 | ||
数A的个位乘数B | 1 | 9 | 0 | |
数A的十位乘数B | 7 | 6 | 0 | |
相加为积 | 9 | 5 | 0 |
通过乘法分配率和结合律即可得25 * 38=5 * 38 + 20 *38。
代码实现如下:
代码只实现正数相乘,若有负数只需设置标志位,输出结果判断加入负号即可
int mul(int *a, int *b, int *mode, int acnt, int bcnt) {//高精度乘法
int t, cntt = acnt > bcnt ? acnt : bcnt;
for (int i = 1; i <= bcnt; i++) {//按照前面的原理依次展开乘法并相加的最后结果
for (int j = 1; j <= acnt; j++) {
if (i == 1) {//单独分开个位乘另一个数
if (mode[j] + a[j] * b[i] >= 10) {//mode数组已初始化,超过十则进位
t = (mode[j] + (a[j] * b[i])) / 10;
mode[j] = (mode[j] + (a[j] * b[i])) % 10;
mode[j + 1] += t;
}
else {
mode[j] += a[j] * b[i];
}
}
else {
mode[j + i - 1] += a[j] * b[i];
if (mode[j + i - 1] >= 10) {
t = mode[j + i - 1] / 10;
mode[j + i - 1] = mode[j + i - 1] % 10;
mode[j + i] += t;
}
}
}
}
for (int i = 1; i <= cntt * 2; i++) {//确保每个位上的数都是一位数
if (mode[i] >= 10) {
t = mode[i] / 10;
mode[i] = mode[i] % 10;
mode[i + 1] += t;
}
}
for (int i = 2 * cntt;; i--) {返回结果的位数
if (mode[i] != 0)return i;
}
}
因为m位数乘n位数的结果会小于2 * max(m,n),故最后计算位数的时候从2 * cntt开始
练习题请参考洛谷P1303
五.高精度除法:
高精度除法实现原理:
高精度除法这一块比较复杂,它可以分为两种情况:第一种情况:高精除以低精,实际上就是对被除的每一位,包括前面的余数都除以除数即利用0~9次的循环减法模拟除法的过程,并计数,从而得到商的值。如图:
此代码仅讨论正整数的相除
代码实现如下:
int div(int *a,int *c, int acnt) {//此处的A[1]代表数的最高位
int x = 0,b;
cin >> b;
for (int i = 1; i <= acnt; i++) {//按位相除
c[i] = (x * 10 + a[i]) / b;
x = (x * 10 + a[i]) % b;
}
int ccnt=1;
while (c[ccnt] == 0 && ccnt < acnt)ccnt++;
for (int i = ccnt; i <= acnt; i++)cout << c[i];
cout << endl;
return 0;
}
第二种情况:
高精度除以高精度,由于除法是乘法的逆过程,也就是说除法其实是求被除数有多少个除数相加,也就是说当被除数和除数都很大时,可以用减法代替除法达到计算的效果,但这不适合高精度除以低精度,因为容易超时。
代码实现如下:
#include<iostream>
using namespace std;
int a[100], b[100], c[100];
int cmp(int a[], int b[]){//比较a、b,若a>b为1;若a<b为-1;若a=b为0
int i;
if (a[0] > b[0])
return 1;
if (a[0] < b[0])
return -1;
for (i = a[0]; i > 0; i--){//从高位到低位比较
if (a[i] > b[i])
return 1;
if (a[i] < b[i])
return -1;
}
return 0;
}
void sub(int a[], int b[]){//计算a-b
int flag;
flag = compare(a, b);
if (flag == 0){//相等
a[0] = 0;
return;
}
if (flag == 1){//大于
for (int i = 1; i <= a[0]; i++){
if (a[i] < b[i]){//若不够向上借位
a[i + 1]--;
a[i] += 10;
}
a[i] -= b[i];
}
while (a[0] > 0 && a[a[0]] == 0)//删除前导0
a[0]--;
return;
}
}
int main(){
char str1[100], str2[100];
memset(a, 0, sizeof(a));
memset(b, 0, sizeof(b));
memset(c, 0, sizeof(c));
cin >> str1 >> str2;
a[0] = strlen(str1);//a[0]存储串1的位数
b[0] = strlen(str2);//b[0]存储串2的位数
for (int i = 1; i <= a[0]; i++)
a[i] = str1[a[0] - i] - '0';
for (int i = 1; i <= b[0]; i++)
b[i] = str2[b[0] - i] - '0';
int temp[100];
c[0] = a[0] - b[0] + 1;//m位数除以n位数结果是m-n位数,初始化结果的位数
for (int i = c[0]; i > 0; i--){
memset(temp, 0, sizeof(temp));
for (int j = 1; j <= b[0]; j++)//从i开始的地方,复制数组b到数组temp
temp[j + i - 1] = b[j];
temp[0] = b[0] + i - 1;
while (cmp(a, temp) >= 0){//用减法模拟
c[i]++;//成功减去一次则结果的当前位加一
sub(a, temp);
}
}
return 0;
}
由于整数除法会将小数点后面的全部截断,故在算才c[i]即结果的第i位时只需要知道b[i]到b[len]之间的数即可,至于b[i]到b[1]之间的数最后除出来的结果在小数位即可忽略。
事实上完全可以用高精度减法加上一个for循环无脑相减直到无法再减为止,只需记得每减一次答案加一并更新被减数的值即可。
至于除法的余数,很显然最后没减成功的一次所保留在a[ ]中的便是此次运算的余数。
总结:
通过以上加减乘除运算我们发现,高精度算法其实或多或少都是基于加减法的,同时负数加法又基于减法,减法又基于加法,乘法依靠加法,而除法则是依赖减法,至于没有讨论到的取模运算仍是依赖于除法和减法。而且你会发现事实上高精度乘法和除法的原理居然就是我们平时做乘法和除法的原理,只是我们平时被复杂的算式所蒙蔽,没看到其中的本质。
会发现我们以上讨论的都是10进制数的运算,那么作为收尾,我们来一道其他进制
题目快落快落。
洛谷P1604 B进制星球
题目背景 进制题目,而且还是个计算器~~ 题目描述
话说有一天,小Z乘坐宇宙飞船,飞到一个美丽的星球。因为历史的原因,科技在这个美丽的星球上并不很发达,星球上人们普遍采用B(2<=B<=36)进制计数。星球上的人们用美味的食物招待了小Z,作为回报,小Z希望送一个能够完成B进制加法的计算器给他们。
现在小Z希望你可以帮助他,编写实现B进制加法的程序。 输入输出格式 输入格式:共3行第1行:一个十进制的整数,表示进制B。第2-3行:每行一个B进制数正整数。数字的每一位属于{0,1,2,3,4,5,6,7,8,9,A,B……},每个数字长度<=2000位。
输出格式:一个B进制数,表示输入的两个数的和。 输入输出样例
输入样例#1: 复制 4 123 321
输出样例#1: 复制 1110
说明 进制计算器
题目链接
我们能发现这题其实和10进制加法区别不大,只是变成了逢B进位而不是逢10进位,储存方式和高精度加法一样,只是现在每个数组空间不仅仅只存储一位数,而是引入两位数,只是这个两位数需要保证小于B。
代码实现如下:
#include<iostream>
using namespace std;
int a[2010], b[2010], c[2010],y, x, ncnt, mcnt, z;
char n[2001], m[2001];
int main() {
cin >> z;
cin >> n; cin >> m;
ncnt = strlen(n); mcnt = strlen(m);
for (int i = 0; i < ncnt; i++)
if (z > 10 && n[i] >= 'A') a[ncnt - i] = n[i] - 'A' + 10;//字符串逆序存储
else a[ncnt - i] = n[i] - '0';//遇到字母转换成数字
for (int i = 0; i < mcnt; i++)
if (z > 10 && m[i] >= 'A') b[mcnt - i] = m[i] - 'A' + 10;
else b[mcnt - i] = m[i] - '0';
while (x <= ncnt || x <= mcnt) {//高精度加法
x++;//x是位数指针
c[x] = y + a[x] + b[x]; //y是进位
y = c[x] / z;
c[x] %= z;//逢z进1
}
while (c[x] == 0 && x > 1) x--; //去前导零
for (int i = x; i >= 1; i--) {
if (c[i] < 10) cout << c[i];
else cout << (char)(c[i] + 'A' - 10);
}
return 0;
}