NOI C++ 课程体系 / 算法篇 / 第9讲
CSP-J 入门级 · 数值处理算法 · m4-09

高精度运算(一)
加法与减法

当 long long 也装不下时,用数组模拟竖式 —— 掌握高精度数的存储、比较、加法与减法,攻克 CSP-J 必考的大数运算。

6 大模块 2 种核心运算 3 个经典案例 5 大易错点 11+20 道练习
本讲 · 第 33/56 讲高精度运算【加法与减法】
模块 01

情景导航

从"装不下的大数"出发,理解高精度加法与减法的必要性。

故事银行清算系统的挑战

一家跨国银行每天要清算全球交易:两笔金额分别为 98765432109876543210 和 12345678901234567890 的交易需要求和。

程序员小李想用 long long 来存,结果程序输出了一个莫名其妙的数!

原因很简单:long long 的上限是 2⁶⁴ − 1 = 18446744073709551615(约 9.2×10¹⁸,只有 19 位),而这两笔 20 位的交易额相加远超上限,发生了溢出。

解决思路:既然一个变量装不下,就用数组——每一位数字占一个格子,模拟小学的竖式计算!

这就是高精度运算:用数组模拟竖式,理论上可以计算任意长度的数。本讲先攻克加法与减法。

①生活类比:小学竖式就是"高精度算法"
📓

数组 = 竖式格子

竖式里每个数位写一个格子里;数组每个元素存一位数字。

→ 数组模拟数位
➡️

从个位算起

竖式从右往左(个位开始)逐位计算。

→ 逆序存储,下标 0 是个位
⬆️

满十进一

加法中本位超过 9 就向前进位。

→ carry 变量
⬇️

不够就借一

减法中本位不够减,就向高位借 1 当 10。

→ borrow 变量
②编程定义:高精度数的存储

高精度数指位数极多、超出 long long 表示范围的整数。C++ 中用 vector<int> 逐位存储。

📌 本讲核心知识地图

板块核心内容关键点
存储与读入字符串读入,逆序存入 vector下标 0 = 个位
比较大小先比位数,再从高位逐位比cmp 函数
高精度加法逐位相加,满十进一carry 进位
高精度减法大减小,不够借一borrow 借位、去前导零
综合应用A+B、A−B、计算 2¹⁰⁰三个经典案例

为什么逆序存储?因为加法中两个数的个位要对齐,而两个数长度可能不同。逆序后个位都在下标 0,从下标 0 开始逐位处理,对齐问题迎刃而解,最后产生的新位(进位)直接 push_back 到尾部即可。

③核心口诀
🔑 口诀一:存储

字符串读入转数组,逆序存放个位前
下标为零是个位,进位新位尾部添

🔑 口诀二:加法

逐位相加加进位,满十留下余数位
商作进位往后传,循环条件别忘了 carry

🔑 口诀三:减法

大数减小才好算,小减大来变符号
不够就向高位借,借一当十记心间
算完去掉前导零,只留一位别删空

模块 02

知识探究

从存储到比较,从加法到减法,一步步搭起高精度运算的完整框架。

①高精度数的存储与读入

大数无法用任何内置类型直接读入,只能当字符串读进来,再逐位拆开存入数组。

C++存储与读入模板
#include <iostream>
#include <vector>
#include <string>
using namespace std;

// 读入一个高精度非负整数,逆序存入 vector
vector<int> readNum() {
    string s;
    cin >> s;
    vector<int> a;
    for (int i = s.size() - 1; i >= 0; i--)
        a.push_back(s[i] - '0');   // 字符数字转整数
    return a;
}

// 逆序输出高精度数
void printNum(vector<int>& a) {
    for (int i = a.size() - 1; i >= 0; i--) cout << a[i];
    cout << endl;
}

两个细节:① s[i] - '0' 把字符 '7' 变成数字 7(利用 ASCII 码连续性);② 存储顺序与读入顺序相反,个位在下标 0。

②比较大小

减法前必须知道谁大谁小。规则和给两个数比大小一样:先比位数,位数多的大;位数相同,从最高位开始逐位比。

