算法基础篇:(二)基础算法之高精度:突破数据极限

目录

前言

一、高精度算法的本质与核心思想

[1.1 什么是高精度算法?](#1.1 什么是高精度算法?)

[1.2 高精度算法的核心要素](#1.2 高精度算法的核心要素)

[1.3 高精度算法的适用场景](#1.3 高精度算法的适用场景)

[1.4 高精度运算的通用预处理步骤](#1.4 高精度运算的通用预处理步骤)

[二、高精度加法(High-Precision Addition)](#二、高精度加法(High-Precision Addition))

[2.1 算法原理](#2.1 算法原理)

[2.2 实现步骤](#2.2 实现步骤)

[2.3 完整代码实现(ACM 模式)](#2.3 完整代码实现(ACM 模式))

[2.4 代码解析与测试](#2.4 代码解析与测试)

代码解析

测试用例

[2.5 易错点分析](#2.5 易错点分析)

[三、高精度减法(High-Precision Subtraction)](#三、高精度减法(High-Precision Subtraction))

[3.1 算法原理](#3.1 算法原理)

[3.2 实现步骤](#3.2 实现步骤)

[3.3 关键辅助函数:比较两个大整数的大小](#3.3 关键辅助函数:比较两个大整数的大小)

[3.4 完整代码实现(ACM 模式)](#3.4 完整代码实现(ACM 模式))

[3.5 代码解析与测试](#3.5 代码解析与测试)

代码解析

测试用例

[3.6 易错点分析](#3.6 易错点分析)

[四、高精度乘法(High-Precision Multiplication)](#四、高精度乘法(High-Precision Multiplication))

[4.1 算法原理](#4.1 算法原理)

[4.2 实现步骤](#4.2 实现步骤)

[4.3 完整代码实现(ACM 模式)](#4.3 完整代码实现(ACM 模式))

[4.4 代码解析与测试](#4.4 代码解析与测试)

代码解析

测试用例

[4.5 易错点分析](#4.5 易错点分析)

[五、高精度除法(High-Precision Division)](#五、高精度除法(High-Precision Division))

[5.1 算法原理](#5.1 算法原理)

[5.2 实现步骤(整除 + 取模)](#5.2 实现步骤(整除 + 取模))

[5.3 完整代码实现(ACM 模式,含整除和取模)](#5.3 完整代码实现(ACM 模式,含整除和取模))

[5.4 代码解析与测试](#5.4 代码解析与测试)

代码解析

测试用例

[5.5 易错点分析](#5.5 易错点分析)

六、高精度算法的优化技巧

[6.1 存储优化:向量 vs 数组](#6.1 存储优化:向量 vs 数组)

[6.2 运算优化:减少冗余操作](#6.2 运算优化:减少冗余操作)

[6.3 性能优化:处理超大长度数据](#6.3 性能优化:处理超大长度数据)

[6.4 代码复用:封装成工具类](#6.4 代码复用:封装成工具类)

七、实战练习与拓展

[7.1 经典练习题(洛谷)](#7.1 经典练习题(洛谷))

[7.2 练习建议](#7.2 练习建议)

总结


前言

在做算法题的时候,我们经常会遇到需要处理极大或极小数值的场景 ------ 比如计算两个 1000 位的整数相乘、求 100! 的精确结果,或是处理天文数字级别的数据。此时,C++ 内置的int(最大约 2×10⁹)、long long(最大约 9×10¹⁸)等基础数据类型早已无法满足需求,它们会因数值溢出而导致结果错误。

为了解决这一问题,高精度算法应运而生。高精度算法的核心思想是 "模拟人类列竖式运算的过程",通过字符串或数组存储超大数值的每一位,再按照四则运算的规则逐位处理,从而实现任意长度数值的精确计算。

本文将从高精度算法的本质出发,详细讲解高精度加法、减法、乘法、除法(含整除与取模)的原理、实现步骤、代码细节及优化技巧。全文采用 C++ 实现,兼容 ACM 竞赛提交格式,同时包含大量实例、易错点分析和实战练习建议,无论你是编程新手还是竞赛选手,都能彻底掌握高精度运算。


一、高精度算法的本质与核心思想

1.1 什么是高精度算法?

高精度算法 (High-Precision Algorithm),又称 "大整数运算" ,是指处理超出标准数据类型表示范围的数值运算的算法。它不依赖硬件提供的数值存储能力,而是通过软件模拟的方式,将超大数值拆分为单个数字(0-9)进行存储和运算,最终得到精确结果。

例如,计算 99999999999999999999 + 1 时:

  • 基础数据类型:long long 无法存储该数值,直接运算会溢出;
  • 高精度算法:将数值存储为字符串 "99999999999999999999",模拟列竖式加法,从右往左逐位相加,处理进位,最终得到结果 "100000000000000000000"

1.2 高精度算法的核心要素

要实现可靠的高精度运算,必须把握以下 4 个核心要素:

  1. 存储方式:选择合适的结构存储每一位数字(数组 / 向量为最优选择);
  2. 位序处理:通常采用 "逆序存储"(低位在前、高位在后),便于从右往左逐位运算;
  3. 进位 / 借位处理:严格遵循竖式运算规则,处理加法进位、减法借位、乘法进位、除法余数;
  4. 结果整理:去掉结果中的前导零,处理负数符号,按正确位序输出。

1.3 高精度算法的适用场景

  • 编程竞赛:NOIP、蓝桥杯等竞赛中的大整数运算题(如高精度加法、乘法、阶乘计算);
  • 工程实践:密码学(大质数运算)、金融计算(高精度货币计算)、科学计算(天文 / 物理数据)等;
  • 算法学习:巩固数组操作、循环控制、逻辑思维,为复杂算法(如高精度动态规划)铺垫基础。

1.4 高精度运算的通用预处理步骤

无论哪种高精度运算,都需要先完成以下预处理工作,这是保证代码简洁、高效的关键:

  1. 读取输入 :用字符串读取超大数值(避免溢出);
  2. 逆序存储 :将字符串转换为数组 / 向量,且低位在前、高位在后 (例如 "1234" 存储为 [4,3,2,1]);
  3. 初始化结果容器:创建存储结果的数组 / 向量,初始值为 0;
  4. 处理符号 :分离数值的正负号(减法 / 除法需特殊处理)。

为什么要逆序存储?

  • 人类在列竖式运算时,是从最低位(最右边)开始逐位计算的,逆序存储后,数组下标 0 对应最低位,下标 1 对应次低位,以此类推,可通过循环从 0 开始逐位处理,无需反向遍历字符串;
  • 进位 / 借位是从低位向高位传递,逆序存储有助于直接在数组尾部扩展高位(如加法最高位有进位时,直接在数组末尾添加 1)。

二、高精度加法(High-Precision Addition)

2.1 算法原理

高精度加法的核心是模拟人类列竖式加法,规则如下:

  1. 两个数的最低位对齐
  2. 从最低位开始,逐位相加(包含上一位的进位);
  3. 当前位结果 =(加数 1 当前位 + 加数 2 当前位 + 进位)% 10
  4. 新的进位 =(加数 1 当前位 + 加数 2 当前位 + 进位)/ 10
  5. 遍历完所有位后,若进位不为 0,需将进位添加到结果的最高位。

示例 :计算 1234 + 56789

  • 逆序存储后:a = [4,3,2,1](1234),b = [9,8,7,6,5](56789);
  • 逐位计算:
    • 位 0:4+9+0=13 → 结果位 0=3,进位 = 1;
    • 位 1:3+8+1=12 → 结果位 1=2,进位 = 1;
    • 位 2:2+7+1=10 → 结果位 2=0,进位 = 1;
    • 位 3:1+6+1=8 → 结果位 3=8,进位 = 0;
    • 位 4:0+5+0=5 → 结果位 4=5,进位 = 0;
  • 结果逆序后:[5,8,0,2,3] → 正序为 58023,与实际结果一致。

2.2 实现步骤

  1. 读取两个大整数的字符串 s1s2
  2. 将字符串逆序转换为向量 ab(低位在前);
  3. 初始化进位 carry = 0,结果向量 res
  4. 循环遍历 ab 的每一位(遍历长度为两者的最大值):
    • a[i](若 i 超出 a 的长度,取 0);
    • b[i](若 i 超出 b 的长度,取 0);
    • 计算当前位总和 sum = a[i] + b[i] + carry
    • 结果位 sum % 10存入 res
    • 更新进位 carry = sum / 10
  5. 遍历结束后,若carry > 0,将 carry 存入 res
  6. res 逆序输出(去掉前导零,此处加法无负号)。

2.3 完整代码实现(ACM 模式)

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

// 高精度加法:a + b,a和b均为非负整数的字符串形式
string add(string s1, string s2) {
    // 逆序存储到向量(低位在前)
    vector<int> a, b, res;
    for (int i = s1.size() - 1; i >= 0; --i) a.push_back(s1[i] - '0');
    for (int i = s2.size() - 1; i >= 0; --i) b.push_back(s2[i] - '0');
    
    int carry = 0;  // 进位
    int len = max(a.size(), b.size());
    for (int i = 0; i < len; ++i) {
        // 取当前位的值,超出长度则为0
        int x = i < a.size() ? a[i] : 0;
        int y = i < b.size() ? b[i] : 0;
        int sum = x + y + carry;
        res.push_back(sum % 10);  // 当前位结果
        carry = sum / 10;         // 更新进位
    }
    // 处理最高位的进位
    if (carry != 0) res.push_back(carry);
    
    // 转换为字符串(逆序输出)
    string ans;
    for (int i = res.size() - 1; i >= 0; --i) {
        ans += (res[i] + '0');
    }
    return ans;
}

int main() {
    string s1, s2;
    cin >> s1 >> s2;
    cout << add(s1, s2) << endl;
    return 0;
}

2.4 代码解析与测试

代码解析
  • 存储转换:通过循环将字符串从尾到头遍历,转换为向量,实现逆序存储;
  • 逐位计算 :用max(a.size(), b.size()) 确保遍历完所有有效位,不足的位用 0 补齐;
  • 进位处理:每次计算后更新进位,遍历结束后若进位不为 0,需添加到结果末尾(最高位);
  • 结果转换:将结果向量逆序遍历,转换为字符串输出,保证正序显示。
测试用例
  • 测试用例 1(普通情况):输入:1234 56789 → 输出:58023
  • 测试用例 2(有进位):输入:9999 1 → 输出:10000
  • 测试用例 3(长度不一致):输入:123456789 987654321 → 输出:1111111110
  • 测试用例 4(含零):输入:0 12345 → 输出:12345

2.5 易错点分析

  1. 忘记处理最高位进位 :如 999 + 1,若未添加进位,结果会是 000 而非 1000
  2. 字符串转向量时顺序错误:若正序存储(高位在前),会导致遍历顺序与竖式运算相反,结果错误;
  3. 未处理长度不一致 :如 123 + 4567,若只遍历到较短的长度,会丢失较长数的高位;
  4. 零的处理 :输入为 0 时,转换后向量为 [0],输出时需正常显示,不能省略。

三、高精度减法(High-Precision Subtraction)

3.1 算法原理

高精度减法的核心是模拟人类列竖式减法 ,规则如下(假设被减数 ≥ 减数,结果非负):

  1. 两个数的最低位对齐
  2. 从最低位开始,逐位相减(若被减数当前位 < 减数当前位,需向高位借位);
  3. 当前位结果 =(被减数当前位 - 减数当前位 + 借位补偿)% 10
  4. 借位标记:若被减数当前位 < 减数当前位,借位为 1(下一位需减 1),否则为 0;
  5. 遍历完所有位后,去掉结果中的前导零(如结果为 000123,需改为 123)。

特殊处理:若被减数 < 减数,结果为负数,需交换被减数和减数,计算绝对值后添加负号。

示例1 :计算 56789 - 1234

  • 逆序存储后:a = [9,8,7,6,5](56789),b = [4,3,2,1](1234);
  • 逐位计算:
    • 位 0:9-4=5 → 结果位 0=5,借位 = 0;
    • 位 1:8-3=5 → 结果位 1=5,借位 = 0;
    • 位 2:7-2=5 → 结果位 2=5,借位 = 0;
    • 位 3:6-1=5 → 结果位 3=5,借位 = 0;
    • 位 4:5-0=5 → 结果位 4=5,借位 = 0;
  • 结果逆序后:[5,5,5,5,5] → 正序为 55555,与实际结果一致。

示例2(含借位) :计算 10000 - 1

  • 逆序存储后:a = [0,0,0,0,1](10000),b = [1](1);
  • 逐位计算:
    • 位 0:0-1 < 0 → 借位 1,补偿 10 → 10-1=9 → 结果位 0=9,借位 = 1;
    • 位 1:0-0-1 < 0 → 借位 1,补偿 10 → 10-0-1=9 → 结果位 1=9,借位 = 1;
    • 位 2:0-0-1 < 0 → 借位 1,补偿 10 → 10-0-1=9 → 结果位 2=9,借位 = 1;
    • 位 3:0-0-1 < 0 → 借位 1,补偿 10 → 10-0-1=9 → 结果位 3=9,借位 = 1;
    • 位 4:1-0-1=0 → 结果位 4=0,借位 = 0;
  • 结果逆序后:[0,9,9,9,9] → 去掉前导零后为 9999,与实际结果一致。

3.2 实现步骤

  1. 读取两个大整数的字符串 s1(被减数)和 s2(减数);
  2. 判断 s1s2 的大小:
    • s1 < s2:结果为负数,交换 s1s2,标记负号;
    • s1 == s2:直接返回 0
  3. 将字符串逆序转换为向量 ab(低位在前);
  4. 初始化借位borrow = 0,结果向量 res
  5. 循环遍历 a 的每一位(a 长度 ≥ b 长度):
    • a[i](必存在);
    • b[i](若 i 超出 b 的长度,取 0);
    • 计算当前位差值diff = a[i] - b[i] - borrow
    • diff < 0diff += 10,borrow = 1(借位);
    • diff ≥ 0borrow = 0
    • diff 存入 res
  6. 去掉 res 中的前导零(从末尾开始删除连续的 0,至少保留 1 个 0);
  7. 若标记了负号,在结果前添加 -,否则直接逆序输出 res

3.3 关键辅助函数:比较两个大整数的大小

由于 s1s2 是字符串形式的大整数,无法直接用 <> 比较,需实现专门的比较函数:

cpp 复制代码
// 比较两个非负大整数字符串的大小:s1 < s2 返回true,否则返回false
bool isLess(string s1, string s2) {
    if (s1.size() != s2.size()) {
        // 长度不同:长度短的数更小
        return s1.size() < s2.size();
    } else {
        // 长度相同:逐位比较,从高位到低位
        return s1 < s2;
    }
}

3.4 完整代码实现(ACM 模式)

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

// 比较两个非负大整数字符串的大小:s1 < s2 返回true,否则返回false
bool isLess(string s1, string s2) {
    if (s1.size() != s2.size()) {
        return s1.size() < s2.size();
    } else {
        return s1 < s2;
    }
}

// 高精度减法:s1 - s2,返回结果字符串(处理负数和零)
string subtract(string s1, string s2) {
    // 处理特殊情况:s1 == s2
    if (s1 == s2) return "0";
    
    bool isNegative = false;
    // 若s1 < s2,交换两者,标记结果为负数
    if (isLess(s1, s2)) {
        swap(s1, s2);
        isNegative = true;
    }
    
    // 逆序存储到向量(低位在前)
    vector<int> a, b, res;
    for (int i = s1.size() - 1; i >= 0; --i) a.push_back(s1[i] - '0');
    for (int i = s2.size() - 1; i >= 0; --i) b.push_back(s2[i] - '0');
    
    int borrow = 0;  // 借位
    for (int i = 0; i < a.size(); ++i) {
        int x = a[i];
        int y = i < b.size() ? b[i] : 0;
        int diff = x - y - borrow;
        
        if (diff < 0) {
            diff += 10;
            borrow = 1;
        } else {
            borrow = 0;
        }
        
        res.push_back(diff);
    }
    
    // 去掉前导零(从末尾开始删除,保留至少一个零)
    while (res.size() > 1 && res.back() == 0) {
        res.pop_back();
    }
    
    // 转换为字符串(逆序输出)
    string ans;
    if (isNegative) ans += '-';  // 添加负号
    for (int i = res.size() - 1; i >= 0; --i) {
        ans += (res[i] + '0');
    }
    return ans;
}

int main() {
    string s1, s2;
    cin >> s1 >> s2;
    cout << subtract(s1, s2) << endl;
    return 0;
}

3.5 代码解析与测试

代码解析
  • 大小比较isLess函数先比较长度,再逐位比较,确保正确判断两个大整数的大小;
  • 负号处理 :通过 isNegative标记结果符号,交换被减数和减数后计算绝对值;
  • 借位处理:若当前位差值为负,添加 10 补偿,同时设置借位为 1,下一位计算时需减去借位;
  • 前导零处理 :从结果向量末尾删除连续的 0(因为逆序存储,末尾对应高位),避免输出 00123 这类结果。
测试用例
  • 测试用例 1(普通情况):输入:56789 1234 → 输出:55555
  • 测试用例 2(含借位):输入:10000 1 → 输出:9999
  • 测试用例 3(被减数小于减数):输入:1234 56789 → 输出:-55555
  • 测试用例 4(含零):输入:12345 0 → 输出:12345
  • 测试用例 5(前导零):输入:1000 999 → 输出:1

3.6 易错点分析

  1. 未处理借位传递 :如 1000 - 1,若借位未传递到所有低位,会导致结果错误(如 999 而非 999,实际上正确,但还是需要注意多借位情况);
  2. 前导零未删除 :如 1230 - 1200,结果向量为 [0,3,0,0],逆序后为 0030,需删除前导零变为 30
  3. 负号标记错误:交换被减数和减数后,忘记标记负号,导致结果符号错误;
  4. 相等情况未处理 :如 1234 - 1234,未直接返回 0,会导致结果为 0000,虽然后续删除前导零后正确,但还是需要优化逻辑。

四、高精度乘法(High-Precision Multiplication)

4.1 算法原理

高精度乘法的核心是模拟人类列竖式乘法,规则如下(假设乘数为非负整数):

  1. 两个数的最低位对齐
  2. 用乘数的每一位分别与被乘数的每一位相乘,结果的最低位对应当前两位的下标之和(如被乘数第 i 位 × 乘数第 j 位,结果最低位在第 i+j 位);
  3. 逐位累加所有乘积结果,同时处理进位;
  4. 遍历完所有位后,去掉结果中的前导零。

关键结论 :若被乘数长度为 len1,乘数长度为 len2,则乘积的最大长度为 len1 + len2(如 999 × 999 = 998001,长度 3+3=6)。

示例 :计算 123 × 45

  • 逆序存储后:a = [3,2,1](123),b = [5,4](45);
  • 逐位相乘并累加:
    • 乘数第 0 位(5)× 被乘数第 0 位(3)= 15 → 结果第 0 位 = 15;
    • 乘数第 0 位(5)× 被乘数第 1 位(2)= 10 → 结果第 1 位 = 10;
    • 乘数第 0 位(5)× 被乘数第 2 位(1)= 5 → 结果第 2 位 = 5;
    • 乘数第 1 位(4)× 被乘数第 0 位(3)= 12 → 结果第 1 位 = 10+12=22;
    • 乘数第 1 位(4)× 被乘数第 1 位(2)= 8 → 结果第 2 位 = 5+8=13;
    • 乘数第 1 位(4)× 被乘数第 2 位(1)= 4 → 结果第 3 位 = 4;
  • 处理进位:
    • 位 0:15 → 结果位 0=5,进位 1;
    • 位 1:22 + 1=23 → 结果位 1=3,进位 2;
    • 位 2:13 + 2=15 → 结果位 2=5,进位 1;
    • 位 3:4 + 1=5 → 结果位 3=5,进位 0;
  • 结果逆序后:[5,5,3,5] → 正序为 5535,与实际结果一致(123×45=5535)。

4.2 实现步骤

  1. 读取两个大整数的字符串 s1(被乘数)和 s2(乘数);
  2. 处理特殊情况:若其中一个数为 0,直接返回 0
  3. 将字符串逆序转换为向量 ab(低位在前);
  4. 初始化结果向量 res(长度为a.size() + b.size(),初始值为 0);
  5. 双重循环逐位相乘并累加
    • 外层循环遍历乘数 b 的每一位 j
    • 内层循环遍历被乘数 a 的每一位 i
    • res[i + j] += a[i] * b[j](累加乘积到对应位置);
  6. 处理进位
    • 遍历 res 的每一位(从 0 到 res.size()-1);
    • 当前位进位 = res[i] / 10
    • 当前位结果 = res[i] % 10
    • 下一位累加进位(res[i+1] += 进位);
  7. 去掉 res 中的前导零(从末尾开始删除连续的 0,至少保留 1 个 0);
  8. res 逆序输出,得到最终结果。

4.3 完整代码实现(ACM 模式)

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

// 高精度乘法:s1 × s2,s1和s2均为非负整数的字符串形式
string multiply(string s1, string s2) {
    // 处理特殊情况:有一个数为0
    if (s1 == "0" || s2 == "0") return "0";
    
    // 逆序存储到向量(低位在前)
    vector<int> a, b;
    for (int i = s1.size() - 1; i >= 0; --i) a.push_back(s1[i] - '0');
    for (int i = s2.size() - 1; i >= 0; --i) b.push_back(s2[i] - '0');
    
    int len1 = a.size(), len2 = b.size();
    vector<int> res(len1 + len2, 0);  // 结果最大长度为len1+len2
    
    // 逐位相乘并累加
    for (int j = 0; j < len2; ++j) {  // 遍历乘数的每一位
        for (int i = 0; i < len1; ++i) {  // 遍历被乘数的每一位
            res[i + j] += a[i] * b[j];
        }
    }
    
    // 处理进位
    int carry = 0;
    for (int i = 0; i < res.size(); ++i) {
        res[i] += carry;
        carry = res[i] / 10;
        res[i] %= 10;
    }
    
    // 去掉前导零(从末尾开始删除)
    while (res.size() > 1 && res.back() == 0) {
        res.pop_back();
    }
    
    // 转换为字符串(逆序输出)
    string ans;
    for (int i = res.size() - 1; i >= 0; --i) {
        ans += (res[i] + '0');
    }
    return ans;
}

int main() {
    string s1, s2;
    cin >> s1 >> s2;
    cout << multiply(s1, s2) << endl;
    return 0;
}

4.4 代码解析与测试

代码解析
  • 特殊情况处理 :若任一输入为 0,直接返回 0,避免无效计算;
  • 结果初始化 :结果向量长度设为 len1 + len2,确保有足够空间存储所有乘积位;
  • 双重循环相乘 :外层遍历乘数,内层遍历被乘数,乘积结果存入res[i+j],符合竖式乘法的位对齐规则;
  • 进位处理:单独遍历结果向量处理进位,逻辑更清晰,能够避免边乘边处理导致的混乱;
  • 前导零处理:与减法一致,从末尾删除连续的 0,确保结果格式正确。
测试用例
  • 测试用例 1(普通情况):输入:123 45 → 输出:5535
  • 测试用例 2(大数相乘):输入:999999999 999999999 → 输出:999999998000000001
  • 测试用例 3(含零):输入:1234 0 → 输出:0
  • 测试用例 4(长度不一致):输入:12345 678 → 输出:8370910
  • 测试用例 5(单个数字):输入:9 9 → 输出:81

4.5 易错点分析

  1. 结果向量长度不足 :若未设置为len1 + len2,可能导致高位乘积溢出向量,结果错误;
  2. 双重循环顺序错误 :若外层遍历被乘数、内层遍历乘数,逻辑上也是正确的,但需注意 ij 的对应关系,避免位对齐错误;
  3. 进位处理顺序错误:需先累加进位再计算当前位结果,否则会遗漏上一位的进位;
  4. 前导零未删除 :如 100 × 123 = 12300,结果向量逆序后为 00321,删除前导零后为 12300,若未删除则输出 0032
  5. 忘记处理进位 :如 999 × 999,若未处理进位,结果会是 81 81 81(未累加进位),而非 998001

五、高精度除法(High-Precision Division)

5.1 算法原理

高精度除法分为高精度除以低精度 (除数为普通整数,如 12345 ÷ 7)和高精度除以高精度 (除数为大整数,如 123456789 ÷ 9876),本文重点讲解更常用的高精度除以低精度

高精度除以低精度的核心是模拟人类列竖式除法,规则如下(假设被除数为非负整数,除数为正整数):

  1. 从被除数的最高位开始,依次取前 k 位组成一个临时数(确保临时数 ≥ 除数);
  2. 计算临时数 ÷ 除数的商,作为结果的当前位;
  3. 计算临时数 % 除数的余数,作为下一次计算的初始临时数;
  4. 重复步骤 1-3,直到遍历完被除数的所有位;
  5. 去掉结果中的前导零,余数保留(若需要)。

示例 :计算 12345 ÷ 7(商为 1763,余数为 4)

  • 被除数正序存储(高位在前):a = [1,2,3,4,5](12345);
  • 逐位计算:
    • 取第 0 位(1):1 < 7 → 临时数 = 1,商当前位补 0(后续删除前导零);
    • 取第 1 位(2):临时数 = 1×10 + 2=12 → 12 ÷7=1(商位 0=1),余数 = 12%7=5;
    • 取第 2 位(3):临时数 = 5×10 +3=53 → 53÷7=7(商位 1=7),余数 = 53%7=4;
    • 取第 3 位(4):临时数 = 4×10 +4=44 →44÷7=6(商位 2=6),余数 = 44%7=2;
    • 取第 4 位(5):临时数 = 2×10 +5=25 →25÷7=3(商位 3=3),余数 = 25%7=4;
  • 结果商为 [1,7,6,3](正序),余数为 4,与实际结果一致。

5.2 实现步骤(整除 + 取模)

  1. 读取高精度被除数字符串 s 和低精度除数 dd > 0);
  2. 处理特殊情况:
    • s == "0":商为 0,余数为 0
    • d == 1:商为 s,余数为 0
  3. 将被除数字符串正序转换为向量 a(高位在前,便于从高位开始取数);
  4. 初始化临时数temp = 0,商向量 res,余数remainder = 0
  5. 循环遍历 a 的每一位(从高位到低位):
    • temp = temp * 10 + a[i](更新临时数);
    • temp >= d
      • 商位 = temp / d,存入 res
      • temp = temp % d(更新余数);
    • temp < dres 不为空(避免前导零):
      • 商位补 0,存入 res
  6. 处理商的前导零:若 res 为空(被除数 < 除数),商为 0;否则删除开头连续的 0
  7. 余数remainder = temp
  8. 将商向量转换为字符串输出,余数按需输出。

5.3 完整代码实现(ACM 模式,含整除和取模)

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

// 高精度除以低精度:s ÷ d,返回 pair<商, 余数>,d为正整数
pair<string, int> divide(string s, int d) {
    vector<int> a;
    for (char c : s) a.push_back(c - '0');  // 正序存储(高位在前)
    
    vector<int> res;
    int temp = 0;  // 临时数,存储当前待除的部分
    
    for (int i = 0; i < a.size(); ++i) {
        temp = temp * 10 + a[i];  // 更新临时数
        if (temp >= d) {
            // 计算当前商位
            res.push_back(temp / d);
            // 更新临时数为余数
            temp = temp % d;
        } else {
            // 临时数小于除数,若商已开始(res非空),补0
            if (!res.empty()) {
                res.push_back(0);
            }
        }
    }
    
    // 处理商的前导零
    string quotient;
    if (res.empty()) {
        // 被除数 < 除数,商为0
        quotient = "0";
    } else {
        // 删除开头的连续零
        int start = 0;
        while (start < res.size() && res[start] == 0) start++;
        if (start == res.size()) {
            quotient = "0";
        } else {
            for (int i = start; i < res.size(); ++i) {
                quotient += (res[i] + '0');
            }
        }
    }
    
    int remainder = temp;  // 最终余数
    return {quotient, remainder};
}

int main() {
    string s;
    int d;
    cin >> s >> d;
    auto [quotient, remainder] = divide(s, d);
    cout << "商:" << quotient << endl;
    cout << "余数:" << remainder << endl;
    return 0;
}

5.4 代码解析与测试

代码解析
  • 存储方式:被除数采用正序存储(高位在前),符合竖式除法从高位开始计算的习惯;
  • 临时数处理temp 存储当前待除的部分,每次更新为temp*10 + a[i],模拟人类逐位取数的过程;
  • 商位补零 :当临时数小于除数但商已开始(res 非空)时,补 0,避免商为 1763 变成 173(中间位漏 0);
  • 前导零处理 :若商向量为空(被除数 < 除数),商为 0;否则删除开头的连续零,确保格式正确;
  • 余数返回 :最终 temp即为余数,满足题目对取模的需求。
测试用例
  • 测试用例 1(普通情况):输入:12345 7 → 输出:商:1763,余数:4
  • 测试用例 2(被除数 < 除数):输入:123 456 → 输出:商:0,余数:123
  • 测试用例 3(整除):输入:10000 25 → 输出:商:400,余数:0
  • 测试用例 4(含零):输入:0 123 → 输出:商:0,余数:0
  • 测试用例 5(大数除法):输入:999999999999999999 9 → 输出:商:111111111111111111,余数:0

5.5 易错点分析

  1. 存储方式错误:若被除数逆序存储(低位在前),会导致从低位开始取数,与竖式除法逻辑相反,结果错误;
  2. 商位补零遗漏 :如 1000 ÷ 25,计算过程中临时数可能出现小于除数的情况,若未补零,商可能为 4 而非 40
  3. 前导零未处理 :如 1234 ÷ 10,商向量为 [1,2,3,4](实际应为 123,余数 4),需删除前导零?不,1234 ÷10 商为 123,余数 4,代码中 temp 初始为 0,遍历 a = [1,2,3,4]
    • i=0:temp=0×10+1=1 <10 → res 为空,不补零;
    • i=1:temp=1×10+2=12 ≥10 → 商位 1,temp=2 → res=[1];
    • i=2:temp=2×10+3=23 ≥10 → 商位 2,temp=3 → res=[1,2];
    • i=3:temp=3×10+4=34 ≥10 → 商位 3,temp=4 → res=[1,2,3];商为 123,正确,无零;
  4. 除数为零未处理 :代码中假设除数 d > 0,实际应用中需添加除数为零的异常处理;
  5. 临时数溢出 :由于除数是低精度整数(int 范围),temp 的最大值为 d-1(每次取模后),是不会溢出的,无需担心。

六、高精度算法的优化技巧

6.1 存储优化:向量 vs 数组

  • 数组:需预先定义最大长度(如 const int N = 1e6),适合已知数据规模的场景,访问速度快;
  • 向量(vector:动态扩容,无需预先定义长度,适合数据规模未知的场景,代码更灵活。

推荐使用向量:竞赛中数据规模往往不固定,向量的动态扩容特性可避免数组长度不足或浪费空间的问题。

6.2 运算优化:减少冗余操作

  1. 提前处理特殊情况 :如加法中的 0、乘法中的 01、除法中的 01,直接返回结果,避免无效计算;
  2. 合并循环:如乘法中,可将 "逐位相乘" 和 "进位处理" 合并为一个循环,但需注意逻辑清晰,避免出错;
  3. 减少类型转换:字符串转向量时,一次性完成转换,避免循环中重复转换。

6.3 性能优化:处理超大长度数据

当数值长度达到 1e5 位时,高精度运算的时间复杂度会显著增加(加法 / 减法:O (n),乘法:O (nm),除法:O (n)),可以通过以下的方式进行优化:

  1. 分块存储 :将每几位数字(如 4 位)存储为一个整数(如 int 可存储 0-9999),减少循环次数(如 1e5 位数字分块后为 2.5e4 个块);
  2. 快速傅里叶变换(FFT) :将乘法的时间复杂度从 O (nm) 优化到 O (n log n),适合超大规模(如 1e5 位)的乘法运算;
  3. 使用更快的输入输出 :竞赛中用scanf/printf 替代 cin/cout,或关闭同步(ios::sync_with_stdio(false); cin.tie(0);),减少输入输出耗时。

6.4 代码复用:封装成工具类

在竞赛或工程实践中,可将高精度运算封装成工具类,便于复用:

cpp 复制代码
class HighPrecision {
public:
    // 加法
    static string add(string s1, string s2) { ... }
    // 减法
    static string subtract(string s1, string s2) { ... }
    // 乘法
    static string multiply(string s1, string s2) { ... }
    // 除法(高精度÷低精度)
    static pair<string, int> divide(string s, int d) { ... }
    // 比较大小
    static bool isLess(string s1, string s2) { ... }
};

七、实战练习与拓展

7.1 经典练习题(洛谷)

  1. 高精度加法:P1601 A+B Problem(高精)https://www.luogu.com.cn/problem/P1601
  2. 高精度减法:P2142 高精度减法 https://www.luogu.com.cn/problem/P2142
  3. 高精度乘法:P1303 A*B Problem https://www.luogu.com.cn/problem/P1303
  4. 高精度除法:P1480 A/B Problem https://www.luogu.com.cn/problem/P1480

7.2 练习建议

  1. 先掌握基础运算 (加、减、乘、除),再尝试综合题(如阶乘之和、大整数幂等);
  2. 短长度数据 开始测试,逐步过渡到超长数据(如 1e4 位),验证代码的稳定性;
  3. 手动模拟运算过程,对比代码输出结果,找出逻辑错误;
  4. 阅读优秀代码,学习优化技巧(如分块存储、FFT 优化等)。

总结

高精度算法看似复杂,但只要抓住 "模拟竖式" 这一核心,拆分步骤、逐步实现,就能写出稳定可靠的代码。它不仅是编程竞赛的必备技能,也是工程实践中处理超大数值的重要工具。

建议在学习后多做练习,通过实际题目巩固知识点,同时尝试拓展负数运算、高精度除以高精度等进阶内容,进一步提升编程能力。

如果本文对你有帮助,欢迎点赞、收藏、转发,也欢迎在评论区交流讨论~

相关推荐
一只老丸2 小时前
HOT100题打卡第30天——技巧
算法
Bi_BIT2 小时前
代码随想录训练营打卡Day38| 动态规划part06
算法·动态规划
手握风云-2 小时前
回溯剪枝的“减法艺术”:化解超时危机的 “救命稻草”(三)
算法·剪枝
元亓亓亓3 小时前
LeetCode热题100--46. 全排列--中等
算法·leetcode·职场和发展
快手技术3 小时前
从“拦路虎”到“修路工”:基于AhaEdit的广告素材修复
前端·算法·架构
qk学算法3 小时前
力扣滑动窗口题目-76最小覆盖子串&&1234替换子串得到平衡字符串
数据结构·算法·leetcode
小欣加油3 小时前
leetcode 860 柠檬水找零
c++·算法·leetcode·职场和发展·贪心算法
粉色挖掘机4 小时前
矩阵在密码学的应用——希尔密码详解
线性代数·算法·机器学习·密码学
七七七七074 小时前
【计算机网络】UDP协议深度解析:从报文结构到可靠性设计
服务器·网络·网络协议·计算机网络·算法·udp