1 什么是高精度?
1.1 为什么用高精度
我们已经熟悉了多种数据类型,每种类型都有其能容纳的数字范围限制。对于一般需求,int
或 long long
类型通常足够使用。然而,在某些极端场景下,比如一位无聊的科学家想要数清楚大海中水分子的总数(???),这样的数值远远超出了 long long
类型的存储范围。这时,我们就需要借助高精度计算来处理这些大规模的数据。
1.2 高精度基本思路
为了应对这种挑战,我们采用了一种巧妙的方法:使用数组来模拟非常长的整数。具体来说,我们可以用数组的每一位来记录这个大数中的每一位数字。例如,一个 n 位的大数就可以用一个包含 n 个元素的数组来表示。
高精度的核心思想就是利用这种数组结构来模拟各种数学运算法则,如加法、减法、乘法等,从而得到精确的运算结果。
2 高精度加法
2.1 思路
因为高精度的本质就是模拟运算,所以我们先想一想小学时怎么学的加法,以912+728为例。
我们从两个数的个位开始,逐位相加,并在必要时向高位进位(满十进一)。在高精度加法中,我们将这个过程扩展到任意长度的数字上,使用数组来存储每一位的结果和进位。
我们来梳理一下思路:先令进位t=0(个位还没有进位),从个位起逐位相加,该位保留的数字为和的个位数,进位取十位数(两数相加最多达到十位,这里可以t=1,或者直接除以10)。到最后一位时,要判断一下最后有没有进位。
2.2 高精度加法模板
#include <bits/stdc++.h>
using namespace std;
vector<int> add(vector<int> a, vector<int> b)
{
vector<int> v; // 存储结果的数组
int t = 0; // 进位
for (int i = 0; i < (int)a.size() || i < (int)b.size(); i++)
{
if (i < (int)a.size())
t += a[i];
if (i < (int)b.size())
t += b[i];
v.push_back(t % 10); // 保存当前位的个位数
t /= 10; // 处理进位
}
if (t == 1) // 如果最高位还有进位
v.push_back(1);
reverse(v.begin(), v.end()); // 由于我们是从低位到高位计算的,因此需要反转结果数组
return v;
}
int main()
{
vector<int> a, b, v;
string A, B; // 用于输入的大整数
cin >> A >> B;
// 倒序读入大整数到数组中
for (int i = A.size() - 1; i >= 0; i--)
a.push_back(A[i] - '0');
for (int i = B.size() - 1; i >= 0; i--)
b.push_back(B[i] - '0');
v = add(a, b);
// 由于我们在计算时反转了数组,这里直接输出即可
for (int num : v)
cout << num;
cout << endl; // 输出换行
return 0;
}
2.3 注意事项
(1)倒序读入:
由于数组是从低地址向高地址存储的,而我们在计算时是从低位向高位进行的,所以需要倒序读入大数。
(2)字符串转数组:
当从字符串读入大数时,需要将字符转换为对应的整数,这通常通过减去字符 '0' 的 ASCII 值来实现。
(3)进位处理:
在逐位相加的过程中,需要注意进位的处理,确保每一位的和都是正确的。
(4)结果输出:
由于结果是倒序存储的,所以在输出时需要从最高位开始输出。
3 高精度减法
3.1 思路
继续想一想小学时我们是怎么计算减法的。
我们学习减法时,总是从个位数开始逐位相减。当某一位相减的结果小于0时,我们会向前一位“借”1,并在当前位加上10,以保持计算的正确性。这种思路同样适用于高精度减法,即处理非常大的整数相减的情况。
首先,我们设定一个进位变量t
为0,用于处理借位情况。接着,我们逐位计算当前位的差值,注意这里默认是较大数减去较小数。如果差值小于0,说明需要向前一位借位,此时将t
设为1(表示借位),并在当前位加上10。若遇到较小数减较大数的情况,可以先按较大数减较小数的方式计算(结果前加负号),最后输出结果时再取反(两个负号相互抵消)。最后,别忘了去除结果可能存在的前导0。
3.2 高精度减法模板
#include <bits/stdc++.h>
using namespace std;
// 比较两个大数,确保大数减小数
bool cmp(vector<int> &A, vector<int> &B)
{
if (A.size() != B.size())
return A.size() > B.size();
for (int i = A.size() - 1; i >= 0; i--)
{
if (A[i] != B[i])
return A[i] > B[i];
}
return true; // 如果完全相等,默认A不小于B
}
// 执行高精度减法
vector<int> subtract(vector<int> &a, vector<int> &b)
{
vector<int> result;
int t = 0; // 借位
for (int i = 0; i < (int)a.size(); i++)
{
t = a[i] - t;
if (i < (int)b.size())
t -= b[i];
if (t < 0)
{
result.push_back(t + 10); // 借位后加10
t = 1; // 设置借位标志
}
else
{
result.push_back(t);
t = 0; // 清除借位标志
}
}
// 去除结果中的前导0
while (!result.empty() && result.back() == 0)
result.pop_back();
// 因为我们是从低位到高位计算的,所以需要反转结果
reverse(result.begin(), result.end());
return result;
}
int main()
{
string num1, num2;
cin >> num1 >> num2;
vector<int> A, B;
// 将字符串转换为逆序整数数组
for (int i = (int)num1.size() - 1; i >= 0; i--)
A.push_back(num1[i] - '0');
for (int i = (int)num2.size() - 1; i >= 0; i--)
B.push_back(num2[i] - '0');
vector<int> result;
if (cmp(A, B))
{
result = subtract(A, B);
}
else
{
cout << '-'; // 输出负号
result = subtract(B, A); // 交换顺序进行减法
}
// 输出结果
for (int num : result)
cout << num;
cout << endl; // 加上换行符,增强输出可读性
return 0;
}
3.3 注意事项
(1)去除前导0:
在计算完成后,结果可能包含前导0,这些0在表示数字时是不必要的,因此需要在输出结果前去除。
(2)逆序处理:
由于我们是从字符串的低位(即字符串的末尾)开始处理,所以得到的结果是逆序的,需要在输出前进行反转。
(3)借位处理:
在处理减法时,需要注意借位情况,确保当前位的计算正确。当前位小于0时,需要向前一位借1,并在当前位加10。
(4)输出负号:
如果较小数减去较大数,需要在结果前添加负号,并在计算时交换两个数的位置。
4 高精度乘法
4.1 大整数乘小数
在这里,我们所说的“小数”并非传统意义上的含有小数点的数,而是指那些数值较小,可以直接使用基本整型变量(如int
)存储的整数。这一节将讨论如何实现一个大整数(由于数值过大,需要使用数组存储其每一位)与这样一个小数的乘法运算。
4.1.1 思路
与高精度加法类似,高精度乘法同样需要逐位处理。但不同的是,乘法运算中,每一位的乘积都可能产生进位,且进位的值可能大于1(在加法中,进位只能是0或1)。因此,在乘法运算中,我们需要额外注意处理这些进位。
4.1.2 高精度乘法(大整数乘小数)模板
#include <bits/stdc++.h>
using namespace std;
// 函数:将大整数(以vector<int>形式存储)与一个小数相乘
vector<int> multiply(vector<int> &a, int b)
{
vector<int> result; // 用于存储结果的vector
int t = 0; // 进位
// 逐位相乘,并处理进位
for (int i = 0; i < a.size(); ++i)
{
t = a[i] * b + t; // 当前位的乘积加上前一位的进位
result.push_back(t % 10); // 存储当前位的个位数
t = t / 10; // 更新进位
}
// 处理最高位的进位(如果存在)
while (t > 0)
{
result.push_back(t % 10);
t /= 10;
}
// 反转结果,因为我们是从最低位开始计算的
reverse(result.begin(), result.end());
return result;
}
int main()
{
string largeNumber;
int smallNumber;
vector<int> digits;
// 输入大整数和小数
cin >> largeNumber >> smallNumber;
// 将大整数字符串转换为vector<int>形式
for (int i = largeNumber.size() - 1; i >= 0; --i)
{
digits.push_back(largeNumber[i] - '0');
}
// 调用乘法函数并输出结果
vector<int> result = multiply(digits, smallNumber);
for (int digit : result)
{
cout << digit;
}
cout << endl;
return 0;
}
4.1.3 注意事项
(1)进位处理:乘法运算中,每一位的乘积都可能产生进位,必须妥善处理。
(2)结果反转:由于我们从大整数的最低位开始计算,因此最终需要将结果反转以得到正确的顺序。
(3)边界情况:考虑输入的大整数或小数可能为0的特殊情况。
(4)性能优化:虽然对于大多数应用来说,上述实现已经足够高效,但在处理极端大数据时,可能需要考虑进一步优化算法或数据结构。
4.2 大数乘大数
处理两个大整数的乘法运算,本质上是将大数乘法分解为小整数乘法与大整数加法的组合过程。
4.2.1 思路
核心思路是将一个大整数的每一位分别与另一个大整数的每一位相乘,并将这些乘积按照其位权(即位置)存储在一个临时矩阵中。随后,通过逐列(或逐行)相加,并处理进位,最终得到乘积的每一位数字。
4.2.2 高精度乘法(大整数乘大整数)模板
#include <bits/stdc++.h>
using namespace std;
vector<int> multiply(vector<int> &a, vector<int> &b)
{
if (a.empty() || b.empty())
return {};
int n = a.size(), m = b.size();
vector<vector<int>> c(m, vector<int>(n + m, 0)); // 初始化乘积矩阵,大小为(m+n)x(m+n),但只使用前m行n+m列
int j=0;
for (int i = 0; i < m; ++i)
{
int t = 0; // 进位
for (j = 0; j < n; ++j)
{
t += a[j] * b[i]; // 乘积累加
t += c[i][j + i]; // 加上之前的结果
c[i][j + i] = t % 10; // 存储当前位的值
t /= 10; // 更新进位
}
// 处理剩余进位
while (t > 0)
{
c[i][j + i] = t % 10;
t /= 10;
j++;
}
}
vector<int> result;
int sum = 0, carry = 0;
// 逐列相加,处理最终结果
for (int i = 0; i < n + m; ++i)
{
sum = carry;
for (int j = 0; j < m; ++j)
{
sum += c[j][i];
}
result.push_back(sum % 10);
carry = sum / 10;
}
// 移除结果中的前导零
while (!result.empty() && result.back() == 0)
{
result.pop_back();
}
// 处理特殊情况:当结果为0时
if (result.empty())
result.push_back(0);
reverse(result.begin(), result.end()); // 反转结果,因为我们是按列处理的
return result;
}
int main()
{
string num1, num2;
cin >> num1 >> num2;
vector<int> a, b;
for (char c : num1)
a.push_back(c - '0');
for (char c : num2)
b.push_back(c - '0');
reverse(a.begin(), a.end()); // 将数字逆序存储,方便从低位开始计算
reverse(b.begin(), b.end());
vector<int> result = multiply(a, b);
for (int digit : result)
{
cout << digit;
}
cout << endl;
return 0;
}
4.2.3 注意事项
(1)进位处理:在乘法运算中,每次乘法操作后都需要检查是否有进位,并妥善处理,确保结果的准确性。
(2)前导零:结果可能包含前导零,这些零在最终展示前需要被移除。
(3)边界情况:特别注意处理乘数为0的边界情况,确保函数能正确返回结果。
(4)逆序存储:将输入的数字逆序存储可以简化计算过程,使我们从最低位开始处理,更符合人类的计算习惯。
5 大整数除法
5.1 大整数除以小数
5.1.1 思路
在处理大整数除以小数的运算时,从大整数的最高位开始,逐位地进行除法操作。这种方法模拟了手工除法的过程,通过不断累积当前处理的位数并除以给定的小数,从而逐步构建出商和余数。
5.1.2 大整数除法(大整数除以小数)模板
#include <bits/stdc++.h>
using namespace std;
// 函数用于执行大整数除以小数的操作
list<int> div(const vector<int> &largeInteger, int decimal, int &remainder)
{
list<int> quotient; // 存储商的每一位
int t = 0; // 用于临时存储当前累积的整数值
// 遍历大整数的每一位
for (int digit : largeInteger)
{
t = t * 10 + digit; // 将当前位加到累积值上
// 执行除法操作
quotient.push_back(t / decimal); // 计算当前位的商
t %= decimal; // 更新余数供下一次循环使用
}
// 移除商前面的前导零
while (!quotient.empty() && quotient.front() == 0)
{
quotient.pop_front();
}
// 存储最终的余数
remainder = t;
return quotient;
}
int main()
{
string largeNumber;
int decimal;
int remainder;
list<int> quotient;
// 输入大整数和小数
cin >> largeNumber >> decimal;
// 将大整数字符串转换为整数向量
vector<int> largeInteger;
for (char c : largeNumber)
{
largeInteger.push_back(c - '0');
}
// 执行除法操作
quotient = div(largeInteger, decimal, remainder);
// 输出商和余数
for (int num : quotient)
{
cout << num;
}
cout << "~" << remainder << endl;
return 0;
}
5.1.3 注意事项
(1)不需要倒序输入输出:
在处理过程中,不需要对输入输出进行倒序处理,因为代码已经按照从高位到低位的顺序处理了大整数的每一位。
(2)除数不为0:
确保输入的小数(decimal
)不为0,以避免除零错误。
(3)处理余数:
输出时,商和余数之间用特定字符(如"~")分隔,以便于区分和读取。
谢谢大家!!!!