C++cmp 函数
// 比较大小:a > b 返回 1,a < b 返回 -1,相等返回 0
int cmp(vector<int>& a, vector<int>& b) {
    if (a.size() != b.size())
        return a.size() > b.size() ? 1 : -1;   // 位数多的数大
    for (int i = a.size() - 1; i >= 0; i--)     // 从高位(下标大)往低位比
        if (a[i] != b[i])
            return a[i] > b[i] ? 1 : -1;
    return 0;   // 完全相等
}
③高精度加法

完全模拟竖式:对应位相加再加上进位,满十留下个位、十位进到下一位。循环条件是三选一:a 没算完、b 没算完、还有进位。

C++add 函数
// 高精度加法:c = a + b(a、b 均为非负整数,逆序存储)
vector<int> add(vector<int>& a, vector<int>& b) {
    vector<int> c;
    int carry = 0;                              // 进位
    for (int i = 0; i < a.size() || i < b.size() || carry; i++) {
        int s = carry;
        if (i < a.size()) s += a[i];
        if (i < b.size()) s += b[i];
        c.push_back(s % 10);                    // 本位
        carry = s / 10;                         // 新进位
    }
    return c;
}

手算模拟:4968 + 372 = ?(逆序存储:a = [8,6,9,4],b = [2,7,3])

位数 ia[i]b[i]s = 和 + 进位本位 push新进位
0(个位)828+2+0 = 1001
1(十位)676+7+1 = 1441
2(百位)939+3+1 = 1331
3(千位)4—4+0+1 = 550

结果(逆序):5,3,4,0 → 正序输出:5340 ✅(4968+372=5340)

④高精度减法

同样模拟竖式,但有两处特殊:不够减时向高位借 1 当 10;算完后必须去掉前导零(最高位多余的 0),例如 1000 − 999 = 001,要变成 1。

C++sub 函数
// 高精度减法:c = a - b(要求 a >= b,逆序存储)
vector<int> sub(vector<int>& a, vector<int>& b) {
    vector<int> c;
    int borrow = 0;                             // 借位标记
    for (int i = 0; i < a.size(); i++) {
        int t = a[i] - borrow - (i < b.size() ? b[i] : 0);
        if (t < 0) { t += 10; borrow = 1; }     // 不够减,借 1 当 10
        else borrow = 0;
        c.push_back(t);
    }
    while (c.size() > 1 && c.back() == 0) c.pop_back();  // 去掉前导零(高位)
    return c;
}

手算模拟:503 − 278 = ?(逆序存储:a = [3,0,5],b = [8,7,2])

位数 ia[i]−借位−b[i]够减?本位借位
0(个位)3 − 0 − 8 = −5❌ 不够−5+10 = 51
1(十位)0 − 1 − 7 = −8❌ 不够−8+10 = 21
2(百位)5 − 1 − 2 = 2✅ 够20

结果(逆序):5,2,2 → 正序输出:225 ✅(503−278=225)

注意:若算出 500−500=0,while 循环因 c.size() > 1 会保留一个 0,不会把结果删空。

⑤案例1:A+B Problem(洛谷 P1601)

题目:输入两个不超过 10⁴⁰ 位的非负整数,输出它们的和。long long 最多存约 19 位数,只能用高精度。

C++A+B 完整程序
#include <iostream>
#include <vector>
#include <string>
using namespace std;

vector<int> add(vector<int>& a, vector<int>& b) {
    vector<int> c;
    int carry = 0;
    for (int i = 0; i < a.size() || i < b.size() || carry; i++) {
        int s = carry;
        if (i < a.size()) s += a[i];
        if (i < b.size()) s += b[i];
        c.push_back(s % 10);
        carry = s / 10;
    }
    return c;
}

int main() {
    string x, y;
    cin >> x >> y;
    vector<int> a, b;
    for (int i = x.size() - 1; i >= 0; i--) a.push_back(x[i] - '0');
    for (int i = y.size() - 1; i >= 0; i--) b.push_back(y[i] - '0');
    vector<int> c = add(a, b);
    for (int i = c.size() - 1; i >= 0; i--) cout << c[i];
    cout << endl;
    return 0;
}

✅ 验证:输入 123456789 和 987654321,程序输出 1111111110,与手算一致。

