当数字大到 long long 装不下 —— 自己用数组造一个「无限长整数」出来
| 0 - 20 min | 搞清楚「为什么要高精度」(第 1 节) |
| 20 - 70 min | 吃透统一模板的三件事:倒序存 / 逐位算 / 去前导零(第 2 节) |
| 70 - 110 min | 逐个拆解加法、减法、乘法的手推过程(第 3、4 节) |
| 110 - 180 min | 做 4 道题,把模板默写一遍(不看正文) |
| 180 - 215 min | 30 天总结:读错题本、整理个人模板文件、写避坑清单 |
| 215 - 240 min | 做最后的自测(2 小时 AC 四题挑战,可另找时间) |
今天有两件事:上午学高精度(这是最后一个新知识点),下午做整月复盘。 高精度本身不难,但它是「必须会」的东西 —— 因为几乎每场比赛都会有一道大数题, 而且学会之后可以直接封装成模板,以后一辈子都能用。
| 类型 | 字节 | 能表示的最大值 | 十进制位数 |
|---|---|---|---|
short | 2 | 32,767 | 5 位 |
int | 4 | 2,147,483,647(约 2.1×10⁹) | 10 位 |
unsigned int | 4 | 4,294,967,295(约 4.3×10⁹) | 10 位 |
long long | 8 | 9,223,372,036,854,775,807(约 9.2×10¹⁸) | 19 位 |
不管你的电脑多快、编译器多新,C++ 里最大的内置整数类型也就 19 位十进制数。 这是由「用固定字节存数」这件事本身决定的,跟语言无关。
| 题目 | 要求算的东西 | 大概多少位 |
|---|---|---|
| P1601 | a + b,a、b ≤ 10⁵⁰⁰ | 501 位 |
| P1303 | a × b,每个 ≤ 10²⁰⁰⁰ | 4000 位 |
| P2142 | a − b,每个 ≤ 10¹⁰⁰⁸⁶ | 10086 位 |
| P1009 | 1! + 2! + … + 50! | 65 位 |
看 P1009:50! 大约是 3×10⁶⁴,早就飞出 long long 的射程了。
更别说 P2142 的 10086 位数 —— 用 long long 连零头都存不下。
C++ 里整数溢出不会报错、不会崩溃、不会警告(开 -Wall 也不会),
它只会悄悄地把高位丢掉,给你一个看着挺正常但其实是错的数。
所以在竞赛里看到「大数」「阶乘」「乘积」「不超过 10^k」这类字眼,
第一反应应该是判断会不会爆 long long,而不是直接上手写。
既然一个变量装不下 1000 位,那就每一位存一个数组元素。 1000 位的数就用长度 1000 的数组存 —— 数组想开多长开多长,位数就没有上限了。
高精度 = 自己实现「小学竖式」。
你在小学学的加法进位、减法借位、乘法逐位相乘,就是全部的算法。
计算机只是把它一位一位地做一遍而已 —— 不需要任何新数学。
四道题看起来不一样(加法、减法、乘法、阶乘之和),但它们的骨架完全一样。 把骨架固化下来,就得到一套能反复用的模板。
小学竖式是从个位开始算的,因为进位是从低位往高位传的。 数组也应该这样:下标 0 存个位,下标 1 存十位,下标 2 存百位……
| 数 | 正常写法 | 倒序存进数组 |
|---|---|---|
| 12345 | "12345" | a[0]=5, a[1]=4, a[2]=3, a[3]=2, a[4]=1 |
| 1000 | "1000" | a[0]=0, a[1]=0, a[2]=0, a[3]=1 |
如果不倒序,a[0] 是最高位,那么「进位」就要从数组末尾往前传,
而且两个位数不同的数相加时,还得先把它们右对齐——非常麻烦。
倒序之后:a[i] 天然对应「第 i 位」,两个数的同一位就是同一个下标,
进位也天然从下标小往下标大走。整个算法变成了一次线性扫描。
三种运算的逐位公式:
| 运算 | 第 i 位的计算 | 进位 / 借位怎么传 |
|---|---|---|
| 加法 | s = a[i] + b[i] + carry | 本位 = s % 10,进位 = s / 10 |
| 减法 | t = a[i] - b[i] - borrow | 若 t < 0 则 t += 10、借位 = 1;否则借位 = 0 |
| 乘法 | c[i+j] += a[i] * b[j] | 先全部堆着,最后统一从低位到高位处理进位 |
加法和减法边算边进位,一遍扫完就出结果。
乘法不能这样:99 × 99 里,c[1] 会被加两次(9×9 和 9×9),
所以必须先把所有乘积堆到对应位置上,最后再统一处理进位。这一点是初学者最容易写错的地方。
倒序存的时候,结果的高位可能会多出一堆 0。比如 1000 − 999 = 1,
算出来数组是 [1, 0, 0, 0],正着输出就是 0001 —— 错。
while(a.size() > 1 && a.back() == 0) a.pop_back(); // ★ 从高位往下砍 0
> 1 这个条件不能省
如果写成 while(a.back() == 0) a.pop_back();,当结果是数字 0 时,
整个数组会被砍空,然后 a.back() 访问空数组 → 运行错误(RE)。
加 a.size() > 1 的意思是:至少给我留一位。这样 0 就变成「一位的 0」,
输出正好是 0。
背下这套模板,四道题全部够用。强烈建议今天就把它抄进你自己的模板文件里。
#include <bits/stdc++.h> using namespace std; typedef vector<int> Big; // 倒序存放:big[0] 是个位 // ① 字符串 → 倒序数组 Big toBig(const string &s){ Big a; for(int i = (int)s.size() - 1; i >= 0; i--) a.push_back(s[i] - '0'); return a; } // ② 倒序数组 → 字符串(顺便去掉前导零) string toStr(Big a){ while(a.size() > 1 && a.back() == 0) a.pop_back(); // ★ 去前导零 string s; for(int i = (int)a.size() - 1; i >= 0; i--) s += char('0' + a[i]); return s; } // ③ 比较大小:a > b 返回 1,a < b 返回 -1,相等返回 0 int cmpBig(Big a, Big b){ while(a.size() > 1 && a.back() == 0) a.pop_back(); while(b.size() > 1 && b.back() == 0) b.pop_back(); if(a.size() != b.size()) return a.size() < b.size() ? -1 : 1; for(int i = (int)a.size() - 1; i >= 0; i--) if(a[i] != b[i]) return a[i] < b[i] ? -1 : 1; return 0; } // ④ 高精度加法 Big addBig(const Big &a, const Big &b){ Big c; int len = (int)max(a.size(), b.size()); int carry = 0; // 进位 for(int i = 0; i < len || carry; i++){ // ★ carry 也当循环条件 int t = carry; if(i < (int)a.size()) t += a[i]; if(i < (int)b.size()) t += b[i]; c.push_back(t % 10); carry = t / 10; } return c; } // ⑤ 高精度减法(要求调用前保证 a >= b,结果非负) Big subBig(const Big &a, const Big &b){ Big c; int borrow = 0; // 借位 for(int i = 0; i < (int)a.size(); i++){ int t = a[i] - borrow - (i < (int)b.size() ? b[i] : 0); if(t < 0){ t += 10; borrow = 1; } // ★ 不够减就借一位 else borrow = 0; c.push_back(t); } return c; // 前导零交给 toStr 处理 } // ⑥ 高精度乘法 Big mulBig(const Big &a, const Big &b){ if((a.size() == 1 && a[0] == 0) || (b.size() == 1 && b[0] == 0)) return Big(1, 0); // 任何数乘 0 都是 0 Big c(a.size() + b.size(), 0); // 结果最多这么长 for(int i = 0; i < (int)a.size(); i++) for(int j = 0; j < (int)b.size(); j++) c[i + j] += a[i] * b[j]; // ★ 先统统堆到 i+j 位上 int carry = 0; // ★ 最后统一处理进位 for(int i = 0; i < (int)c.size(); i++){ int t = c[i] + carry; c[i] = t % 10; carry = t / 10; } while(c.size() > 1 && c.back() == 0) c.pop_back(); return c; }
vector<int> 而不是定长数组
定长数组要自己维护「当前有多长」这个变量(比如 int len),
传参时还得把长度一起传过去,很容易忘记更新。
vector 自带 size() 和 push_back(),
长度就是数据的长度,不可能对不上。代价只是稍微慢一点点,完全可以接受。
两个数倒序存:1001 → [1,0,0,1],9099 → [9,9,0,9]。
| i | a[i] | b[i] | carry(进) | t = 和 | c[i] = t%10 | carry(出) |
|---|---|---|---|---|---|---|
| 0 | 1 | 9 | 0 | 10 | 0 | 1 |
| 1 | 0 | 9 | 1 | 10 | 0 | 1 |
| 2 | 0 | 0 | 1 | 1 | 1 | 0 |
| 3 | 1 | 9 | 0 | 10 | 0 | 1 |
| 4 | — | — | 1 | 1 | 1 | 0 |
结果数组 [0,0,1,0,1],倒过来读是 10100 ✓(1001 + 9099 = 10100)
两个 4 位数相加,结果可能是 5 位数(比如 9999 + 1 = 10000)。
所以循环条件必须写 i < len || carry:
即使两个数的位都取完了,只要还有进位,就要再补一位。
如果只写 i < len,9999 + 1 就会算成 0000 —— 这就是最经典的「加法进位丢失」bug。
1000 → [0,0,0,1],999 → [9,9,9]。
| i | a[i] | b[i] | borrow(借) | t = 差 | c[i] | borrow(出) |
|---|---|---|---|---|---|---|
| 0 | 0 | 9 | 0 | −9 → +10 = 1 | 1 | 1 |
| 1 | 0 | 9 | 1 | −10 → +10 = 0 | 0 | 1 |
| 2 | 0 | 9 | 1 | −10 → +10 = 0 | 0 | 1 |
| 3 | 1 | 没有这一位 | 1 | 0 | 0 | 0 |
结果数组 [1,0,0,0],正着读是 0001 —— 这里就必须去前导零了,
砍掉高位的 3 个 0,剩下 [1],输出 1 ✓
模板里的 subBig(a, b) 只处理 a ≥ b 的情况,因为「借位」的定义就是「上一位借给下一位」,
如果 a < b,最高位会一直借下去,借出一个负数出来,这套逻辑就崩了。
所以标准写法是:
if(cmpBig(a, b) >= 0) cout << toStr(subBig(a, b)) << endl;
else cout << '-' << toStr(subBig(b, a)) << endl; // 负数:输出负号 + 绝对值的差
先判断符号、再算绝对值 —— 这就是 P2142 的全部难点。
a[i] × b[j] 的结果落在第 i + j 位为什么是 i + j?用位数来想:a[i] 代表的是 a[i] × 10ⁱ,
b[j] 代表 b[j] × 10ʲ,两者相乘就是
(a[i]×b[j]) × 10^(i+j) —— 10 的幂次是 i+j,所以它属于第 i+j 位。
两个数都是 [9, 9]。先开一个长度为 2+2 = 4 的结果数组,全部填 0。
| i | j | 乘积 | 累加到 | c 数组现在的样子 |
|---|---|---|---|---|
| 0 | 0 | 9×9 = 81 | c[0] | [81, 0, 0, 0] |
| 0 | 1 | 9×9 = 81 | c[1] | [81, 81, 0, 0] |
| 1 | 0 | 9×9 = 81 | c[1] | [81, 162, 0, 0] ← 同一个位置被加了两次! |
| 1 | 1 | 9×9 = 81 | c[2] | [81, 162, 81, 0] |
然后从低位到高位统一处理进位:
| i | t = c[i] + carry | c[i] 最终 | carry |
|---|---|---|---|
| 0 | 81 + 0 = 81 | 1 | 8 |
| 1 | 162 + 8 = 170 | 0 | 17 |
| 2 | 81 + 17 = 98 | 8 | 9 |
| 3 | 0 + 9 = 9 | 9 | 0 |
结果数组 [1,0,8,9],倒过来读是 9801 ✓(99 × 99 = 9801)
1. 边乘边进位。如果你在 c[i+j] += a[i]*b[j] 的同时就处理进位,
后面还会再往 c[i+j] 里加东西,进位就乱了。
必须先全部堆好,再统一进位(因为 c[i+j] 会被访问多次)。
2. 结果是 0 的情况。0 × 12345,两层循环算出来 c 全是 0,
去前导零之后只剩一个 0,输出 0,其实是对的 —— 但如果结果数组长度为 0 就会崩。
模板里我直接特判了「任何数乘 0 都是 0」,最省心。
m 位数 × n 位数,结果最多 m + n 位(比如 99 × 99 是 4 位)。
所以 Big c(a.size() + b.size(), 0); 一定够,多一个都不用。
比如 2000 位数相乘,结果数组开 4000 —— 和实际输出 4000 位正好吻合(我在实测里验证过)。
如果乘数是一个普通的 int(比如 P1009 里的 i! = (i−1)! × i,
这里的 i 只是个不超过 50 的小整数),那就根本不用两层循环:
Big mulInt(const Big &a, int k){
if(k == 0) return Big(1, 0);
Big c;
long long carry = 0;
for(int i = 0; i < (int)a.size() || carry; i++){
long long t = carry;
if(i < (int)a.size()) t += (long long)a[i] * k; // ★ 一位对上整个 k
c.push_back((int)(t % 10));
carry = t / 10;
}
while(c.size() > 1 && c.back() == 0) c.pop_back();
return c;
}
它其实就是「把乘法退化成加法」:每一位乘 k 再传进位,一遍线性扫描结束。 P1009 用它比自己写高精度乘法快得多也好写得多。
carry 必须用 long long
a[i] * k 最大是 9 × 50 = 450,加上进位也远没超 int。
但如果 k 是个更大的数(比如 10⁹),9 × 10⁹ 就爆 int 了。
养成习惯:乘法里所有中间变量一律用 long long。
下面每道题的代码都用 g++ -std=c++14 -O2 -Wall
实际编译并运行验证过。为了每份代码都能独立编译,我只贴出本题用得到的那几个函数
(完整的「六件套」在第 2 节)。
题意:给定两个非负整数 a、b,求 a + b。题目明确说了不用考虑负数。
20% 的数据 a, b ≤ 10⁹;40% 的数据 a, b ≤ 10¹⁸;100% 的数据 0 ≤ a, b ≤ 10⁵⁰⁰。
看清楚:10⁵⁰⁰ 是 1 后面 500 个 0,也就是最多 501 位。
long long 只有 19 位,所以 40% 那两档用 long long 能过,
但满分必须用高精度。这就是"子任务分"的典型结构 —— 比赛时不会写高精度也先把 40 分拿了。
另外注意 a、b 都可以是 0,别漏了 0 + 0 这种情况。
就是第 3 节手推的那套:读成字符串 → 倒序存 → 逐位相加并传进位 → 去前导零 → 倒着输出拼回字符串。
关键的一句是循环条件 i < len || carry,它保证了「最高位进位」不会丢。
1 1
2
1001 9099
10100
#include <iostream> #include <string> #include <vector> #include <algorithm> using namespace std; typedef vector<int> Big; // 倒序存放:big[0] 是个位 Big toBig(const string &s){ Big a; for(int i = (int)s.size() - 1; i >= 0; i--) a.push_back(s[i] - '0'); return a; } string toStr(Big a){ while(a.size() > 1 && a.back() == 0) a.pop_back(); // ★ 去前导零 string s; for(int i = (int)a.size() - 1; i >= 0; i--) s += char('0' + a[i]); return s; } Big addBig(const Big &a, const Big &b){ Big c; int len = (int)max(a.size(), b.size()); int carry = 0; // 进位 for(int i = 0; i < len || carry; i++){ // ★ carry 也当循环条件 int t = carry; if(i < (int)a.size()) t += a[i]; if(i < (int)b.size()) t += b[i]; c.push_back(t % 10); carry = t / 10; } return c; } int main(){ string sa, sb; cin >> sa >> sb; cout << toStr(addBig(toBig(sa), toBig(sb))) << endl; return 0; }
官方样例 1 1 / 1 → 2 完全一致
官方样例 2 1001 / 9099 → 10100 完全一致
边界 0 / 0 → 0 正确(去前导零的 > 1 条件生效,没把数组砍空)
边界 10⁵⁰⁰ / 1(1 后面 500 个 0 加上 1)→ 输出 501 位,开头是 1000…、结尾是 …0001 正确
边界 500 个 9 加 1 → 输出 1 后面 500 个 0,共 501 位 正确(这是纯进位测试,验证了 || carry 那一句)
随机对拍:用 40 组随机大数(位数 1 ~ 60)和 PowerShell 的 bigint 精确值逐一比对, 加法 40 组全部一致(同一次对拍里还顺带验证了乘法和减法,见下面两题)。
|| carry:999 + 1 会输出 000。cin >> long long 读:501 位的数读进来直接失败(这是 40 分和 100 分的分界线)。1 + 1 会输出 02。'0':存进去的是 ASCII 码 48 ~ 57,算出来全是错的。00123。
我的模板对这种情况天然兼容(去前导零在 toStr 里做了,输入有前导零也不影响求和)。题意:给定两个正整数 a、b,求 a − b。如果结果是负数,要输出负号。
20% 的数据 a、b 在 long long 存储范围内;
100% 的数据 0 < a, b ≤ 10¹⁰⁰⁸⁶。
注意两点:① 10¹⁰⁰⁸⁶ 是 10087 位数,相当大(这就是为什么必须用 vector 而不是固定数组 —— 也有人直接开 int[10100]);
② 题目没说 a ≥ b,而且输出格式里专门写了「如果 a − b < 0,请输出负号」,
所以结果可能是负的,必须处理。
减法模板只能算「大减小」,所以分两步:
cmpBig 比较 a 和 b 的大小。subBig(a, b);否则输出一个负号,再输出 subBig(b, a)。cmpBig 怎么写才不出错
不能直接比字符串("100" < "99" 是真的,但 100 > 99)。正确的比较顺序是:
① 先去掉两边的前导零(否则 "099" 会被当成 3 位数)
② 位数不同 → 位数多的更大
③ 位数相同 → 从最高位往下逐位比,第一个不同的位决定大小
④ 全都相同 → 相等
2 1
1
#include <iostream>
#include <string>
#include <vector>
using namespace std;
typedef vector<int> Big;
Big toBig(const string &s){
Big a;
for(int i = (int)s.size() - 1; i >= 0; i--) a.push_back(s[i] - '0');
return a;
}
string toStr(Big a){
while(a.size() > 1 && a.back() == 0) a.pop_back();
string s;
for(int i = (int)a.size() - 1; i >= 0; i--) s += char('0' + a[i]);
return s;
}
// 比较大小(先去掉前导零再比):a > b 返回 1,a < b 返回 -1,相等返回 0
int cmpBig(Big a, Big b){
while(a.size() > 1 && a.back() == 0) a.pop_back(); // ★ 必须先规范化
while(b.size() > 1 && b.back() == 0) b.pop_back();
if(a.size() != b.size()) return a.size() < b.size() ? -1 : 1; // 位数多的更大
for(int i = (int)a.size() - 1; i >= 0; i--) // 从高位往下逐位比
if(a[i] != b[i]) return a[i] < b[i] ? -1 : 1;
return 0;
}
// 高精度减法(要求已经保证 a >= b,结果非负)
Big subBig(const Big &a, const Big &b){
Big c;
int borrow = 0; // 借位
for(int i = 0; i < (int)a.size(); i++){
int t = a[i] - borrow - (i < (int)b.size() ? b[i] : 0);
if(t < 0){ t += 10; borrow = 1; } // ★ 不够减,向前借一位
else borrow = 0;
c.push_back(t);
}
return c; // 前导零交给 toStr 处理
}
int main(){
string sa, sb;
cin >> sa >> sb;
Big a = toBig(sa), b = toBig(sb);
if(cmpBig(a, b) >= 0){
cout << toStr(subBig(a, b)) << endl;
}else{
cout << '-' << toStr(subBig(b, a)) << endl; // ★ 负号 + 绝对值的差
}
return 0;
}
官方样例 2 / 1 → 1 完全一致
前导零测试 1000 / 999 → 1 正确(不是 0001)
负数测试 1 / 2 → -1 正确(负号输出正确)
相等测试 1000000 / 1000000 → 0 正确(不是空输出、也不崩)
最大规模 10¹⁰⁰⁸⁶ / 1(a 是 1 后面 10086 个 0,即 10087 位数,正好是题面上限)
→ 输出 10086 位,每一位都是 9,与 10¹⁰⁰⁸⁶ − 1 的理论值逐字符精确一致 正确
随机对拍:40 组随机大数(位数 1 ~ 60,故意让 a 可能小于 b)和 bigint 精确值比对, 减法 40 组全部一致,含负数结果也正确。
"100" < "99",位数不同的比较一定错。必须比长度。"099" 长度是 3,会被误判成比 "100" 还大吗?
其实 "099" vs "100" 长度相同,逐位比第一位 0 < 1 就对了;
但 "099" vs "9" 长度 3 > 1 就会误判成 099 > 9。所以 先规范化。subBig 不判符号,会输出一堆乱码(借位借穿了)。1 − 1 = 0,cmpBig 返回 0,走的是 >= 0 分支,
输出 0,不会输出 -0。如果你写成 if(cmpBig(a,b) > 0),
那么 1 − 1 就会走 else 分支输出 -0 —— 这是个很隐蔽的坑。t = a[i] - b[i] 少了 - borrow,结果会偏大。题意:给出两个非负整数,求它们的乘积。
每个非负整数不超过 10²⁰⁰⁰。
10²⁰⁰⁰ 是 2001 位数,乘起来结果是 4000 位左右。 题目说「非负」,所以 0 也要能正确处理。
另外这题没有子任务分,long long 一分都拿不到,必须老老实实写高精度。
套第 4 节的乘法模板。三个关键点再强调一次:
a.size() + b.size() 位 —— m 位数乘 n 位数最多 m + n 位。c[i + j] += a[i] * b[j],先全部堆着。1 2
2
#include <iostream>
#include <string>
#include <vector>
using namespace std;
typedef vector<int> Big;
Big toBig(const string &s){
Big a;
for(int i = (int)s.size() - 1; i >= 0; i--) a.push_back(s[i] - '0');
return a;
}
string toStr(Big a){
while(a.size() > 1 && a.back() == 0) a.pop_back();
string s;
for(int i = (int)a.size() - 1; i >= 0; i--) s += char('0' + a[i]);
return s;
}
// 高精度乘法
Big mulBig(const Big &a, const Big &b){
if((a.size() == 1 && a[0] == 0) || (b.size() == 1 && b[0] == 0))
return Big(1, 0); // ★ 任何数乘 0 都是 0
Big c(a.size() + b.size(), 0); // 结果最多这么长
for(int i = 0; i < (int)a.size(); i++)
for(int j = 0; j < (int)b.size(); j++)
c[i + j] += a[i] * b[j]; // ★ 先统统堆到 i+j 位上
int carry = 0; // ★ 最后统一处理进位
for(int i = 0; i < (int)c.size(); i++){
int t = c[i] + carry;
c[i] = t % 10;
carry = t / 10;
}
while(c.size() > 1 && c.back() == 0) c.pop_back();
return c;
}
int main(){
string sa, sb;
cin >> sa >> sb;
cout << toStr(mulBig(toBig(sa), toBig(sb))) << endl;
return 0;
}
官方样例 1 / 2 → 2 完全一致
边界 0 / 999 → 0 正确(乘 0 特判生效)
999 / 999 → 998001 正确
20 个 9 × 20 个 9 → 9999999999999999999800000000000000000001(40 位) 正确
(验算:(10²⁰−1)² = 10⁴⁰ − 2×10²⁰ + 1)
最大规模 2000 个 9 × 2000 个 9 → 输出 4000 位,和理论值
(10²⁰⁰⁰−1)² = 10⁴⁰⁰⁰ − 2×10²⁰⁰⁰ + 1 的字符串逐字符精确比对通过(1999 个 9、1 个 8、1999 个 0、1 个 1)。
2000 个 7 × 1 → 输出 2000 位,和输入完全一样 正确(边界:乘数是 1 位数)
随机对拍:40 组随机大数(位数 1 ~ 60)与 bigint 精确值比对,乘法 40 组全部一致。
c[i+j] 会被加多次,先堆后进位是硬性要求。a.size() + b.size() - 1 在最高位有进位时会越界。
直接开 a.size() + b.size(),多一位不用白不用。vector 最省心。toStr 里的 > 1 保护,数组会被砍空。题意:用高精度算出 S = 1! + 2! + 3! + … + n!,n ≤ 50。
1 ≤ n ≤ 50。
看起来很小,但 50! 大约是 3.04×10⁶⁴,有 65 位 —— long long 差得远。
题目里还有一句提醒很值得注意:《深入浅出基础篇》拿这题当例题,但书上数据只有 n ≤ 20,
用书里的代码过不了本题。 20! 约 2.4×10¹⁸ 刚好卡在 long long 边缘 ——
这正是「数据范围决定算法」的活教材。
不要想着「先算 50! 再回头算 49!」—— 那样每次都要重算一遍乘法,很浪费。递推着来:
| 步骤 | 做什么 | 用哪个模板函数 |
|---|---|---|
| 初始 | fact = 1(代表 0!),sum = 0 | — |
| 循环 i = 1…n | fact = fact × i → 得到 i! | mulInt(高精度 × 低精度) |
| 同上 | sum = sum + fact | addBig |
| 结束 | 输出 sum | toStr |
因为乘数 i 最大就是 50,是个普通 int。
mulInt 只要一遍线性扫描(O(位数)),而 mulBig 要两层循环(O(位数²))。
n = 50 时位数才 65,两者都很快,但 「能用低精度就别上高精度」是个好习惯 ——
如果哪天要算 100000!,差距就是几万倍。
| i | fact = fact × i | sum = sum + fact |
|---|---|---|
| 开始 | 1 | 0 |
| 1 | 1 × 1 = 1 | 0 + 1 = 1 |
| 2 | 1 × 2 = 2 | 1 + 2 = 3 |
| 3 | 2 × 3 = 6 | 3 + 6 = 9 |
1! + 2! + 3! = 1 + 2 + 6 = 9 ✓ 正好是样例输出。
3
9
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
typedef vector<int> Big;
Big toBig(const string &s){
Big a;
for(int i = (int)s.size() - 1; i >= 0; i--) a.push_back(s[i] - '0');
return a;
}
string toStr(Big a){
while(a.size() > 1 && a.back() == 0) a.pop_back();
string s;
for(int i = (int)a.size() - 1; i >= 0; i--) s += char('0' + a[i]);
return s;
}
// 高精度加法
Big addBig(const Big &a, const Big &b){
Big c;
int len = (int)max(a.size(), b.size());
int carry = 0;
for(int i = 0; i < len || carry; i++){
int t = carry;
if(i < (int)a.size()) t += a[i];
if(i < (int)b.size()) t += b[i];
c.push_back(t % 10);
carry = t / 10;
}
return c;
}
// 高精度 × 低精度(k 是一个普通的 int)
Big mulInt(const Big &a, int k){
if(k == 0) return Big(1, 0);
Big c;
long long carry = 0;
for(int i = 0; i < (int)a.size() || carry; i++){
long long t = carry;
if(i < (int)a.size()) t += (long long)a[i] * k; // ★ 一位对上整个 k
c.push_back((int)(t % 10));
carry = t / 10;
}
while(c.size() > 1 && c.back() == 0) c.pop_back();
return c;
}
int main(){
int n;
cin >> n;
Big fact = toBig("1"); // 当前的阶乘 i!
Big sum = toBig("0"); // 累加和
for(int i = 1; i <= n; i++){
fact = mulInt(fact, i); // i! = (i-1)! * i
sum = addBig(sum, fact); // 累加
}
cout << toStr(sum) << endl;
return 0;
}
官方样例 3 → 9 完全一致
边界 n = 1 → 1 正确
n = 50(最大数据) → 输出 65 位:
31035053229546199656252032972759319953190362094566672920420940313
全程对拍:把 n = 1, 2, 3, …, 50 全部 50 个答案都算出来, 和用任意精度整数(bigint)独立算出的精确值逐一比对,50 个全部一致,0 个失败。
fact 初始化成 0:那样整个循环乘出来一直是 0,答案也是 0。
必须初始成 1(0! = 1 是数学定义)。for i: for j = 1..i: fact *= j,
虽然 n ≤ 50 也能过,但完全没必要 —— 用递推 i! = (i−1)! × i 一遍就够。int:k 一大就溢出了。统一用 long long。sum 也要用高精度:50 个 65 位的数相加,肯定也是高精度。| # | 错误写法 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | 正序存(a[0] 是最高位) |
进位方向反了,两个不同位数的数还得右对齐,代码立刻复杂一倍 | 低位在数组前面:a[0] = 个位 |
| 2 | 转数组时忘了 - '0' |
存的是 ASCII 码 48~57,算出来全是垃圾 | a.push_back(s[i] - '0') |
| 3 | 加法循环只写 i < len |
999 + 1 = 000,最高位进位丢掉了 |
i < len || carry |
| 4 | 去前导零写 while(a.back()==0) pop_back() |
结果是 0 时数组被砍空 → RE | 加保护:while(a.size() > 1 && a.back()==0) |
| 5 | 忘了去前导零 | 1000−999 输出 0001 |
统一在 toStr 里做,所有输出都过一遍 |
| 6 | 减法不比较大小 | a < b 时借位借穿,输出乱码 | 先 cmpBig,负数就输出负号 + subBig(b,a) |
| 7 | 用字符串直接比大小 | "100" < "99" 恒为真,判断全反 |
先比位数,位数相同再从高位逐位比 |
| 8 | 乘法边乘边进位 | c[i+j] 后面还会被累加,进位全乱 |
先全部 c[i+j] += a[i]*b[j],最后统一进位 |
| 9 | 乘法结果数组开 m+n-1 |
最高位有进位时越界 | 开 a.size() + b.size() |
| 10 | P1009 的 fact 初始化成 0 |
答案恒为 0 | 初始化成 1(0! = 1) |
| 11 | 中间变量用 int |
乘法/进位悄悄溢出 | 高精度里的乘法、进位一律 long long |
| 12 | 看到大数才想起 long long 不够 | WA 半天找不到原因(溢出无提示) | 读到「≤10^k」先估位数,超过 18 位就要高精度 |
答:因为进位和借位都是从低位往高位传的。
倒序存放(a[0] = 个位)之后,第 i 位就是下标 i,进位只需要「往后一个下标传」,
整个算法变成一次从下标 0 到末尾的线性扫描。
顺带还解决了位数对齐问题:1234 + 56,倒序后是 [4,3,2,1] 和 [6,5],
第二位直接取 a[1]=3、b[1]=5,天然的个位对齐个位。如果正序存,还得先把两个数右对齐。
9999 + 1 用模板算,结果数组会是什么?答:[0, 0, 0, 0, 1],倒过来读是 10000。
9999 → [9,9,9,9],1 → [1]。
i=0: 9+1 = 10 → c[0]=0, carry=1
i=1: 9+0+1 = 10 → c[1]=0, carry=1
i=2: 9+0+1 = 10 → c[2]=0, carry=1
i=3: 9+0+1 = 10 → c[3]=0, carry=1
i=4: i < len(4) 不成立,但 carry = 1 成立 → 再补一位 c[4]=1
如果循环条件少了 || carry,第 5 位就不会产生,输出变成 0000。
12 × 34,把乘法的两个阶段都写出来。答:12 → [2,1],34 → [4,3]。结果数组长度 2+2 = 4,初始 [0,0,0,0]。
阶段一 · 堆乘积:
得到 [8, 10, 3, 0]。注意 c[1] = 10 已经超过 9 了,这是正常的,进位留到第二阶段。
阶段二 · 统一进位:
[8,0,4,0] → 去前导零 → [8,0,4] → 倒读 408 ✓(12 × 34 = 408)
mulInt 而不直接用 mulBig?答:因为乘数 i 是个小整数(≤ 50),不是高精度数。
mulInt 把「每一位乘 k」一次扫完,复杂度 O(位数);
mulBig 要两层嵌套循环,复杂度 O(位数²)。
把 i 硬转成高精度再乘,白白多算一位 × 一位的乘法。
n = 50 时两者都很快,但习惯要养对:能用低精度的场合就别上高精度。
0 × 0 用乘法模板算,会发生什么?答:模板里第一句就特判了:a[0]==0 || b[0]==0 时直接返回 Big(1, 0),
也就是「长度为 1、内容为 0」的数组。经过 toStr 输出就是 0。
如果不特判:c 长度是 1+1 = 2,全是 0;去前导零后剩 [0],输出也是 0 —— 其实也对。
特判主要是为了省掉无谓的循环,同时避免「长度为 0 的数组」这种危险状态。
【高精度 · 三件事】 ① 倒序存 a[0] 是个位,a[1] 是十位 … ② 逐位算 + 进位 / - 借位 / × 先堆后排 ③ 去前导零 while(size > 1 && back() == 0) pop_back(); 【为什么倒序】 进位、借位都是从低位往高位传 倒序后第 i 位 = 下标 i,天然对齐,线性扫一遍 【六件套】 Big toBig(const string &s); // 字符串 → 倒序数组 string toStr(Big a); // 倒序数组 → 字符串(去前导零) int cmpBig(Big a, Big b); // 先比位数,再逐位比 Big addBig(const Big &a, const Big &b); Big subBig(const Big &a, const Big &b); // 要求 a >= b Big mulBig(const Big &a, const Big &b); Big mulInt(const Big &a, int k); // 高精度 × 低精度 【加法】 carry = 0; for(i = 0; i < len || carry; i++){ t = carry + a[i] + b[i]; c.push_back(t % 10); carry = t / 10; } // ★ 少了 "|| carry",999+1 会变成 000 【减法】 borrow = 0; for(i = 0; i < a.size(); i++){ t = a[i] - borrow - b[i]; if(t < 0){ t += 10; borrow = 1; } else borrow = 0; c.push_back(t); } // 先用 cmpBig 判符号:a<b 就输出 '-' 再算 subBig(b,a) // ★ 相等时走 >= 分支,不会输出 -0 【乘法】 // 阶段一:先堆,不进位 c.assign(a.size() + b.size(), 0); for(i) for(j) c[i + j] += a[i] * b[j]; // 阶段二:统一进位 carry = 0; for(i = 0; i < c.size(); i++){ t = c[i] + carry; c[i] = t % 10; carry = t / 10; } // ★ a[i]*b[j] 落在第 i+j 位 // ★ 结果最多 m+n 位 【什么时候该上高精度】 ≤ 9 位 → int ≤ 18 位 → long long > 18 位 → 高精度 看到「阶乘」「大数乘积」「≤10^k 且 k ≥ 19」→ 立刻警觉 【P1009 阶乘之和】 fact = 1; sum = 0; for(i = 1..n){ fact = mulInt(fact, i); sum = addBig(sum, fact); } // ★ fact 初始是 1 不是 0(0! = 1)
恭喜你走到这里。回头看,30 天前你还在为「输入两个数输出它们的和」调格式, 今天你已经能手写高精度乘法、能分析二分答案的单调性、能设计出背包的状态转移方程了。 这不是一点点进步,这是换了一个人。
| 阶段 | 主题 | 核心收获 |
|---|---|---|
| Day 1–6 | 语法与数组基本功 评测机制与输入输出 / 循环与累加(整数浮点陷阱)/ while 与终止条件 / 多重循环与图形输出 / 一维数组 / 桶计数 |
把「想法翻译成代码」这件事练到不用想;理解数据范围决定变量类型;学会「用值当下标」这个影响深远的思路 |
| Day 7–14 | 二维数组、字符串、函数与贪心 二维数组 / 字符与字符串 / 数字进制格式 / 函数 / 结构体 / 排序 / 贪心入门 / 贪心进阶 |
从「写一段代码」升级到「写一个模块」;掌握 sort 和自定义比较函数;第一次接触「证明贪心正确性」这件事 |
| Day 15–21 | 前缀和、递归、搜索 前缀和与差分 / 递归 / 递推与记忆化 / DFS 入门 / DFS 回溯 / BFS 入门 / BFS 进阶 |
学会「用预处理换查询速度」;掌握递归的思考方式;能独立写出 DFS 回溯和 BFS 最短路 —— 这是第一次真正意义上的算法 |
| Day 22–28 | 二分、动态规划、栈与队列 二分查找 / 二分答案 / 二分综合 / 线性 DP / 0-1 背包 / 完全背包 / 栈与队列 |
掌握「二分答案」这个把求解变判定的强套路;跨过 DP 这道大坎(状态设计 + 转移方程 + 边界);熟悉 STL 容器 |
| Day 29–30 | 模拟赛 + 高精度 限时模拟赛 / 高精度加减乘、阶乘之和 |
把前面所有基本功在压力下用一遍;补上最后一块拼图 —— 没有内置类型够用时的兜底手段 |
主线一:用空间换时间。桶计数是(用数组换 O(1) 查询)、前缀和是(用数组换 O(1) 区间和)、 记忆化搜索是(用数组换重复子问题)、DP 也是(用数组换指数级枚举)。这是算法里最核心的交易。
主线二:把大问题切成小问题。递归、分治、二分、DP —— 全都是这一个思想的不同外衣。
主线三:先把边界想清楚再写代码。数组开多大、从 0 还是 1 开始、 空集怎么处理、n = 0 / n = 1 怎么办 —— 你这一个月踩的坑,十有八九都在这里。
long long 却开了 int<= 或 < 搞反| 题号 | 题目 | 考什么 | 难度 |
|---|---|---|---|
| P1047 | 校门外的树 | 数组标记(Day 5–6 的基本功) | 入门 |
| P1094 | 纪念品分组 | 排序 + 贪心双指针(Day 12–13) | 普及- |
| P1605 | 迷宫 | DFS 回溯 + 网格(Day 19) | 普及- |
| P1048 | 采药 | 0/1 背包(Day 26) | 普及- |
说明你已经具备参加新生赛的实力了。
这四道题分别考察数组、贪心、搜索、DP —— 正好覆盖了新生赛最常考的四个方向, 而且没有一道是「偏题」。能两小时内全过,你的基本功是扎实的。
如果卡住了,别灰心,去翻对应的那一天: 哪一题卡住,就说明那一块需要二刷。这本身就是最有价值的诊断结果。
| 方向 | 具体建议 |
|---|---|
| 短期 (1–2 周) |
· 把 30 天里所有没做出来或看了题解才会的题重写一遍(不看题解) · 在洛谷「官方题单 → 新手村 / 普及练习场」里按专题刷,每个专题刷 5–10 题 · 重点补:DP 和 搜索。这两块是分水岭,也是 30 天里练得最少的 |
| 中期 (1–2 月) |
· 学新知识点:并查集、图论基础(最短路、最小生成树)、树与二叉树、STL 深水区(map / set / priority_queue) · 开始打 洛谷月赛 / Div.3 难度的比赛,每场都写赛后复盘 · 建立「一题三刷」习惯:第一遍看思路、第二遍不看题解、第三遍限时重写 |
| 长期 | · 目标定在 CSP-J / 蓝桥杯省赛 这类入门级赛事 · 从「会做」升级到「做得快、写得稳」—— 这需要模板化 + 限时训练 · 保持一周至少 5 题的节奏,比一次爆刷 50 题有用得多 |
| 习惯 | · 每道 AC 的题都写 2–3 行题解笔记(记录关键洞察,不用长) · 每场比赛后统计:哪题超时、哪题 WA 几次、下次怎么改 · 永远先看数据范围,再想算法 |
算法竞赛最有趣的地方在于:你不会的东西,一定是能被学会的。 今天让你抓耳挠腮的 DP,三个月后就是肌肉记忆;
这 30 天你最大的收获其实不是某个具体算法,而是「遇到不会的题不慌,能自己拆解、查资料、试错、最终解决」的能力。 这个能力跟竞赛无关,但会跟着你很久。
计划到这里就结束了,但你的路才刚开始。去做那道你一直不敢点开的题吧。 🎉