高精度运算
在 C++ 中,64 位无符号整型,即unsigned long long最大只能表示到 2 64 2^{64} 264-1=18446744073709551615。
而有些题目可能需要用到更大的整数类型
比如我们做过的 a + b 问题,给你两个数,输出它们的和,非常简单
可如果 a,b 的范围超出了unsigned long long所表示的范围那该怎么做呢?
对于非常大范围数据的运算,其实我们可以通过数组来模拟实现高精度的运算
在学习如何对高精度的数进行运算之前,我们首先来看一看,应该如何读取大整数。
高精度的读入
我们可以用一个数组来保存整个高精度整数,并记录高精度整数的长度len。为了便于之后的计算,我们都会在读入的时候把这个整数倒置。也就是说 a[0] 保存个位数字,a[1] 保存十位数字……,这样做是为了方便后续的运算。
string num;
cin >> num;
int a[105];
int len = num.size();
for (int i = 0; i < len; i++) {
a[i] = num[len - 1 - i] - '0'; // 注意这里要减去 '0',因为我们要把字符 '0' 变为整数 0
}
高精度的输出
输出一个高精度的数就非常容易了,直接把数组中的元素倒序输出就可以啦。
for (int i = len - 1; i >= 0; i--) {
cout << a[i];
}
cout << endl;
高精度加单精度
什么是单精度整数?单精度整数就是一个与高精度整数对应的概念,表示可以用一个整型(int或long long)变量存储的整数。
如何进行计算呢?我们回顾一下竖式加法:
2 3 1 1 9
+ 1
-------------
2 3 1 1 10
-------------
2 3 1 2 0
怎样计算呢?首先,我们要把加的数和高精度整数的个位对齐。之后,我们让小整数和高精度整数的个位相加,最后再处理进位操作就可以了。
接下来,我们看一个更为特殊的例子:
2 3 1 1 9
+ 99
-----------------
2 3 1 1 108
-----------------
2 3 1 11 8
-----------------
2 3 2 1 8
我们总结一下计算过程:
将要加的数加到高精度数组下标为0的位置上,让下一位数加上当前位上的数除以 10 的商,当前位上的数对10取模,如果下一位数经过上一步计算结果后大于等于10,则继续执行上述操作。
高精度加单精度的关键代码如下:
int a[105], x, len;
// 将高精度读入到 a,倒序放置
// 高精度的位数记为 len
// 将单精度数读入到 x
a[0] += x;
for (int i = 0; i < len; i++) {
a[i + 1] += a[i] / 10;
a[i] %= 10;
}
对每一位进行完进位操作之后,还需要判断a[len]是否大于0。如果大于0,就需要再继续考虑进位操作并将长度加1。
while (a[len]) {
a[len + 1] += a[len] / 10;
a[len] %= 10;
len++;
}
这样,就完成了高精度与单精度的加法运算,它的完整代码如下:
#include <iostream>
#include <string>
using namespace std;
string num;
int x, len;
int a[105];
int main() {
cin >> num;
cin >> x;
len = num.size();
for (int i = 0; i < len; i++) {
a[i] = num[len - 1 - i] - '0';
}
a[0] += x;
for (int i = 0; i < len; i++) {
a[i + 1] += a[i] / 10;
a[i] %= 10;
}
while (a[len]) {
a[len + 1] += a[len] / 10;
a[len] %= 10;
len++;
}
for (int i = len - 1; i >= 0; i--) {
cout << a[i];
}
cout << endl;
return 0;
}
高精度减单精度
学完高精度加单精度,那么,要如何实现高精度与单精度的减法操作呢?
首先,我们来看一个高精度的整数要如何和一个单精度的整数进行减法操作。当然,我们在这一节里先不考虑高精度整数比单精度整数小的情况,也就是说结果不会是负数。
我们还是从竖式减法入手,看一看应该如何进行高精度的减法操作。
5 9 2 3 4
- 9
-------------
5 9 2 3 -5
-------------
5 9 2 2 5
高精度减单精度的过程和高精度加单精度其实是非常类似的。我们来看一下计算过程:
在高精度数组下标为 0 的位置减去单精度整数,如果当前位上的数小于0,就让当前指向的下一位减去1,并让当前位加上10,直到当前位上的数不小于0
然后,向前移动一位,继续执行上述操作
除了上述过程和加法有所差异以外,最后的长度处理也与加法有所不同。进行相减操作以后,高精度整数的总长度有可能会变小,所以需要判断是否当前高精度整数的最高位已经是0,如果是0 则需要让总长度减去1。要注意如果减完结果就是0了,我们保留这个唯一的0。
核心代码如下:
a[0] -= x;
for (int i = 0; i < len; i++) {
while (a[i] < 0) {
a[i + 1]--;
a[i] += 10;
}
}
while (len > 1 && a[len - 1] == 0) {
len--;
}
完整代码如下:
#include <iostream>
#include <string>
using namespace std;
string num;
int x, len;
int a[105];
int main() {
cin >> num;
cin >> x;
len = num.size();
for (int i = 0; i < len; i++) {
a[i] = num[len - 1 - i] - '0';
}
a[0] -= x;
for (int i = 0; i < len; i++) {
while (a[i] < 0) {
a[i + 1]--;
a[i] += 10;
}
}
while (len > 1 && a[len - 1] == 0) {
len--;
}
for (int i = len - 1; i >= 0; i--) {
cout << a[i];
}
cout << endl;
return 0;
}
高精度加高精度
前面我们学习了如何计算高精度整数加上单精度整数的结果。接下来,我们来看一看如何计算高精度整数加上高精度整数的运算结果。
我们回忆一下加法的竖式运算:
8 3 2 5 1
+ 4 2 7 9
------------
8 7 5 3 0
我们是怎样一步步计算的呢?是不是首先计算右数第一位的两个数的和,然后处理进位;然后计算进位和第二位两个数的和,然后处理进位……直到处理完最高位,计算结束。
接下来,我们试着用一种和之前不太一样的方法来在竖式上计算加法。
首先,将两个数的最低位对齐,并计算每一位上两个数的和。
8 3 2 5 1
+ 4 2 7 9
----------------
8 7 4 12 10
之后,从第一位开始,依次处理进位。在这里,我们处理进位的方法和高精度加上单精度没有任何区别。
8 3 2 5 1
+) 4 2 7 9
------------
8 7 5 3 0
这样,就完成了高精度加上高精度的计算。
和高精度加单精度相比,唯一的不同就是求和的过程。我们这里不再是只让储存高精度的数组下标为 0 的位置加上一个x了,而是让每一位都加上另外一个高精度对应位置上的值,然后再处理进位。
int a1[105], len1, a2[105], len2;
// 分别读入两个高精度数,第一个数保存在 a1 里,长度为 len1,第二个数保存在 a2 里,长度为 len2
len1 = max(len1, len2);
for (int i = 0; i < len1; i++) {
a1[i] += a2[i];
}
for (int i = 0; i < len1; i++) {
a1[i + 1] += a1[i] / 10;
a1[i] %= 10;
}
while (a1[len1]) {
a1[len1 + 1] += a1[len1] / 10;
a1[len1] %= 10;
len1++;
}
完整代码如下:
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
string num1, num2;
int a1[105], a2[105], len1, len2;
int main() {
cin >> num1 >> num2;
len1 = num1.size();
for (int i = 0; i < len1; i++) {
a1[i] = num1[len1 - 1 - i] - '0';
}
len2 = num2.size();
for (int i = 0; i < len2; i++) {
a2[i] = num2[len2 - 1 - i] - '0';
}
len1 = max(len1, len2);
for (int i = 0; i < len1; i++) {
a1[i] += a2[i];
}
for (int i = 0; i < len1; i++) {
a1[i + 1] += a1[i] / 10;
a1[i] %= 10;
}
while (a1[len1]) {
a1[len1 + 1] += a1[len1] / 10;
a1[len1] %= 10;
len1++;
}
for (int i = len1 - 1; i >= 0; i--) {
cout << a1[i];
}
cout << endl;
return 0;
}
高精度减高精度
刚才我们学习了如何计算一个高精度整数和单精度整数的减法。接下来,我们看看要如何计算一个高精度整数和一个高精度整数的减法操作。
首先有一个高精度减法的特殊情况:如果第一个数小于第二个数,我们要怎么去计算它们两个的差呢?(在高精度减单精度时我们没考虑这个问题)
我们可以在结果的最前面加上一个负号,并交换这两个数,这样最终结算的结果就和我们期望的一致了。
例如,我们要计算 2-5 的结果,应该为-3,那么我们首先写出一个负号,然后计算5-2的值并输出-3。
比较两个高精度数大小可以先比较两个数的位数,位数不同时位数少的小,位数相同时可以从最高位开始比较,遇到某一位不同,比较小的就小,此时其实比较的也是两个数本身的字典序,字典序小的就小。
这样,我们就可以把问题简化了。现在我们需要解决的是,如何计算两个高精度整数的减法结果,保证前一个数不小于后一个数。
和加法一样,我们继续用竖式来分析这个过程:
5 2 3 1 4
- 9 7 6 8
------------
4 2 5 4 6
我们参考前面高精度减单精度的方法,首先,让两个高精度整数的最低位对齐,作差,然后,从最低位开始,如果发现当前位上的数小于0,就让当前位加上10,更高的一位减去1。
5 2 3 1 4
- 9 7 6 8
---------------------
5 -7 -4 -5 -4
---------------------
5 -7 -4 -6 6
---------------------
5 -7 -5 4 6
---------------------
5 -8 5 4 6
---------------------
4 2 5 4 6
通过刚才这个例子,我们基本对减法的借位处理有了一个初步的印象。其实,借位过程跟我们前面实现的高精度减单精度的借位处理过程是完全一样的。
不同之处在于,我们需要先对两个数的每一位进行减法操作,并且在最后由于长度的减少值可能不止1,所以需要不断地判断最高位是否已经被减到0。
参考代码如下:
bool sgn = false; // 如果结果是负数,sgn 为 true
bool cmp(string num1, string num2) {
if (num1.size() != num2.size()) {
return num1.size() < num2.size();
}
return num1 < num2;
}
if (cmp(num1, num2)) {
sgn = true;
swap(num1, num2);
}
for (int i = 0; i < len1; i++) {
a1[i] -= a2[i];
}
for (int i = 0; i < len1; i++) {
while (a1[i] < 0) {
a1[i + 1]--;
a1[i] += 10;
}
}
while (len1 > 1 && a1[len1 - 1] == 0) {
len1--;
}
if (sgn) {
cout << "-";
}
for (int i = len1 - 1; i >= 0; i--) {
cout << a1[i];
}
cout << endl;
完整代码如下:
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
string num1, num2;
int a1[105], a2[105], len1, len2;
bool sgn;
bool cmp(string a, string b) {
if (a.size() != b.size()) {
return a.size() < b.size();
}
return a < b;
}
int main() {
cin >> num1 >> num2;
if (cmp(num1, num2)) {
sgn = true;
swap(num1, num2);
}
len1 = num1.size();
for (int i = 0; i < len1; i++) {
a1[i] = num1[len1 - 1 - i] - '0';
}
len2 = num2.size();
for (int i = 0; i < len2; i++) {
a2[i] = num2[len2 - 1 - i] - '0';
}
for (int i = 0; i < len1; i++) {
a1[i] -= a2[i];
}
for (int i = 0; i < len1; i++) {
while (a1[i] < 0) {
a1[i + 1]--;
a1[i] += 10;
}
}
while (len1 > 1 && a1[len1 - 1] == 0) {
len1--;
}
if (sgn) {
cout << "-";
}
for (int i = len1 - 1; i >= 0; i--) {
cout << a1[i];
}
cout << endl;
return 0;
}
高精度加减法的总结
高精度加法
(1)注意需要处理高位进位的问题
(2)要用 while 进行高位处理
高精度减法
高精度减法需要处理的问题比加法更多一些
(1)若是小数减大数的运算,我们一般会考虑将两个数调换位置,进行大数减小数的运算,当然最后不要忘记输出负号
(2)跟加法的进位一样,存在退位的问题
(3)跟加法的高位进位一样,存在高位的 0 也就是前导零的问题,减法要注意可能会出现高位多个 0 的情况,例如 1000-999=0001
高精度乘单精度
前面我们已经学习了如何对高精度整数进行加法和减法运算。接下来,我们来学习一个相比于之前复杂一些的运算——乘法运算。
为了便于理解,我们还是先从高精度乘以单精度入手,看一看它的计算过程。
5 2 4
x 3
--------------
15 6 12
--------------
15 7 2
--------------
1 5 7 2
我们总结一下高精度乘以单精度的计算过程:
(1)将单精度整数乘到高精度整数的每一位上
(2)从最低位开始,依次向前处理进位,这个过程和加法的一致
(3)处理最终结果长度增加的情况,这个过程也和加法的一致
核心代码如下:
// 第一步,计算每一位的乘积
for (int i = 0; i < len; i++) {
a[i] *= x;
}
// 第二步,处理进位
for (int i = 0; i < len; i++) {
a[i + 1] += a[i] / 10;
a[i] %= 10;
}
// 第三步,处理长度增加的情况
while (a[len]) {
a[len + 1] += a[len] / 10;
a[len] %= 10;
len++;
}
完整代码如下:
#include <iostream>
#include <string>
using namespace std;
string num;
int x, len;
int a[105];
int main() {
cin >> num;
cin >> x;
len = num.size();
for (int i = 0; i < len; i++) {
a[i] = num[len - 1 - i] - '0';
}
for (int i = len - 1; i >= 0; i--) {
cout << a[i];
}
cout << endl;
return 0;
}
高精度乘高精度
我们仍然将整个过程拆解为 “计算乘积” 和 “进位” 两部分。首先,我们计算第二个高精度整数的最后一位和第一个高精度整数每一位的乘积,放在对应的位置上
2 5 3
x 6 7
------------------
14 35 21
接下来,我们再计算第二个高精度整数的倒数第二位和第一个高精度整数每一位的乘积,这里需要注意,我们要把这些乘积放置的位置相比于刚才统一向左移动一位,然后,计算每一个位置上所有乘积的和,再依次处理进位和长度增加的情况:
2 5 3
x 6 7
------------------
14 35 21
12 30 18
------------------
12 44 53 21
------------------
1 6 9 5 1
容易发现,两个下标分别为 i、j 位置的数,他们的乘积是加到答案中下标为 i + j 的位置。
对于进位和长度增加的处理,与高精度乘以单精度完全一致,无需任何调整。
参考代码如下:
for (int i = 0; i < len1; i++) {
for (int j = 0; j < len2; j++) {
a[i + j] += a1[i] * a2[j];
}
}
//长度为len1和len2的两个数相乘,如果允许每一位上超过10的话,结果为len1 + len2位
len = len1 + len2 - 1;
for (int i = 0; i < len; i++) {
a[i + 1] += a[i] / 10;
a[i] %= 10;
}
while (a[len]) {
a[len + 1] += a[len] / 10;
a[len] %= 10;
len++;
}
高精度乘高精度的完整代码实现如下:
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
string num1, num2;
int a1[105], a2[105], len1, len2, a[205], len;
int main() {
cin >> num1 >> num2;
len1 = num1.size();
for (int i = 0; i < len1; i++) {
a1[i] = num1[len1 - 1 - i] - '0';
}
len2 = num2.size();
for (int i = 0; i < len2; i++) {
a2[i] = num2[len2 - 1 - i] - '0';
}
for (int i = 0; i < len1; i++) {
for (int j = 0; j < len2; j++) {
a[i + j] += a1[i] * a2[j];
}
}
len = len1 + len2 - 1;
for (int i = 0; i < len; i++) {
a[i + 1] += a[i] / 10;
a[i] %= 10;
}
while (a[len]) {
a[len + 1] += a[len] / 10;
a[len] %= 10;
len++;
}
for (int i = len - 1; i >= 0; i--) {
cout << a[i];
}
cout << endl;
return 0;
}
高精度除单精度
对于除法,我们不用将数据反向存入数组中,应为在进行除法运算时,我们首先计算的高位。以下是除法运算的模拟运算:
0 1 2
------------------
21 )2 5 3
0
------------------
2 5
2 1
------------------
4 3
4 2
------------------
1
从上面我们可以看出,对于被除数的每一位,都要对应商的一位,商多少是由上一步计算的余数乘10加上本次要计算的数再整除除数(对于单精度除数我们将其看成一个整体,而不是分开看每一位)决定,直到被除数的每一位都参加过计算为止。
最后需要注意的是,商可能有包含多个前导0,我们是需要将其删掉的,但是,如果商的结果全是0,我们需要保留最后一个0。
参考代码如下:
#include<iostream>
#include<string>
using namespace std;
int a[1005],ans[1005];//a是用于装被除数的数组 ,ans用于装商
int main(){
string num;
int x;
cin>>num;//被除数
cin>>x;//除数
int numlen=num.size();
for(int i=0;i<numlen;i++){//除法不用反向存储
a[i+1]=num[i]-'0';
}
int mod=0;//上一步除法的余数
for(int i=1;i<=numlen;i++){//模拟除法
ans[i]=(mod*10+a[i])/x;
mod=(mod*10+a[i])%x;
}
int index=1;//表示商的非0的开始位置
while(ans[index]==0&&index<numlen){
index++;
}
for(int i=index;i<=numlen;i++){
cout<<ans[i];//输出商
}
cout<<"............."<<mod<<endl;//输出余数
cout<<endl;
return 0;
}