⑥案例2:A−B Problem(洛谷 P2142)

题目:输入两个整数,输出 A−B 的值。难点:结果可能是负数——先用 cmp 判断,若 a < b 就输出负号再算 b − a。

C++主函数(配合 cmp / sub)
int main() {
    string x, y;
    cin >> x >> y;
    vector<int> a, b;
    for (int i = x.size() - 1; i >= 0; i--) a.push_back(x[i] - '0');
    for (int i = y.size() - 1; i >= 0; i--) b.push_back(y[i] - '0');

    vector<int> c;
    if (cmp(a, b) >= 0) {          // a >= b:直接算 a - b
        c = sub(a, b);
    } else {                        // a < b:输出负号,算 b - a
        cout << "-";
        c = sub(b, a);
    }
    for (int i = c.size() - 1; i >= 0; i--) cout << c[i];
    cout << endl;
    return 0;
}

✅ 验证:输入 5 和 12,cmp 判断 5 < 12,先输出 "-",再算 12−5=7,最终输出 -7,正确。

⑦案例3:计算 2¹⁰⁰

2¹⁰⁰ 是一个 31 位的大数,long long 无能为力。技巧:翻倍 100 次——每次执行 t = t + t,用刚学的高精度加法实现"×2"。

C++用高精加法计算 2^100
int main() {
    vector<int> t;
    t.push_back(1);                    // t = 1
    for (int k = 0; k < 100; k++)
        t = add(t, t);                 // 每次翻倍:t = t + t
    printNum(t);                       // 输出 2 的 100 次方
    return 0;
}

运行结果:

2^100 = 1267650600228229401496703205376(共 31 位)

前几步手算核对:t 从 1 出发,翻倍依次得到 2、4、8、16、32……与 2 的幂完全一致。翻倍 100 次即 2¹⁰⁰。

模块 03

实践应用

三道随堂练习,先自己想,再点开解析对照。

题1加法手算模拟

用高精度加法的流程手算 60606 + 50505,写出每一步的本位与进位,并给出最终结果。

查看参考答案

逆序:a = [6,0,6,0,6],b = [5,0,5,0,5]。

i=0:6+5=11 → 本位 1,进位 1;i=1:0+0+1=1 → 本位 1;i=2:6+5=11 → 本位 1,进位 1;i=3:0+0+1=1 → 本位 1;i=4:6+5=11 → 本位 1,进位 1;i=5:循环条件 carry=1 触发 → 本位 1。

结果:111111(60606+50505=111111)✅

题2减法借位模拟

用高精度减法的流程手算 8000 − 3729,注意连续借位,并思考前导零去了几位。

查看参考答案

逆序:a = [0,0,0,8],b = [9,2,7,3]。

i=0:0−9 不够 → 1,借位 1;i=1:0−1−2=−3 不够 → 7,借位 1;i=2:0−1−7=−8 不够 → 2,借位 1;i=3:8−1−3=4 够 → 4。

逆序结果 [1,7,2,4] → 正序 4271。验证:8000−3729=4271 ✅

本题最高位不是 0,没有前导零;若算 8000−7999=0001,会去掉三个 0,输出 1。

题3综合编程:翻倍多少次超过 10¹⁸

从 1 开始不断翻倍(t = t + t),第一次超过 10¹⁸ 时是第几次翻倍?10¹⁸ 已接近 long long 上限,请用高精度编程验证。

查看参考答案

翻倍 k 次后 t = 2^k。2⁵⁹ = 576460752303423488 < 10¹⁸ < 2⁶⁰ = 1152921504606846976。

第 60 次翻倍时第一次超过 10¹⁸。

程序:把案例3 的 for (k = 0; k < 100; k++) 改成 while (cmp(t, B) < 0) t = add(t, t);(B 为 10¹⁸ 的高精度数),计数器记录次数即可。

模块 04

总结提升

把零散的知识点串成体系,再向前多看一步。

总结核心要点
知识点核心写法易忘点
存储字符串读入,s[i]-'0' 逆序入 vector个位在下标 0
比较先比位数,再从高位逐位比从下标大的一端开始
加法s=carry+a[i]+b[i],本位 s%10,进位 s/10循环条件带上 || carry
减法不够减 +10 并标记借位去前导零、小减大变符号
乘除扩展下一讲:高精×单精、高精÷单精加减是地基
拓展压位优化

