算法基础篇:(二)基础算法之高精度:突破数据极限
目录
前言
一、高精度算法的本质与核心思想
1.1 什么是高精度算法?
1.2 高精度算法的核心要素
1.3 高精度算法的适用场景
1.4 高精度运算的通用预处理步骤
二、高精度加法(High-Precision Addition)
2.1 算法原理
2.2 实现步骤
2.3 完整代码实现(ACM 模式)
2.4 代码解析与测试
代码解析
测试用例
2.5 易错点分析
三、高精度减法(High-Precision Subtraction)
3.1 算法原理
3.2 实现步骤
3.3 关键辅助函数:比较两个大整数的大小
3.4 完整代码实现(ACM 模式)
3.5 代码解析与测试
代码解析
测试用例
3.6 易错点分析
四、高精度乘法(High-Precision Multiplication)
4.1 算法原理
4.2 实现步骤
4.3 完整代码实现(ACM 模式)
4.4 代码解析与测试
代码解析
测试用例
4.5 易错点分析
五、高精度除法(High-Precision Division)
5.1 算法原理
5.2 实现步骤(整除 + 取模)
5.3 完整代码实现(ACM 模式,含整除和取模)
5.4 代码解析与测试
代码解析
测试用例
5.5 易错点分析
六、高精度算法的优化技巧
6.1 存储优化:向量 vs 数组
6.2 运算优化:减少冗余操作
6.3 性能优化:处理超大长度数据
6.4 代码复用:封装成工具类
七、实战练习与拓展
7.1 经典练习题(洛谷)
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.3 高精度算法的适用场景
- 编程竞赛:NOIP、蓝桥杯等竞赛中的大整数运算题(如高精度加法、乘法、阶乘计算);
- 工程实践:密码学(大质数运算)、金融计算(高精度货币计算)、科学计算(天文 / 物理数据)等;
- 算法学习:巩固数组操作、循环控制、逻辑思维,为复杂算法(如高精度动态规划)铺垫基础。
1.4 高精度运算的通用预处理步骤
无论哪种高精度运算,都需要先完成以下预处理工作,这是保证代码简洁、高效的关键:
- 读取输入:用字符串读取超大数值(避免溢出);
- 逆序存储:将字符串转换为数组 / 向量,且低位在前、高位在后(例如
"1234"存储为[4,3,2,1]); - 初始化结果容器:创建存储结果的数组 / 向量,初始值为 0;
- 处理符号:分离数值的正负号(减法 / 除法需特殊处理)。
为什么要逆序存储?
- 人类在列竖式运算时,是从最低位(最右边)开始逐位计算的,逆序存储后,数组下标 0 对应最低位,下标 1 对应次低位,以此类推,可通过循环从 0 开始逐位处理,无需反向遍历字符串;
- 进位 / 借位是从低位向高位传递,逆序存储有助于直接在数组尾部扩展高位(如加法最高位有进位时,直接在数组末尾添加 1)。
二、高精度加法(High-Precision Addition)
2.1 算法原理
高精度加法的核心是模拟人类列竖式加法,规则如下:
- 两个数的最低位对齐;
- 从最低位开始,逐位相加(包含上一位的进位);
- 当前位结果 =(加数 1 当前位 + 加数 2 当前位 + 进位)% 10;
- 新的进位 =(加数 1 当前位 + 加数 2 当前位 + 进位)/ 10;
- 遍历完所有位后,若进位不为 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 实现步骤
- 读取两个大整数的字符串
s1和s2;- 将字符串逆序转换为向量
a和b(低位在前);- 初始化进位
carry = 0,结果向量res;- 循环遍历
a和b的每一位(遍历长度为两者的最大值):
- 取
a[i](若 i 超出 a 的长度,取 0);- 取
b[i](若 i 超出 b 的长度,取 0);- 计算当前位总和
sum = a[i] + b[i] + carry;- 结果位
sum % 10存入res;- 更新进位
carry = sum / 10;- 遍历结束后,若
carry > 0,将carry存入res;- 将
res逆序输出(去掉前导零,此处加法无负号)。
2.3 完整代码实现(ACM 模式)
#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) {// 取当前位的值,超出长度则为0int 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 易错点分析
- 忘记处理最高位进位:如
999 + 1,若未添加进位,结果会是000而非1000; - 字符串转向量时顺序错误:若正序存储(高位在前),会导致遍历顺序与竖式运算相反,结果错误;
- 未处理长度不一致:如
123 + 4567,若只遍历到较短的长度,会丢失较长数的高位; - 零的处理:输入为
0时,转换后向量为[0],输出时需正常显示,不能省略。
三、高精度减法(High-Precision Subtraction)
3.1 算法原理
高精度减法的核心是模拟人类列竖式减法,规则如下(假设被减数 ≥ 减数,结果非负):
- 两个数的最低位对齐;
- 从最低位开始,逐位相减(若被减数当前位 < 减数当前位,需向高位借位);
- 当前位结果 =(被减数当前位 - 减数当前位 + 借位补偿)% 10;
- 借位标记:若被减数当前位 < 减数当前位,借位为 1(下一位需减 1),否则为 0;
- 遍历完所有位后,去掉结果中的前导零(如结果为
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 实现步骤
- 读取两个大整数的字符串
s1(被减数)和s2(减数);- 判断
s1和s2的大小:
- 若
s1 < s2:结果为负数,交换s1和s2,标记负号;- 若
s1 == s2:直接返回0;- 将字符串逆序转换为向量
a和b(低位在前);- 初始化借位
borrow = 0,结果向量res;- 循环遍历
a的每一位(a长度 ≥b长度):
- 取
a[i](必存在);- 取
b[i](若 i 超出 b 的长度,取 0);- 计算当前位差值
diff = a[i] - b[i] - borrow;- 若
diff < 0:diff += 10,borrow = 1(借位);- 若
diff ≥ 0:borrow = 0;- 将
diff存入res;- 去掉
res中的前导零(从末尾开始删除连续的 0,至少保留 1 个 0);- 若标记了负号,在结果前添加
-,否则直接逆序输出res。
3.3 关键辅助函数:比较两个大整数的大小
由于 s1 和 s2 是字符串形式的大整数,无法直接用 < 或 > 比较,需实现专门的比较函数:
// 比较两个非负大整数字符串的大小: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 模式)
#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 == s2if (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 易错点分析
- 未处理借位传递:如
1000 - 1,若借位未传递到所有低位,会导致结果错误(如999而非999,实际上正确,但还是需要注意多借位情况); - 前导零未删除:如
1230 - 1200,结果向量为[0,3,0,0],逆序后为0030,需删除前导零变为30; - 负号标记错误:交换被减数和减数后,忘记标记负号,导致结果符号错误;
- 相等情况未处理:如
1234 - 1234,未直接返回0,会导致结果为0000,虽然后续删除前导零后正确,但还是需要优化逻辑。
四、高精度乘法(High-Precision Multiplication)
4.1 算法原理
高精度乘法的核心是模拟人类列竖式乘法,规则如下(假设乘数为非负整数):
- 两个数的最低位对齐;
- 用乘数的每一位分别与被乘数的每一位相乘,结果的最低位对应当前两位的下标之和(如被乘数第 i 位 × 乘数第 j 位,结果最低位在第 i+j 位);
- 逐位累加所有乘积结果,同时处理进位;
- 遍历完所有位后,去掉结果中的前导零。
关键结论:若被乘数长度为 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 实现步骤
- 读取两个大整数的字符串
s1(被乘数)和s2(乘数);- 处理特殊情况:若其中一个数为
0,直接返回0;- 将字符串逆序转换为向量
a和b(低位在前);- 初始化结果向量
res(长度为a.size() + b.size(),初始值为 0);- 双重循环逐位相乘并累加:
- 外层循环遍历乘数
b的每一位j;- 内层循环遍历被乘数
a的每一位i;res[i + j] += a[i] * b[j](累加乘积到对应位置);- 处理进位:
- 遍历
res的每一位(从 0 到res.size()-1);- 当前位进位 =
res[i] / 10;- 当前位结果 =
res[i] % 10;- 下一位累加进位(
res[i+1] += 进位);- 去掉
res中的前导零(从末尾开始删除连续的 0,至少保留 1 个 0);- 将
res逆序输出,得到最终结果。
4.3 完整代码实现(ACM 模式)
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;// 高精度乘法:s1 × s2,s1和s2均为非负整数的字符串形式
string multiply(string s1, string s2) {// 处理特殊情况:有一个数为0if (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 易错点分析
- 结果向量长度不足:若未设置为
len1 + len2,可能导致高位乘积溢出向量,结果错误; - 双重循环顺序错误:若外层遍历被乘数、内层遍历乘数,逻辑上也是正确的,但需注意
i和j的对应关系,避免位对齐错误; - 进位处理顺序错误:需先累加进位再计算当前位结果,否则会遗漏上一位的进位;
- 前导零未删除:如
100 × 123 = 12300,结果向量逆序后为00321,删除前导零后为12300,若未删除则输出0032; - 忘记处理进位:如
999 × 999,若未处理进位,结果会是81 81 81(未累加进位),而非998001。
五、高精度除法(High-Precision Division)
5.1 算法原理
高精度除法分为高精度除以低精度(除数为普通整数,如 12345 ÷ 7)和高精度除以高精度(除数为大整数,如 123456789 ÷ 9876),本文重点讲解更常用的高精度除以低精度。
高精度除以低精度的核心是模拟人类列竖式除法,规则如下(假设被除数为非负整数,除数为正整数):
- 从被除数的最高位开始,依次取前 k 位组成一个临时数(确保临时数 ≥ 除数);
- 计算临时数 ÷ 除数的商,作为结果的当前位;
- 计算临时数 % 除数的余数,作为下一次计算的初始临时数;
- 重复步骤 1-3,直到遍历完被除数的所有位;
- 去掉结果中的前导零,余数保留(若需要)。
示例:计算 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 实现步骤(整除 + 取模)
- 读取高精度被除数字符串
s和低精度除数d(d > 0);- 处理特殊情况:
- 若
s == "0":商为0,余数为0;- 若
d == 1:商为s,余数为0;- 将被除数字符串正序转换为向量
a(高位在前,便于从高位开始取数);- 初始化临时数
temp = 0,商向量res,余数remainder = 0;- 循环遍历
a的每一位(从高位到低位):
temp = temp * 10 + a[i](更新临时数);- 若
temp >= d:
- 商位 =
temp / d,存入res;temp = temp % d(更新余数);- 若
temp < d且res不为空(避免前导零):
- 商位补
0,存入res;- 处理商的前导零:若
res为空(被除数 < 除数),商为0;否则删除开头连续的0;- 余数
remainder = temp;- 将商向量转换为字符串输出,余数按需输出。
5.3 完整代码实现(ACM 模式,含整除和取模)
#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非空),补0if (!res.empty()) {res.push_back(0);}}}// 处理商的前导零string quotient;if (res.empty()) {// 被除数 < 除数,商为0quotient = "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 易错点分析
- 存储方式错误:若被除数逆序存储(低位在前),会导致从低位开始取数,与竖式除法逻辑相反,结果错误;
- 商位补零遗漏:如
1000 ÷ 25,计算过程中临时数可能出现小于除数的情况,若未补零,商可能为4而非40; - 前导零未处理:如
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,正确,无零;
- 除数为零未处理:代码中假设除数
d > 0,实际应用中需添加除数为零的异常处理; - 临时数溢出:由于除数是低精度整数(
int范围),temp的最大值为d-1(每次取模后),是不会溢出的,无需担心。
六、高精度算法的优化技巧
6.1 存储优化:向量 vs 数组
- 数组:需预先定义最大长度(如
const int N = 1e6),适合已知数据规模的场景,访问速度快; - 向量(
vector):动态扩容,无需预先定义长度,适合数据规模未知的场景,代码更灵活。
推荐使用向量:竞赛中数据规模往往不固定,向量的动态扩容特性可避免数组长度不足或浪费空间的问题。
6.2 运算优化:减少冗余操作
- 提前处理特殊情况:如加法中的
0、乘法中的0和1、除法中的0和1,直接返回结果,避免无效计算; - 合并循环:如乘法中,可将 “逐位相乘” 和 “进位处理” 合并为一个循环,但需注意逻辑清晰,避免出错;
- 减少类型转换:字符串转向量时,一次性完成转换,避免循环中重复转换。
6.3 性能优化:处理超大长度数据
当数值长度达到 1e5 位时,高精度运算的时间复杂度会显著增加(加法 / 减法:O (n),乘法:O (nm),除法:O (n)),可以通过以下的方式进行优化:
- 分块存储:将每几位数字(如 4 位)存储为一个整数(如
int可存储 0-9999),减少循环次数(如 1e5 位数字分块后为 2.5e4 个块); - 快速傅里叶变换(FFT):将乘法的时间复杂度从 O (nm) 优化到 O (n log n),适合超大规模(如 1e5 位)的乘法运算;
- 使用更快的输入输出:竞赛中用
scanf/printf替代cin/cout,或关闭同步(ios::sync_with_stdio(false); cin.tie(0);),减少输入输出耗时。
6.4 代码复用:封装成工具类
在竞赛或工程实践中,可将高精度运算封装成工具类,便于复用:
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 经典练习题(洛谷)
- 高精度加法: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
7.2 练习建议
- 先掌握基础运算(加、减、乘、除),再尝试综合题(如阶乘之和、大整数幂等);
- 从短长度数据开始测试,逐步过渡到超长数据(如 1e4 位),验证代码的稳定性;
- 手动模拟运算过程,对比代码输出结果,找出逻辑错误;
- 阅读优秀代码,学习优化技巧(如分块存储、FFT 优化等)。
总结
高精度算法看似复杂,但只要抓住 “模拟竖式” 这一核心,拆分步骤、逐步实现,就能写出稳定可靠的代码。它不仅是编程竞赛的必备技能,也是工程实践中处理超大数值的重要工具。
建议在学习后多做练习,通过实际题目巩固知识点,同时尝试拓展负数运算、高精度除以高精度等进阶内容,进一步提升编程能力。
如果本文对你有帮助,欢迎点赞、收藏、转发,也欢迎在评论区交流讨论~
