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

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

头像

🔥 星恒随风: 个人主页
❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 《C++从入门到入土》
✨ 数据即知识,压缩即智能

在刚开始学算法的时候,我们经常会碰到一种题:题目看起来就是普通的加减乘除,但数据范围大得离谱。

比如:

a, b <= 10^500

或者:

a <= 10^5000

这时候 intlong 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,b109
对于 40 % 40\% 40% 的测试数据, a , b ≤ 10 18 a,b \le 10^{18} a,b1018
对于 100 % 100\% 100% 的测试数据, 0 ≤ a , b ≤ 10 500 0\le a,b \le 10^{500} 0a,b10500


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 ab 的值。

输入格式

输入共两行,每行一个正整数,分别为 a , b a,b a,b

输出格式

输出一行一个整数,表示 a − b a-b ab 的值。

如果 a − b < 0 a-b<0 ab<0请输出负号

输入输出样例 #1

输入 #1

2
1

输出 #1

1

说明/提示

对于 20 % 20\% 20% 的数据, a , b a,b a,blong long类型的存储范围内;
对于 100 % 100\% 100% 的数据, 0 < a , b ≤ 10 10086 0<a,b\le 10^{10086} 0<a,b1010086


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} 0a105000 1 ≤ b ≤ 10 9 1\le b\le 10^9 1b109


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]保存余数、去前导零

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值