上面每一位存一个数字(0~9),其实一个 int 能存下约 9 位数字。让每个数组元素存 9 位数字(即按 10⁹ 进制运算),数组长度缩短为原来的 1/9,运算速度大幅提升。进位改为 carry = s / 1000000000,本位为 s % 1000000000,输出时最高位正常打印、其余位补足 9 位。竞赛中处理上千位的大数时都会使用这一技巧。

思考思考与启发

🤔 思考一:为什么循环条件 i < a.size() || i < b.size() || carry 不能漏掉 carry?——如果漏掉,像 999+1=1000 这种"最高位再进一位"的情况就会丢掉最高位。

🤔 思考二:减法为什么要"大减小"?——竖式里我们从不做"小减大",遇到小减大时先算大减小再加负号,这样借位逻辑永远安全。

💡 启发:高精度的本质是"数据结构 + 模拟":用 vector 组织数据,用人类的竖式规则组织流程。很多看似复杂的问题,都可以拆成"会做的事 + 多做几遍"。

模块 05

学习检测

五大易错点 + 速查表 + 笔记卡,考前快速过一遍。

⚠️5 大易错点

易错点 1:漏掉最后的进位。999 + 1 时,加完所有位后 carry 仍是 1,循环条件必须含 || carry。

易错点 2:忘记去掉前导零。1000 − 999 的逆序结果是 [1,0,0,0] → 正序 0001,必须 while 去零后输出 1。

易错点 3:小数减大数没处理负号。应先 cmp 判断,负数情况输出 "-" 后计算 b − a。

易错点 4:顺序存储导致对齐错乱。若个位存在数组末尾,两个长度不同的数相加时下标对不齐,务必逆序存储。

易错点 5:忘记 s[i]-'0' 转换。直接 push_back(s[i]) 存进去的是 ASCII 码(如 '7' 存成 55),运算结果全错。

📋速查表
操作代码片段说明
读入a.push_back(s[i]-'0')(i 从高位到低位)逆序存储
输出for(i=size-1;i>=0;i--) cout<<a[i];从高位打印
加法本位/进位c.push_back(s%10); carry=s/10;s = 两位和 + carry
减法借位if(t<0){t+=10;borrow=1;}借 1 当 10
去前导零while(c.size()>1&&c.back()==0)c.pop_back();仅减法需要
比较先比 size,再从高位比cmp 函数
📌笔记卡

📐 long long 上限:2⁶³ − 1 = 9223372036854775807(有符号),约 19 位数。位数超过 19 就必须上高精度。

📐 逆序存储:个位在下标 0,对齐、进位、push_back 新位全部顺势而为。

📐 减法三件套:cmp 判大小 → 大减小 → 去前导零(结果可能为负)。

📐 翻倍技巧:t = t + t 等价于 t × 2,用加法就能算出 2¹⁰⁰ 这样的幂。

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

高精度数采用逆序存储(个位在下标 0)的主要原因是()

A. 节省内存   B. 便于两个长度不同的数从个位对齐   C. 输出更快   D. 读入更方便

查看参考答案

答案:B

竖式计算要求个位对齐。逆序存储后个位都在下标 0,逐位处理天然对齐,进位产生的新位直接 push_back 到尾部。

考点:逆序存储后个位统一落在下标 0,逐位运算天然对齐,新产生的高位直接追加到数组尾部。

易错:以为逆序存储是为了“节省空间”;或写程序时在输出前忘记把结果倒序打印。

A2选择题

执行 char c = '7'; int x = c - '0'; 后,x 的值是()

A. 55   B. 7   C. '7'   D. 48

查看参考答案

答案:B. 7

'7' 的 ASCII 码是 55,'0' 是 48,55 − 48 = 7。这就是字符数字转整数的标准方法。

考点:字符数字转整数用 c−'0',因为 '0'~'9' 的 ASCII 码是连续排列的。

易错:写成 (int)c 或 c−'',得到的只是 ASCII 码而不是真正的数值。

