六个章节带你入门算法:第一章 第2节 高精度

文章目录
- 六个章节带你入门算法:第一章 第2节 高精度
- 一、为什么要用高精度?
- 二、高精度的统一套路
- 三、[高精度加法:P1601 A+B Problem](https://www.luogu.com.cn/problem/P1601)
- 四、[高精度减法:P2142 高精度减法](https://www.luogu.com.cn/problem/P2142)
- 五、[高精度乘法:P1303 A*B Problem](https://www.luogu.com.cn/problem/P1303)
- 六、[高精度除法:P1480 A/B Problem](https://www.luogu.com.cn/problem/P1480)
- 七、四种高精度运算的对比
在刚开始学算法的时候,我们经常会碰到一种题:题目看起来就是普通的加减乘除,但数据范围大得离谱。
比如:
a, b <= 10^500
或者:
a <= 10^5000
这时候 int、long long 肯定都装不下。
即使是 unsigned long long,也远远不够。
那怎么办?
一个很朴素的想法是:既然数字太大不能直接存,那就把它当成字符串读进来,然后一位一位地处理。
这就是高精度算法的基本思路。
高精度听起来很“高级”,但入门阶段其实不用想得太复杂。它的本质就是:
用数组模拟小学列竖式计算。
一、为什么要用高精度?
C++ 中常见整数类型的范围大致如下:
int:约 2 × 10^9
long long:约 9 × 10^18
如果题目中的数达到:
10^500
10^2000
10^5000
这些类型就完全不够用了。
所以我们需要换一种方式存数字。
例如数字:
123456
我们可以用字符串读入:
string s = "123456";
然后把每一位拆出来放进数组。
比较常见的处理方式是逆序存储:
数字:123456
数组:
a[0] = 6
a[1] = 5
a[2] = 4
a[3] = 3
a[4] = 2
a[5] = 1
也就是说,个位放在数组下标 0 的位置。
为什么要逆序存?
因为加法、减法、乘法的竖式计算,都是从低位开始算的。个位、十位、百位这样往上走,正好对应数组下标从小到大遍历,代码会更自然。
二、高精度的统一套路
高精度题一般都可以拆成四步:
1. 用字符串读入大整数
2. 把字符串每一位转成数字,逆序存进数组
3. 用数组模拟竖式运算
4. 处理进位、借位、前导零,然后输出
三、高精度加法:P1601 A+B Problem
1. 题目背景
本题是高精度加法的模板题。
题目描述
给定两个非负整数 a , b a,b a,b,求它们的和。不用考虑负数。
输入格式
输入共两行,每行一个非负整数,分别为 a , b a,b a,b。
输出格式
输出一行一个非负整数,表示 a + b a+b a+b 的值。
输入输出样例 #1
输入 #1
1
1
输出 #1
2
输入输出样例 #2
输入 #2
1001
9099
输出 #2
10100
说明/提示
对于
20
%
20\%
20% 的测试数据,
a
,
b
≤
10
9
a,b \le 10^9
a,b≤109;
对于
40
%
40\%
40% 的测试数据,
a
,
b
≤
10
18
a,b \le 10^{18}
a,b≤1018;
对于
100
%
100\%
100% 的测试数据,
0
≤
a
,
b
≤
10
500
0\le a,b \le 10^{500}
0≤a,b≤10500。
2. 思路分析
高精度加法就是模拟小学列竖式。
例如:
1001
+ 9099
------
10100
从个位开始:
1 + 9 = 10,当前位写 0,进 1
0 + 9 + 1 = 10,当前位写 0,进 1
0 + 0 + 1 = 1,当前位写 1
1 + 9 = 10,当前位写 0,进 1
最后还有进位 1
数组逆序之后,代码就是从 i = 0 开始往后扫。
核心操作:
c[i] += a[i] + b[i];
c[i + 1] += c[i] / 10;
c[i] %= 10;
3. 参考代码
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N], b[N], c[N];
int la, lb, lc;
// 高精度加法模板:c = a + b
void add(int c[], int a[], int b[])
{
for (int i = 0; i < lc; i++)
{
c[i] += a[i] + b[i]; // 当前位相加
c[i + 1] += c[i] / 10; // 处理进位
c[i] %= 10; // 当前位保留个位
}
if (c[lc]) lc++; // 如果最高位还有进位,长度加一
}
int main()
{
string x, y;
cin >> x >> y;
la = x.size();
lb = y.size();
lc = max(la, lb);
// 逆序存储
for (int i = 0; i < la; i++) a[la - 1 - i] = x[i] - '0';
for (int i = 0; i < lb; i++) b[lb - 1 - i] = y[i] - '0';
add(c, a, b);
for (int i = lc - 1; i >= 0; i--) cout << c[i];
return 0;
}
4. 易错点
第一,忘记处理最后的进位。
比如:
999 + 1 = 1000
如果最后没有判断 c[lc],就可能输出成 000 或者少一位。
第二,数组没有逆序存储,导致从高位开始加。
高精度加法最好从低位开始处理,逆序存储会省掉很多麻烦。
第三,数组开小了。
竞赛中模板题数据可能很大,数组最好开得宽松一些。
四、高精度减法:P2142 高精度减法
1. 题目描述
给定两个正整数 a , b a,b a,b,求 a − b a-b a−b 的值。
输入格式
输入共两行,每行一个正整数,分别为 a , b a,b a,b。
输出格式
输出一行一个整数,表示 a − b a-b a−b 的值。
如果 a − b < 0 a-b<0 a−b<0,请输出负号。
输入输出样例 #1
输入 #1
2
1
输出 #1
1
说明/提示
对于
20
%
20\%
20% 的数据,
a
,
b
a,b
a,b 在long long类型的存储范围内;
对于
100
%
100\%
100% 的数据,
0
<
a
,
b
≤
10
10086
0<a,b\le 10^{10086}
0<a,b≤1010086。
2. 思路分析
减法比加法多两个问题。
第一个问题:结果可能为负数。
所以我们可以先比较两个数的大小。
如果 a < b,就交换两个数,然后输出一个负号。
也就是说:
1 - 2
可以变成:
-(2 - 1)
第二个问题:要处理借位。
比如:
1000 - 1
个位不够减,需要向高位借位。
核心代码是:
c[i] += a[i] - b[i];
if (c[i] < 0)
{
c[i + 1] -= 1;
c[i] += 10;
}
这里的 c[i + 1] -= 1 就表示向高位借了一位。
3. 如何比较两个大整数大小?
因为数字是字符串,不能直接转成整数。
比较方法:
1. 如果长度不同,长度长的数更大
2. 如果长度相同,按字典序比较
比如:
12345 > 999
因为前者长度更长。
而:
12345 < 22345
长度相同,直接按字典序比较即可。
4. 参考代码
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N], b[N], c[N];
int la, lb, lc;
// 判断 x 是否小于 y
bool cmp(string& x, string& y)
{
if (x.size() != y.size()) return x.size() < y.size();
return x < y;
}
// 高精度减法模板:c = a - b
void sub(int c[], int a[], int b[])
{
for (int i = 0; i < lc; i++)
{
c[i] += a[i] - b[i];
if (c[i] < 0)
{
c[i + 1] -= 1; // 向高位借位
c[i] += 10;
}
}
// 去掉前导零
while (lc > 1 && c[lc - 1] == 0) lc--;
}
int main()
{
string x, y;
cin >> x >> y;
// 如果 x < y,交换后计算 y - x,并输出负号
if (cmp(x, y))
{
swap(x, y);
cout << '-';
}
la = x.size();
lb = y.size();
lc = max(la, lb);
for (int i = 0; i < la; i++) a[la - 1 - i] = x[i] - '0';
for (int i = 0; i < lb; i++) b[lb - 1 - i] = y[i] - '0';
sub(c, a, b);
for (int i = lc - 1; i >= 0; i--) cout << c[i];
return 0;
}
5. 易错点
第一,比较大小时直接用字符串字典序。
比如:
"999" > "1000"
如果只看字典序,这个判断是错的。
所以必须先比较长度。
第二,忘记去掉前导零。
比如:
1000 - 999 = 1
数组里可能会留下:
0001
输出时就可能变成:
0001
所以要用:
while (lc > 1 && c[lc - 1] == 0) lc--;
第三,结果为 0 时不能把所有位都删掉。
所以条件必须是 lc > 1,至少保留一位。
五、高精度乘法:P1303 A*B Problem
1. 题目背景
高精度乘法模板题。
题目描述
给出两个非负整数,求它们的乘积。
输入格式
输入共两行,每行一个非负整数。
输出格式
输出一个非负整数表示乘积。
输入输出样例 #1
输入 #1
1
2
输出 #1
2
说明/提示
每个非负整数不超过 10 2000 10^{2000} 102000
2. 思路分析
乘法仍然是模拟小学竖式。
以:
123 × 45
为例:
123
× 45
---------
615
492
---------
5535
如果数字逆序存储:
123 -> a[0] = 3, a[1] = 2, a[2] = 1
45 -> b[0] = 5, b[1] = 4
那么:
a[0] * b[0] 影响 c[0]
a[1] * b[0] 影响 c[1]
a[0] * b[1] 影响 c[1]
a[2] * b[1] 影响 c[3]
总结下来就是:
c[i + j] += a[i] * b[j];
这里的 i + j 很关键。
因为第 i 位和第 j 位相乘,结果会落到第 i + j 位上。
不过这里先不急着处理进位。
可以先把所有乘积累加到对应位置,最后统一处理进位。
3. 参考代码
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N], b[N], c[N];
int la, lb, lc;
// 高精度乘法模板:c = a * b
void mul(int c[], int a[], int b[])
{
// 1. 无进位相乘,然后累加
for (int i = 0; i < la; i++)
{
for (int j = 0; j < lb; j++)
{
c[i + j] += a[i] * b[j];
}
}
// 2. 统一处理进位
for (int i = 0; i < lc; i++)
{
c[i + 1] += c[i] / 10;
c[i] %= 10;
}
// 3. 去掉前导零
while (lc > 1 && c[lc - 1] == 0) lc--;
}
int main()
{
string x, y;
cin >> x >> y;
la = x.size();
lb = y.size();
lc = la + lb;
for (int i = 0; i < la; i++) a[la - 1 - i] = x[i] - '0';
for (int i = 0; i < lb; i++) b[lb - 1 - i] = y[i] - '0';
mul(c, a, b);
for (int i = lc - 1; i >= 0; i--) cout << c[i];
return 0;
}
4. 为什么结果长度最多是 la + lb?
两个数相乘,结果位数最多是两个数位数之和。
比如:
99 × 99 = 9801
两个数都是 2 位,结果最多 4 位。
再比如:
100 × 100 = 10000
两个数都是 3 位,结果最多 6 位,实际是 5 位。
所以乘法里可以先写:
lc = la + lb;
最后再去前导零。
5. 易错点
第一,忘记 c[i + j] 这个位置关系。
第二,边乘边进位容易写乱。
第三,忘记处理乘以 0。
六、高精度除法:P1480 A/B Problem
题目描述
输入两个整数 a , b a,b a,b,输出它们的商。
输入格式
两行,第一行是被除数,第二行是除数。
输出格式
一行,商的整数部分。
输入输出样例 #1
输入 #1
10
2
输出 #1
5
说明/提示
0 ≤ a ≤ 10 5000 0\le a\le 10^{5000} 0≤a≤105000, 1 ≤ b ≤ 10 9 1\le b\le 10^9 1≤b≤109。
2. 思路分析
除法和加减乘不一样。
加减乘一般从低位往高位处理,而除法竖式是从高位往低位处理的。
比如:
1234 / 12
我们从最高位开始看:
先看 1,不够除
再看 12,可以除
再把余数带到下一位
程序里可以维护一个变量 t,表示当前余数或者当前被除数。
每次处理一位:
t = t * 10 + a[i];
c[i] = t / b;
t %= b;
因为数组是逆序存储的,最高位在 a[la - 1],所以除法循环要从 la - 1 走到 0。
3. 参考代码
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
int a[N], b, c[N];
int la, lc;
// 高精度除法模板:c = a / b
// 这里是 高精度 / 低精度
void divide(int c[], int a[], int b)
{
LL t = 0; // 当前余数,也可以理解成当前被除数
for (int i = la - 1; i >= 0; i--)
{
t = t * 10 + a[i];
c[i] = t / b;
t %= b;
}
// 去掉前导零
while (lc > 1 && c[lc - 1] == 0) lc--;
}
int main()
{
string x;
cin >> x >> b;
la = x.size();
lc = la;
for (int i = 0; i < la; i++) a[la - 1 - i] = x[i] - '0';
divide(c, a, b);
for (int i = lc - 1; i >= 0; i--) cout << c[i];
return 0;
}
4. 为什么除法要用 long long t?
因为 t 每次会执行:
t = t * 10 + a[i];
如果除数 b 比较大,t 也可能比较大。
题目里常见的低精度除数可能达到 10^9,用 int 有溢出的风险,所以这里用 long long 更稳。
5. 为什么除法输出还是倒着输出?
虽然除法是从高位开始算的,但我们数组依然采用逆序存储。
比如商是:
102
数组中还是:
c[0] = 2
c[1] = 0
c[2] = 1
所以最后输出时仍然是:
for (int i = lc - 1; i >= 0; i--) cout << c[i];
七、四种高精度运算的对比
| 运算 | 处理方向 | 核心操作 | 关键细节 |
|---|---|---|---|
| 加法 | 低位到高位 | 对应位相加 | 处理进位 |
| 减法 | 低位到高位 | 对应位相减 | 比大小、借位、负号 |
| 乘法 | 低位到高位 | c[i + j] += a[i] * b[j] | 统一进位、去前导零 |
| 除法 | 高位到低位 | t = t * 10 + a[i] | 保存余数、去前导零 |
1486

被折叠的 条评论
为什么被折叠?



