高精度
前言¶
太长不看版:结尾自取模板……
什么时候需要高精度呢?有时候题目计算的中间结果,甚至输入本身,都存在超过语言内建整型(最大 64 位或 128 位)所能表示的范围。这个时候就只好自己处理它们啦。
高精度问题包含很多小的细节,实现上也有很多讲究。
所以今天就来一起实现一个简单的计算器吧。 输入:一个形如 输出:运算结果。任务
a <op> b
的表达式。a
、b
分别是长度不超过 <op>
是一个字符(+
、-
、*
或 /
),表示运算。+
、-
、*
运算,输出一行表示结果;/
运算,输出两行分别表示商和余数。
存储¶
在平常的实现中,高精度数字利用字符串表示,每一个字符表示数字的一个十进制位。因此可以说,高精度数值计算实际上是一种特别的字符串处理。
读入字符串时,数字最高位在字符串首(下标小的位置)。但是习惯上,下标最小的位置存放的是数字的 最低位 ,即存储反转的字符串。这么做的原因在于,数字的长度可能发生变化,但我们希望同样权值位始终保持对齐(例如,希望所有的个位都在下标 [0]
,所有的十位都在下标 [1]
……);同时,加、减、乘的运算一般都从个位开始进行(回想小学的竖式运算~),这都给了「反转存储」以充分的理由。
此后我们将一直沿用这一约定。定义一个常数 LEN = 1004
表示程序所容纳的最大长度。
由此不难写出读入高精度数字的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void clear(int a[LEN]) { for (int i = 0; i < LEN; ++i) a[i] = 0; } void read(int a[LEN]) { static char s[LEN + 1]; scanf("%s", s); clear(a); int len = strlen(s); // 如上所述,反转 for (int i = 0; i < len; ++i) a[len - i - 1] = s[i] - '0'; // s[i] - '0' 就是 s[i] 所表示的数码 // 有些同学可能更习惯用 ord(s[i]) - ord('0') 的方式理解 } |
输出也按照存储的逆序输出。由于不希望输出前导零,故这里从最高位开始向下寻找第一个非零位,从此处开始输出;终止条件 i >= 1
而不是 i >= 0
是因为当整个数字等于 0
。
1 2 3 4 5 6 7 | void print(int a[LEN]) { int i; for (i = LEN - 1; i >= 1; --i) if (a[i] != 0) break; for (; i >= 0; --i) putchar(a[i] + '0'); putchar('\n'); } |
拼起来就是一个完整的复读机程序咯。copycat.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <cstdio>
#include <cstring>
static const int LEN = 1004;
int a[LEN], b[LEN];
void clear(int a[LEN]) {
for (int i = 0; i < LEN; ++i) a[i] = 0;
}
void read(int a[LEN]) {
static char s[LEN + 1];
scanf("%s", s);
clear(a);
int len = strlen(s);
for (int i = 0; i < len; ++i) a[len - i - 1] = s[i] - '0';
}
void print(int a[LEN]) {
int i;
for (i = LEN - 1; i >= 1; --i)
if (a[i] != 0) break;
for (; i >= 0; --i) putchar(a[i] + '0');
putchar('\n');
}
int main() {
read(a);
print(a);
return 0;
}
四则运算¶
四则运算中难度也各不相同。最简单的是高精度加减法,其次是高精度—单精度(普通的 int
)乘法和高精度—高精度乘法,最后是高精度—高精度除法。
我们将按这个顺序分别实现所有要求的功能。
加法¶
高精度加法,其实就是竖式加法啦。
也就是从最低位开始,将两个加数对应位置上的数码相加,并判断是否达到或超过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void add(int a[LEN], int b[LEN], int c[LEN]) { clear(c); // 高精度实现中,一般令数组的最大长度 LEN 比可能的输入大一些 // 然后略去末尾的几次循环,这样一来可以省去不少边界情况的处理 // 因为实际输入不会超过 1000 位,故在此循环到 LEN - 1 = 1003 已经足够 for (int i = 0; i < LEN - 1; ++i) { // 将相应位上的数码相加 c[i] += a[i] + b[i]; if (c[i] >= 10) { // 进位 c[i + 1] += 1; c[i] -= 10; } } } |
试着和上一部分结合,可以得到一个加法计算器。adder.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <cstdio>
#include <cstring>
static const int LEN = 1004;
int a[LEN], b[LEN], c[LEN];
void clear(int a[LEN]) {
for (int i = 0; i < LEN; ++i) a[i] = 0;
}
void read(int a[LEN]) {
static char s[LEN + 1];
scanf("%s", s);
clear(a);
int len = strlen(s);
for (int i = 0; i < len; ++i) a[len - i - 1] = s[i] - '0';
}
void print(int a[LEN]) {
int i;
for (i = LEN - 1; i >= 1; --i)
if (a[i] != 0) break;
for (; i >= 0; --i) putchar(a[i] + '0');
putchar('\n');
}
void add(int a[LEN], int b[LEN], int c[LEN]) {
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
c[i] += a[i] + b[i];
if (c[i] >= 10) {
c[i + 1] += 1;
c[i] -= 10;
}
}
}
int main() {
read(a);
read(b);
add(a, b, c);
print(c);
return 0;
}
减法¶
高精度加法,也就是竖式减法啦。
从个位起逐位相减,遇到负的情况则向上一位借
1 2 3 4 5 6 7 8 9 10 11 12 13 | void sub(int a[LEN], int b[LEN], int c[LEN]) { clear(c); for (int i = 0; i < LEN - 1; ++i) { // 逐位相减 c[i] += a[i] - b[i]; if (c[i] < 0) { // 借位 c[i + 1] -= 1; c[i] += 10; } } } |
将上一个程序中的 add()
替换成 sub()
,就有了一个减法计算器。subtractor.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <cstdio>
#include <cstring>
static const int LEN = 1004;
int a[LEN], b[LEN], c[LEN];
void clear(int a[LEN])
{
for (int i = 0; i < LEN; ++i) a[i] = 0;
}
void read(int a[LEN])
{
static char s[LEN + 1];
scanf("%s", s);
clear(a);
int len = strlen(s);
for (int i = 0; i < len; ++i)
a[len - i - 1] = s[i] - '0';
}
void print(int a[LEN])
{
int i;
for (i = LEN - 1; i >= 1; --i) if (a[i] != 0) break;
for (; i >= 0; --i) putchar(a[i] + '0');
putchar('\n');
}
void sub(int a[LEN], int b[LEN], int c[LEN])
{
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
c[i] += a[i] - b[i];
if (c[i] < 0) {
c[i + 1] -= 1;
c[i] += 10;
}
}
}
int main()
{
read(a);
read(b);
sub(a, b, c);
print(c);
return 0;
}
试一试,输入 1 2
——输出 /9999999
,诶这个 OI Wiki 怎么给了我一份假的代码啊……
如你所见,上面的程序只能处理
乘法¶
高精度—单精度¶
高精度乘法,也就是竖……等会儿等会儿!
先考虑一个简单的情况:乘数中的一个是普通的 int
类型。有没有简单的处理方法呢?
一个直观的思路是直接将
重整的方式,也是从个位开始逐位向上处理进位。但是这里的进位可能非常大,甚至远大于
当然,也是出于这个原因,这个方法需要特别关注乘数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void mul_short(int a[LEN], int b, int c[LEN]) { clear(c); for (int i = 0; i < LEN - 1; ++i) { // 直接把 a 的第 i 位数码乘以乘数,加入结果 c[i] += a[i] * b; if (c[i] >= 10) { // 处理进位 // c[i] / 10 即除法的商数成为进位的增量值 c[i + 1] += c[i] / 10; // 而 c[i] % 10 即除法的余数成为在当前位留下的值 c[i] %= 10; } } } |
高精度—高精度¶
如果两个乘数都是高精度,那么竖式乘法又可以大显身手了。
回想竖式乘法的每一步,实际上是计算了若干
于是可以将
注意这个过程与竖式乘法不尽相同,我们的算法在每一步乘的过程中并不进位,而是将所有的结果保留在对应的位置上,到最后再统一处理进位,但这不会影响结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void mul(int a[LEN], int b[LEN], int c[LEN]) { clear(c); for (int i = 0; i < LEN - 1; ++i) { // 这里直接计算结果中的从低到高第 i 位,且一并处理了进位 // 第 i 次循环为 c[i] 加上了所有满足 p + q = i 的 a[p] 与 b[q] 的乘积之和 // 这样做的效果和直接进行上图的运算最后求和是一样的,只是更加简短的一种实现方式 for (int j = 0; j <= i; ++j) c[i] += a[j] * b[i - j]; if (c[i] >= 10) { c[i + 1] += c[i] / 10; c[i] %= 10; } } } |
除法¶
高精度除法,也就是竖~~~~竖式长除法啦!
竖式长除法实际上可以看作一个逐次减法的过程。例如上图中商数十位的计算可以这样理解:将
为了减少冗余运算,我们提前得到被除数的长度
参考程序实现了一个函数 greater_eq()
用于判断被除数以下标 last_dg
为最低位,是否可以再减去除数而保持非负。此后对于商的每一位,不断调用 greater_eq()
,并在成立的时候用高精度减法从余数中减去除数,也即模拟了竖式除法的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // 被除数 a 以下标 last_dg 为最低位,是否可以再减去除数 b 而保持非负 // len 是除数 b 的长度,避免反复计算 inline bool greater_eq(int a[LEN], int b[LEN], int last_dg, int len) { // 有可能被除数剩余的部分比除数长,这个情况下最多多出 1 位,故如此判断即可 if (a[last_dg + len] != 0) return true; // 从高位到低位,逐位比较 for (int i = len - 1; i >= 0; --i) { if (a[last_dg + i] > b[i]) return true; if (a[last_dg + i] < b[i]) return false; } // 相等的情形下也是可行的 return true; } void div(int a[LEN], int b[LEN], int c[LEN], int d[LEN]) { clear(c); clear(d); int la, lb; for (la = LEN - 1; la > 0; --la) if (a[la - 1] != 0) break; for (lb = LEN - 1; lb > 0; --lb) if (b[lb - 1] != 0) break; if (lb == 0) { puts("> <"); return; } // 除数不能为零 // c 是商 // d 是被除数的剩余部分,算法结束后自然成为余数 for (int i = 0; i < la; ++i) d[i] = a[i]; for (int i = la - lb; i >= 0; --i) { // 计算商的第 i 位 while (greater_eq(d, b, i, lb)) { // 若可以减,则减 // 这一段是一个高精度减法 for (int j = 0; j < lb; ++j) { d[i + j] -= b[j]; if (d[i + j] < 0) { d[i + j + 1] -= 1; d[i + j] += 10; } } // 使商的这一位增加 1 c[i] += 1; // 返回循环开头,重新检查 } } } |
入门篇完成!¶
将上面介绍的四则运算的实现结合,即可完成开头提到的计算器程序。calculator.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#include <cstdio>
#include <cstring>
static const int LEN = 1004;
int a[LEN], b[LEN], c[LEN], d[LEN];
void clear(int a[LEN]) {
for (int i = 0; i < LEN; ++i) a[i] = 0;
}
void read(int a[LEN]) {
static char s[LEN + 1];
scanf("%s", s);
clear(a);
int len = strlen(s);
for (int i = 0; i < len; ++i) a[len - i - 1] = s[i] - '0';
}
void print(int a[LEN]) {
int i;
for (i = LEN - 1; i >= 1; --i)
if (a[i] != 0) break;
for (; i >= 0; --i) putchar(a[i] + '0');
putchar('\n');
}
void add(int a[LEN], int b[LEN], int c[LEN]) {
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
c[i] += a[i] + b[i];
if (c[i] >= 10) {
c[i + 1] += 1;
c[i] -= 10;
}
}
}
void sub(int a[LEN], int b[LEN], int c[LEN]) {
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
c[i] += a[i] - b[i];
if (c[i] < 0) {
c[i + 1] -= 1;
c[i] += 10;
}
}
}
void mul(int a[LEN], int b[LEN], int c[LEN]) {
clear(c);
for (int i = 0; i < LEN - 1; ++i) {
for (int j = 0; j <= i; ++j) c[i] += a[j] * b[i - j];
if (c[i] >= 10) {
c[i + 1] += c[i] / 10;
c[i] %= 10;
}
}
}
inline bool greater_eq(int a[LEN], int b[LEN], int last_dg, int len) {
if (a[last_dg + len] != 0) return true;
for (int i = len - 1; i >= 0; --i) {
if (a[last_dg + i] > b[i]) return true;
if (a[last_dg + i] < b[i]) return false;
}
return true;
}
void div(int a[LEN], int b[LEN], int c[LEN], int d[LEN]) {
clear(c);
clear(d);
int la, lb;
for (la = LEN - 1; la > 0; --la)
if (a[la - 1] != 0) break;
for (lb = LEN - 1; lb > 0; --lb)
if (b[lb - 1] != 0) break;
if (lb == 0) {
puts("> <");
return;
}
for (int i = 0; i < la; ++i) d[i] = a[i];
for (int i = la - lb; i >= 0; --i) {
while (greater_eq(d, b, i, lb)) {
for (int j = 0; j < lb; ++j) {
d[i + j] -= b[j];
if (d[i + j] < 0) {
d[i + j + 1] -= 1;
d[i + j] += 10;
}
}
c[i] += 1;
}
}
}
int main() {
read(a);
char op[4];
scanf("%s", op);
read(b);
switch (op[0]) {
case '+':
add(a, b, c);
print(c);
break;
case '-':
sub(a, b, c);
print(c);
break;
case '*':
mul(a, b, c);
print(c);
break;
case '/':
div(a, b, c, d);
print(c);
print(d);
break;
default:
puts("> <");
}
return 0;
}
封装类¶
这里有一个封装好的高精度整数类。这里是另一个模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#define MAXN 9999
// MAXN 是一位中最大的数字
#define MAXSIZE 10024
// MAXSIZE 是位数
#define DLEN 4
// DLEN 记录压几位
struct Big {
int a[MAXSIZE], len;
bool flag; //标记符号'-'
Big() {
len = 1;
memset(a, 0, sizeof a);
flag = 0;
}
Big(const int);
Big(const char*);
Big(const Big&);
Big& operator=(const Big&); //注意这里operator有&,因为赋值有修改……
//由于OI中要求效率
//此处不使用泛型函数
//故不重载
// istream& operator>>(istream&, BigNum&); //重载输入运算符
// ostream& operator<<(ostream&, BigNum&); //重载输出运算符
Big operator+(const Big&) const;
Big operator-(const Big&) const;
Big operator*(const Big&)const;
Big operator/(const int&) const;
// TODO: Big / Big;
Big operator^(const int&) const;
// TODO: Big ^ Big;
// TODO: Big 位运算;
int operator%(const int&) const;
// TODO: Big ^ Big;
bool operator<(const Big&) const;
bool operator<(const int& t) const;
inline void print();
};
// README::不要随随便便把参数都变成引用,那样没办法传值
Big::Big(const int b) {
int c, d = b;
len = 0;
// memset(a,0,sizeof a);
CLR(a);
while (d > MAXN) {
c = d - (d / (MAXN + 1) * (MAXN + 1));
d = d / (MAXN + 1);
a[len++] = c;
}
a[len++] = d;
}
Big::Big(const char* s) {
int t, k, index, l;
CLR(a);
l = strlen(s);
len = l / DLEN;
if (l % DLEN) ++len;
index = 0;
for (int i = l - 1; i >= 0; i -= DLEN) {
t = 0;
k = i - DLEN + 1;
if (k < 0) k = 0;
g(j, k, i) t = t * 10 + s[j] - '0';
a[index++] = t;
}
}
Big::Big(const Big& T) : len(T.len) {
CLR(a);
f(i, 0, len) a[i] = T.a[i];
// TODO:重载此处?
}
Big& Big::operator=(const Big& T) {
CLR(a);
len = T.len;
f(i, 0, len) a[i] = T.a[i];
return *this;
}
Big Big::operator+(const Big& T) const {
Big t(*this);
int big = len;
if (T.len > len) big = T.len;
f(i, 0, big) {
t.a[i] += T.a[i];
if (t.a[i] > MAXN) {
++t.a[i + 1];
t.a[i] -= MAXN + 1;
}
}
if (t.a[big])
t.len = big + 1;
else
t.len = big;
return t;
}
Big Big::operator-(const Big& T) const {
int big;
bool ctf;
Big t1, t2;
if (*this < T) {
t1 = T;
t2 = *this;
ctf = 1;
} else {
t1 = *this;
t2 = T;
ctf = 0;
}
big = t1.len;
int j = 0;
f(i, 0, big) {
if (t1.a[i] < t2.a[i]) {
j = i + 1;
while (t1.a[j] == 0) ++j;
--t1.a[j--];
// WTF?
while (j > i) t1.a[j--] += MAXN;
t1.a[i] += MAXN + 1 - t2.a[i];
} else
t1.a[i] -= t2.a[i];
}
t1.len = big;
while (t1.len > 1 && t1.a[t1.len - 1] == 0) {
--t1.len;
--big;
}
if (ctf) t1.a[big - 1] = -t1.a[big - 1];
return t1;
}
Big Big::operator*(const Big& T) const {
Big res;
int up;
int te, tee;
f(i, 0, len) {
up = 0;
f(j, 0, T.len) {
te = a[i] * T.a[j] + res.a[i + j] + up;
if (te > MAXN) {
tee = te - te / (MAXN + 1) * (MAXN + 1);
up = te / (MAXN + 1);
res.a[i + j] = tee;
} else {
up = 0;
res.a[i + j] = te;
}
}
if (up) res.a[i + T.len] = up;
}
res.len = len + T.len;
while (res.len > 1 && res.a[res.len - 1] == 0) --res.len;
return res;
}
Big Big::operator/(const int& b) const {
Big res;
int down = 0;
gd(i, len - 1, 0) {
res.a[i] = (a[i] + down * (MAXN + 1) / b);
down = a[i] + down * (MAXN + 1) - res.a[i] * b;
}
res.len = len;
while (res.len > 1 && res.a[res.len - 1] == 0) --res.len;
return res;
}
int Big::operator%(const int& b) const {
int d = 0;
gd(i, len - 1, 0) d = (d * (MAXN + 1) % b + a[i]) % b;
return d;
}
Big Big::operator^(const int& n) const {
Big t(n), res(1);
// TODO::快速幂这样写好丑= =//DONE:)
int y = n;
while (y) {
if (y & 1) res = res * t;
t = t * t;
y >>= 1;
}
return res;
}
bool Big::operator<(const Big& T) const {
int ln;
if (len < T.len) return 233;
if (len == T.len) {
ln = len - 1;
while (ln >= 0 && a[ln] == T.a[ln]) --ln;
if (ln >= 0 && a[ln] < T.a[ln]) return 233;
return 0;
}
return 0;
}
inline bool Big::operator<(const int& t) const {
Big tee(t);
return *this < tee;
}
inline void Big::print() {
printf("%d", a[len - 1]);
gd(i, len - 2, 0) { printf("%04d", a[i]); }
}
inline void print(Big s) {
// s不要是引用,要不然你怎么print(a * b);
int len = s.len;
printf("%d", s.a[len - 1]);
gd(i, len - 2, 0) { printf("%04d", s.a[i]); }
}
char s[100024];
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用