A3选择题

计算 99999999 + 1(8 个 9 加 1)时,高精度加法循环共执行()次

A. 8   B. 9   C. 10   D. 7

查看参考答案

答案:B. 9

前 8 次处理原有数位,第 8 次算完 carry 仍为 1,第 9 次由 || carry 触发,产生新的最高位 1。结果为 100000000(9 位)。

考点:需要在原有的 8 位之外再多走一轮,才能把最高位产生的进位写成新的数位。

易错:循环条件只写 i

A4选择题

高精度减法算完后执行 while (c.size() > 1 && c.back() == 0) c.pop_back(); 的目的是()

A. 删除个位   B. 去掉最高位多余的前导零   C. 排序   D. 防止溢出

查看参考答案

答案:B

逆序存储中 back() 是最高位。减法可能产生前导零(如 1000−999=0001),要去掉只留一个有效位;条件 size()>1 保证结果为 0 时不会删空。

考点:逆序存储中 back() 就是最高位,减法结果可能出现前导零,需要清零后再输出。

易错:把条件写成 while(c.back()==0) 而没有 size()>1 的保护,结果为 0 时会把整个数组删空。

🟡 B组 · 提高(4题)

B1计算题

用高精度加法计算:99999999999999999999 + 1 = ?(20 个 9)

查看参考答案

答案:100000000000000000000(1 后面 20 个 0,共 21 位)

每一位 9+1 都产生进位,最后由 carry 再进一位。这是"全 9 加 1"的经典连锁进位。

思路:逐位相加并维护进位 carry,20 个 9 会一路连锁进位到最高位,最终结果是 21 位数。

易错:数错结果的位数(写成 20 位),或误以为结果是 1 后面跟 19 个 0。

B2计算题

用高精度减法计算:10000 − 1 = ?程序输出的过程去掉了几个前导零?

查看参考答案

答案:9999,去掉 0 个前导零

逆序 a=[0,0,0,0,1],b=[1]。i=0:0−1 不够 → 9,借位 1;i=1~3:0−1−0=−1 → 9,借位 1;i=4:1−1=0。逆序结果 [9,9,9,9,0] → 正序 09999,去掉 1 个前导零后输出 9999。(题目第二问答案:1 个)

思路:减法逐位执行“不够就向前借位”,最高位处理完后还要清除结果中的前导零再输出。

易错:忘记借位或借位后没有从更高位减去 1,算出的结果会整体偏大 1 位;输出时也容易把多出来的 0 一起打印。

B3编程题

用高精度翻倍法计算 2⁶⁴,并说明为什么这个数恰好是 unsigned long long 的"溢出点 +1"。

查看参考答案
C++完整参考代码
#include <iostream>
#include <vector>
#include <climits>
using namespace std;

// 高精度加法:数字倒序存放,a[0] 是个位
vector<int> add(const vector<int>& a, const vector<int>& b) {
    vector<int> c;
    int carry = 0;                       // 进位
    for (size_t i = 0; i < a.size() || i < b.size() || carry; i++) {
        int t = carry;
        if (i < a.size()) t += a[i];
        if (i < b.size()) t += b[i];
        c.push_back(t % 10);             // 本位
        carry = t / 10;                  // 进位留到下一轮
    }
    return c;
}

void print(const vector<int>& a) {
    for (int i = (int)a.size() - 1; i >= 0; i--) cout << a[i];
    cout << endl;
}

int main() {
    vector<int> v(1, 1);                 // 从 1 = 2^0 开始
    for (int i = 0; i < 64; i++)         // 翻倍 64 次:v = v + v
        v = add(v, v);
    print(v);                            // 2^64  = 18446744073709551616(20 位)
    cout << ULLONG_MAX << endl;          // 2^64-1 = 18446744073709551615
    return 0;
}

答案:2⁶⁴ = 18446744073709551616(20 位)。unsigned long long 恰好用 64 个二进制位存数,最大值是 2⁶⁴ − 1 = 18446744073709551615。2⁶⁴ 比它大 1,正好装不下——所以从 2⁶⁴ 起必须改用高精度(字符串/数组模拟)。

思路:「翻倍法」就是把当前数和自己相加一次,加 64 次就从 2⁰ = 1 得到 2⁶⁴。串起来看:2⁰=1、2¹=2、2²=4、2³=8……每翻一次倍,位数大约涨 log₁₀2 ≈ 0.301 位,64 次后约 64×0.301 ≈ 19.3 位,实际正好 20 位。因为 2⁶⁴ 已经超出 64 位能表示的范围,普通 long long 会溢出成 0,只能靠高精度数组存每一位。

考点:高精度加法的「倒序存位 + 进位」模板;无符号整数的位宽与取值上限;溢出点的含义(能表示的最大值再加 1 就溢出)。

易错:① 数组/vector 要倒序存(个位在 a[0]),输出时从高位往低位打印;② 循环条件别漏掉最后的 carry,否则最高位的进位会丢;③ 误以为 unsigned long long 能存下 2⁶⁴ —— 它只能存到 2⁶⁴−1;④ 忘了 include <climits> 就用 ULLONG_MAX 会编译不过。

B4编程题

用高精度加法计算 123456789 + 987654321,写出每一步的本位与进位。

查看参考答案
C++完整参考代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    string s1 = "123456789", s2 = "987654321";
    vector<int> a, b;
    for (int i = (int)s1.size() - 1; i >= 0; i--) a.push_back(s1[i] - '0');
    for (int i = (int)s2.size() - 1; i >= 0; i--) b.push_back(s2[i] - '0');

    vector<int> c;
    int carry = 0;
    for (size_t i = 0; i < a.size() || i < b.size() || carry; i++) {
        int d1 = (i < a.size()) ? a[i] : 0;
        int d2 = (i < b.size()) ? b[i] : 0;
        int t = d1 + d2 + carry;         // 本位两数 + 上一位进位
        c.push_back(t % 10);
        int newCarry = t / 10;
        cout << "第" << i + 1 << "位: " << d1 << " + " << d2
             << " + 进位" << carry << " = " << t
             << "  =>  本位 " << t % 10 << ", 进位 " << newCarry << "\n";
        carry = newCarry;
    }

    cout << "结果: ";
    for (int i = (int)c.size() - 1; i >= 0; i--) cout << c[i];
    cout << endl;                        // 1111111110(共 10 位)
    return 0;
}

答案:123456789 + 987654321 = 1111111110(共 10 位)。逐位(含进位):9+1=10 → 本位 0 进 1;8+2+1=11 → 本位 1 进 1;7+3+1=11 → 本位 1 进 1;6+4+1=11 → 本位 1 进 1;5+5+1=11 → 本位 1 进 1;4+6+1=11 → 本位 1 进 1;3+7+1=11 → 本位 1 进 1;2+8+1=11 → 本位 1 进 1;1+9+1=11 → 本位 1 进 1;最后 carry=1 → 最高位 1。

思路:高精度加法就是「模拟小学竖式」:把两个数倒序存进数组,让个位对齐个位,从最低位开始逐位相加。每一位的和等于「本位两数 + 上一位的进位」,写下的数字是和对 10 取余,进位是和整除 10,一直算到两个数组都用完且进位为 0。本题两个数位数相同,恰好每位都产生进位,所以结果整整齐齐是 1111111110。

考点:高精度加法的核心三步——倒序存位、逐位相加带进位、从高位倒着输出;处理两数位数不等时用三元运算符补 0。

易错:① 忘记加上一位的进位 carry;② 循环结束后漏掉最高位的进位(本题正好 carry=1,必须补一位 1,否则只剩 9 位);③ 两个数位数不同时直接按较长者取位,短的那个会越界;④ 输出顺序写反,写成从低位到高位打印。

🔴 C组 · 挑战(3题)

C1编程题

斐波那契数列 F(1)=F(2)=1,F(n)=F(n−1)+F(n−2)。用高精度加法求 F(100)。

查看参考答案
C++完整参考代码
#include <iostream>
#include <vector>
using namespace std;

// 高精度加法:倒序存位,a[0] 是个位
vector<int> add(const vector<int>& a, const vector<int>& b) {
    vector<int> c;
    int carry = 0;
    for (size_t i = 0; i < a.size() || i < b.size() || carry; i++) {
        int t = carry;
        if (i < a.size()) t += a[i];
        if (i < b.size()) t += b[i];
        c.push_back(t % 10);
        carry = t / 10;
    }
    return c;
}

int main() {
    vector<int> f1(1, 1), f2(1, 1);       // F(1) = F(2) = 1
    for (int i = 3; i <= 100; i++) {
        vector<int> f3 = add(f1, f2);     // F(i) = F(i-1) + F(i-2)
        f1 = f2;                          // 滚动:只保留最近两项
        f2 = f3;
    }
    for (int i = (int)f2.size() - 1; i >= 0; i--) cout << f2[i];
    cout << endl;                         // 354224848179261915075(21 位)
    return 0;
}

答案:F(100) = 354224848179261915075(21 位)。

思路:递推式 F(n) = F(n−1) + F(n−2) 没变,只是加法换成了高精度加法。用两个 vector 分别保存相邻两项,每轮算出新的一项后「滚动」一下:f1 ← f2、f2 ← f3,这样空间只要 O(1) 级别的高精度数。从 F(1)、F(2) 出发循环 98 次就得到 F(100)。顺便记:F(93) 约 1.2×10¹⁹ 已经超过 long long 上限 9223372036854775807,所以本题必须用高精度。

考点:高精度加法的复用与「滚动变量」思想;递推(动态规划)的空间优化;long long 的表示范围(约 9.2×10¹⁸,即 F(92) 尚可、F(93) 起溢出)。

易错:① 倒序存位后输出要反向遍历,否则数字全反;② 滚动赋值顺序写错,先写 f2 = f3 再写 f1 = f2 会把 F(n−1) 覆盖掉;③ 循环次数数错:从 3 到 100 共 98 轮;④ 用 int 存结果位数不够、或用 long long 直接算会静默溢出成负数。

C2编程题

"逆序相加"游戏:把一个数与它的逆序数相加,重复直到得到回文数。求 68 需要几步?回文数是多少?用高精度编程验证。

查看参考答案
C++完整参考代码
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

vector<int> add(const vector<int>& a, const vector<int>& b) {
    vector<int> c;
    int carry = 0;
    for (size_t i = 0; i < a.size() || i < b.size() || carry; i++) {
        int t = carry;
        if (i < a.size()) t += a[i];
        if (i < b.size()) t += b[i];
        c.push_back(t % 10);
        carry = t / 10;
    }
    return c;
}

vector<int> toVec(const string& s) {              // 字符串 -> 倒序数字
    vector<int> v;
    for (int i = (int)s.size() - 1; i >= 0; i--) v.push_back(s[i] - '0');
    return v;
}

string toStr(const vector<int>& v) {              // 倒序数字 -> 字符串
    string s;
    for (int i = (int)v.size() - 1; i >= 0; i--) s += char('0' + v[i]);
    return s;
}

int main() {
    string s = "68";
    int step = 0;
    while (true) {
        string r = s;
        reverse(r.begin(), r.end());              // 求逆序数
        if (r == s) break;                        // 已是回文数,结束
        vector<int> sum = add(toVec(s), toVec(r));
        s = toStr(sum);
        step++;
        cout << "第 " << step << " 步: " << s << endl;
    }
    cout << "共 " << step << " 步,回文数 " << s << endl;   // 3 步,1111
    return 0;
}

答案:3 步,回文数是 1111。过程:68 + 86 = 154 → 154 + 451 = 605 → 605 + 506 = 1111(1111 正读反读都一样,停止)。

思路:反复做两件事:① 判断当前数是不是回文——把字符串翻转过来和原串比较,相等就是回文;② 不是回文就把它和逆序数相加。这里每一步的和都可能超过 long long(题目要求通用的高精度做法),所以用高精度加法:先把两个字符串转成倒序数字数组,相加后再拼回字符串。每加一次步骤数 +1,直到出现回文数为止。

考点:高精度加法的应用;回文数的判定(正序串 == 逆序串);字符串与数字数组之间的相互转换;while + break 的循环控制。

易错:① 判定回文时直接翻转原串,把原串也改了(要先用临时变量 r 承接);② add 的结果高位可能有前导 0,转回字符串时要处理(正常进位情况下最高位不会为 0,但通用实现最好去掉);③ 死循环风险:题目说多数数最终会变回文,但像 196 这类「利克瑞尔数」至今没人证明会停,实战要设步数上限;④ 用 long long 直接算,遇到大数会溢出。

C3编程题

综合题:读入两个可能长达 500 位的整数 A、B,输出 A−B(结果可能为负)。写出完整程序。

查看参考答案
C++完整参考代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;

// 比较两个非负整数:a >= b 返回 true
bool ge(const string& a, const string& b) {
    if (a.size() != b.size()) return a.size() > b.size();
    return a >= b;
}

// 高精度减法,要求 a >= b;倒序存位,返回同样倒序的结果
vector<int> sub(const vector<int>& a, const vector<int>& b) {
    vector<int> c;
    int borrow = 0;                              // 借位
    for (size_t i = 0; i < a.size(); i++) {
        int t = a[i] - borrow - ((i < b.size()) ? b[i] : 0);
        if (t < 0) { t += 10; borrow = 1; }      // 不够减,向高位借 1
        else       { borrow = 0; }
        c.push_back(t);
    }
    while (c.size() > 1 && c.back() == 0) c.pop_back();   // 去前导零
    return c;
}

int main() {
    string A, B;
    cin >> A >> B;
    vector<int> a, b;
    for (int i = (int)A.size() - 1; i >= 0; i--) a.push_back(A[i] - '0');
    for (int i = (int)B.size() - 1; i >= 0; i--) b.push_back(B[i] - '0');

    vector<int> c;
    if (ge(A, B)) {
        c = sub(a, b);                           // A >= B,直接相减
    } else {
        cout << "-";                             // A < B,先输出负号
        c = sub(b, a);
    }
    for (int i = (int)c.size() - 1; i >= 0; i--) cout << c[i];
    cout << endl;
    return 0;
}

答案:读入两个可能长达 500 位的整数 A、B,输出 A − B(可为负)。验证:A=1000、B=1 → 输出 999;A=1、B=1000 → 输出 -999;A = B → 输出 0(去前导零后保留一个 0)。

思路:分三步走。第一步比较大小:先比位数,位数多的更大;位数相同时按字符串字典序比较即可(因为都是同样的数字字符)。第二步定符号:若 A ≥ B 就正常算,否则先输出一个负号,再算 B − A。第三步在高精度 sub 里模拟竖式减法:从低位往高位,某一位不够减就向高位借 1(本位加 10、borrow 记 1)。减完记得把高位多余的 0 去掉,全是 0 时保留一个 0。

考点:高精度减法的「借位」实现;大整数的大小比较;负数结果的符号处理;前导零的清除(与加法最大的不同点)。

易错:① 忘记比较大小就直接 sub(a,b),会算出错误结果或下溢;② 借位忘记在下一轮扣掉,或者是先把 a[i] 减了再判断正负;③ 去前导零时把结果 0 也删空,要用 while (c.size() > 1 && c.back() == 0) 兜住;④ 比较时直接比字符串长度就下结论,位数相同的情况没处理;⑤ 结果位数可能和 a 相同,用 a 的长度定循环上界即可。

🏆洛谷原题精选(20道)

以下题目均涉及高精度加法、减法或大数处理,按难度从低到高排列:

1. P1601A+B Problem(高精)入门
2. P2142高精度减法入门
3. P1307数字反转入门
4. P1553数字反转(升级版)入门
5. P1303A*B Problem普及−
6. P1009阶乘之和普及−
7. P1932A+B A-B A*B A/B Problem普及−
8. P2437蜜蜂路线普及−
9. P1255数楼梯普及−
10. P1002过河卒普及−
11. P1044栈普及−
12. P1249最大乘积普及−
13. P1591阶乘数码普及−
14. P1480A/B Problem普及−
15. P1005矩阵取数游戏普及/提高−
16. P10662^k进制数普及/提高−
17. P1012拼数普及−
18. P1603斯诺登的密码普及−
19. P1957口算练习题普及−
20. P1598垂直柱状图普及−