0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1601,日期见页头。两边不一致时信原站。
题目背景
本题是高精度加法的模板题。
题目描述
给定两个非负整数 a, b,求它们的和。不用考虑负数。
输入格式
输入共两行,每行一个非负整数,分别为 a, b。
输出格式
输出一行一个非负整数,表示 a + b 的值。
数据范围
对于 20% 的测试数据,a, b ≤ 10⁹;
对于 40% 的测试数据,a, b ≤ 10¹⁸;
对于 100% 的测试数据,0 ≤ a, b ≤ 10⁵⁰⁰。
时限 1 秒,内存 512 MB(524288 KB)。
输入输出样例
输入
1 1
输出
2
样例一:1 + 1 = 2。
输入
1001 9099
输出
10100
样例二:1001 + 9099 = 10100。⚠ 这一组不是上一组的重复 —— 见第 4 步那张表。
1第 ① 版:读进来直接加 —— 而它值 40 分,是明码标价的
这道题的数据范围写了三档,而它们不是背景,是三件工具:
| 档 | 意味着什么 |
|---|---|
20% 的数据 a, b ≤ 10⁹ |
和 ≤ 2×10⁹,int 上限 2147483647 ⇒ ★ 连 int 都恰好够,余量 7.37% |
40% 的数据 a, b ≤ 10¹⁸ |
和 ≤ 2×10¹⁸,long long 上限 9.22×10¹⁸ ⇒ 余量 4.61 倍 |
100% 的数据 a, b ≤ 10⁵⁰⁰ |
501 位,而 64 位整数只有 19~20 位 |
⇒ 一个 64 位整数读进来直接加,稳拿 40 分。 ★ 这不是「碰巧能骗分」,是出题人把它写在题面上递过来的。
⚠ 顺带看一眼它在大数据上怎么错:cin >> x 读一个 501 位的数会读失败 ——
C++11 起 operator>> 把 x 置成该类型的最大值并设 failbit,不崩溃、也不是随机值。
⇒ 它会安静地打出一个巨大的常数。「它没崩」离「它对了」还差很远。
// ✗ 第 ① 版:所有人真实的第一反应 —— 一个 64 位整数读进来直接加//// 它不是瞎写:题面的数据范围**自己把它的分数写出来了**。//// · 20% 的数据 a, b ≤ 10⁹ ⇒ 和 ≤ 2×10⁹,**连 int 都恰好装得下**(上限 2147483647,余量 7.4%)// · 40% 的数据 a, b ≤ 10¹⁸ ⇒ 和 ≤ 2×10¹⁸ < 9.22×10¹⁸ ⇒ **long long 稳稳的**// · 100% 的数据 a, b ≤ 10⁵⁰⁰ ⇒ 501 位,而 64 位整数只有 19~20 位 ⇒ **差 25 倍位数**//// ⇒ 这一版**稳拿 40 分**,而且是出题人明码标价递给你的 40 分。//// ⚠ 两处写法上的说明,都是为了让「它错成什么样」可复现:// ① 用 unsigned long long 而不是 long long —— **有符号溢出是 UB**,// 演示一个错误写法的时候不能让它的输出依赖编译器([第 45 章](/ch/45-estimate/)那条);// ② `cin >> x` 读一个 501 位的数会**读失败**:C++11 起它把 x 置成该类型的最大值并设 failbit// (不是随机值,也不崩溃)⇒ **「它没崩」离「它对了」还差很远。**
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
unsigned long long a = 0, b = 0; cin >> a >> b; cout << a + b << "\n"; // unsigned 的溢出是「模 2⁶⁴ 绕回」,有定义 return 0;}点「运行 ▶」看结果
2第 ② 版:改成一位一位地加 —— 而第一次写,多半会写成左对齐
数读进来是 string,谁都会顺手写 a[i] = s[i] - '0'。
于是下标 0 成了最高位,而接着「一位一位加起来」的时候,人会按下标配对 ——
也就是把两个数左对齐了:
12345 12345
+ 67 ==> + 67 <- 67 被当成了 67000
------- -------竖式是右对齐的(个位对个位),而这种存法下下标 0 离个位最远。
⇒ ★★ 第 44 章第 5 步那句「倒着存」,全部理由就是这个:
让下标和「这一位有多大」对上,右对齐就变成免费的,
乘法里那句 c[i+j] 也几乎是抄的。
★ 而这个错法的触发条件精确到一句话:两个数的位数不同。 位数相同时左对齐 ≡ 右对齐 ⇒ 它在等长输入上是完全正确的 (下面生成器档 1 量的就是这件事,那是一个能证的精确的 0)。
3★ 正解 —— 加上两处「不是算法」的细节
for (i = 0; i < a.size() || i < b.size() || carry; i++) { ... }|| carry—— 「最高位还要再进一位」那一格(999 + 1 = 1000)。 少了它,算出来的恒等于(a + b) mod 10^max(la, lb)。trim的条件是size() > 1,不是!empty()—— 差一个字,0会被删成空数组,输出变成一个空行。 ⚠ 而题面写的是0 ≤ a, b⇒0 0是一组完全合法的输入。
★ 而「结果最多多长」是能证也能数的:a + b 的位数只可能是
max(la, lb) 或者它 + 1(全枚举 a, b ≤ 999 共 100 万组,0 组例外)。
// P1601 A+B Problem(高精)—— ★ 正解:倒着存的十进制数组,一次加法//// 这一份就是[第 44 章第 7 步](/ch/44-bignum/)那个 add() 原样搬过来,// 只是把「同时输出加减乘」换成了这道题要的那一行。//// 表示约定(和本章正文一字不差):// · vector<int>,一格一位十进制数字,**下标 0 是个位**;// · 允许中途出现前导零(数组末尾的 0),最后统一由 trim() 去掉;// · **0 就是 {0}**,长度 1 —— trim() 里 `size() > 1` 那个 1 的全部理由。//// ⚠ 这道题的两个坑都不在算法里:// ① 题面写的是 `0 ≤ a, b` —— **0 是合法输入**,而 `0 + 0` 要打出一个 `0`,不是空行;// ② 循环条件里那个 `|| carry` ——「最高位再进一位」(999 + 1 = 1000)少了它就少一位。//// 复杂度 O(n),n ≤ 501 ⇒ 什么都不用担心。
#include <bits/stdc++.h>using namespace std;
using Big = vector<int>;
Big parse(const string& s) { Big v; for (int i = (int)s.size() - 1; i >= 0; i--) v.push_back(s[i] - '0'); return v;}
string show(const Big& v) { string s; for (int i = (int)v.size() - 1; i >= 0; i--) s += char('0' + v[i]); return s;}
/** ★ 条件是 size() > 1,不是 !empty():差一个字,0 就被删成空数组 */void trim(Big& v) { while (v.size() > 1 && v.back() == 0) v.pop_back();}
Big add(const Big& a, const Big& b) { Big c; int carry = 0; // ★ `|| carry` 就是「最高位还要再进一位」那一格 for (size_t i = 0; i < a.size() || i < b.size() || carry; i++) { int cur = carry; if (i < a.size()) cur += a[i]; if (i < b.size()) cur += b[i]; c.push_back(cur % 10); carry = cur / 10; } trim(c); return c;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
string sa, sb; if (!(cin >> sa >> sb)) return 0; cout << show(add(parse(sa), parse(sb))) << "\n"; return 0;}点「运行 ▶」看结果
4⚠ 官方给了两组样例 —— 而它们只挡住了三个错法里的一个
| 版本 | 样例一 1 + 1 |
样例二 1001 + 9099 |
结论 |
|---|---|---|---|
| ★ 正解 | 2 | 10100 | |
| ✗ 64 位整数 | 2 | 10100 | 全放过(这两组小得很) |
| ✗ 左对齐 | 2 | 10100 | 全放过 —— ⚠ 两组样例里两个加数都是等长的 |
✗ 少了 || carry |
2 | 100 | ★ 样例二一测就死(1001 + 9099 恰好进到第 5 位) |
| ✗ trim 删过头 | 2 | 10100 | 全放过(这两组的答案都不是 0) |
⇒ ★★ 「官方给了几组就跑几组,它们不是同一件事的重复」的又一次: 样例一什么都问不出来,样例二挡住了一个。 ⚠ 而「左对齐」被放过的原因很具体 —— 不是样例太小,是两组样例的两个加数都恰好一样长 (「这组样例在结构上问不出这个问题」)。
5★ 对拍:参照物要换一种表示,否则同一个笔误两边一起犯
这一章的错几乎全在下标和边界上。两份代码要是连表示都一样,
同一个笔误会同时犯 —— 对拍等于没做(本章 brute.cpp 顶上那条规矩)。
所以参照物换成一格存 9 位十进制(base = 10⁹):分组方式、进位时机、
去前导零的位置、输出的写法(★ 除最高格外每格都要 %09llu)全都不一样。
⚠ 而它顺手称出一件本章没说的事:压位之后中间值是另一笔账。
一格一位时每格最大 9 + 9 + 1 = 19;压 9 位之后是 999999999 × 2 + 1 = 1999999999 ——
★ 而这和上面那张表里 20% 档那笔账是同一个算式:int 恰好还够(余量 7.4%),
再压一位就当场不够。
⇒ ★★ 「要不要 long long」不是数字大小的问题,是「你把几位塞进一格、又拿它做什么运算」的问题。 (这句话在下一道 P1303 上会再用一次,而那次的差距更狠: 换成乘法之后,压到一格四位就已经从 16 万跳到 500 亿。)
| 档位(每档 300 轮) | ✗ 64 位 | ✗ 左对齐 | ✗ 少了 || carry |
✗ trim 删过头 |
|---|---|---|---|---|
| 0 ★ 顺手写的(位数各自随机 1~12) | 0 | 275 | 23 | ★ 精确的 0 |
| 1 ⚠ 两个数等长 | 0 | ★ 精确的 0 | 190 | 精确的 0 |
2 ★ 含 0(各 1/2 概率取 0) |
0 | 78 | 3 | 68 |
| 3 ★ 顶格(495~501 位) | 300 | 259 | 34 | 精确的 0 |
★★★ 三条读得出来的结论:
- ★★★ 同一个旋钮(两个数等不等长)把两个 bug 推向相反方向 ——
等长把「左对齐」打成能证的精确的 0(左对齐 ≡ 右对齐),
却把「少了
|| carry」从 23 顶到 190(8.3 倍)。 ⚠ 道理一句话:位数不同的时候,短的那个几乎不可能把和顶出一位去。 ⇒ 同一个旋钮推两个 bug 反向这件事,本书至此第七次。 - ★★★ 顺手写的那一档,同时是两个 bug 的弱区 ——
「少了
|| carry」只有 23 / 300,而「trim 删过头」是结构性的精确的 0: 随机造「首位非零的正整数」的生成器一辈子造不出 0, 而题面第一行就写着0 ≤ a, b,题单注解还专门点了0 + 0那一组。 ⇒ 「生成器最自然的默认值往往正是某个 bug 的藏身处」。 - ★★ 那十六格「触发 ≡ 抓获」全部一个不差,而这一次「≡」不是运气 ——
高精度这一章的三个 bug 都能写成一句精确的等式
(左对齐算的是「短的那个乘上 10 的若干次方」、少了
|| carry算的是(a+b) mod 10^max、 trim 删过头只在答案为 0 时发作)。 ⇒ ★★★ 能把「它算了什么」写成式子,触发条件就写得准;写得准之后,「≡」是必然的。 (第 35 章 P4147 那次写不成 ≡,恰恰因为那个 bug 有两种效应叠在一起。)
6★ 哪一版就已经能过了
// P1601 解析页上所有数字的出处。./p1601Count [csv]//// 四件事:// ① 题面那三档数据范围各自意味着什么(20% 档连 int 都恰好够,余量算出来);// ② 四个档位里每个 bug 的**触发条件**各满足了多少轮(用来和对拍的抓获数逐格对);// ③ 「a + b 的位数只可能是 max(la, lb) 或者它 + 1」—— 小范围全枚举,0 组例外;// ④ 顶格一次加法要做多少次一位数加法。#include <bits/stdc++.h>using namespace std;
/** 高位在前的字符串加法(只在这份统计程序里用,和被测的几版都不共享代码) */static string addStr(const string& x, const string& y) { size_t n = max(x.size(), y.size()); string a = string(n - x.size(), '0') + x, b = string(n - y.size(), '0') + y, c(n, '0'); int carry = 0; for (size_t k = n; k-- > 0;) { int cur = (a[k] - '0') + (b[k] - '0') + carry; c[k] = char('0' + cur % 10); carry = cur / 10; } if (carry) c = char('0' + carry) + c; size_t i = 0; while (i + 1 < c.size() && c[i] == '0') i++; return c.substr(i);}
/** 和 p1601Gen.cpp 一模一样的一份,用来数「触发条件满足了多少轮」 */static void gen(unsigned seed, int mode, string& a, string& b) { mt19937 rng(seed * 1000003u + 20260904u); int lo = mode == 3 ? 495 : 1, hi = mode == 3 ? 501 : 12; unsigned r1 = rng() % (unsigned)(hi - lo + 1); unsigned r2 = rng() % (unsigned)(hi - lo + 1); int la = lo + (int)r1, lb = lo + (int)r2; if (mode == 1) lb = la; auto one = [&](int len) { string s; unsigned h = rng() % 9u; s += char('1' + h); for (int i = 1; i < len; i++) { unsigned d = rng() % 10u; s += char('0' + d); } return s; }; a = one(la); b = one(lb); if (mode == 2) { unsigned z1 = rng() % 2u, z2 = rng() % 2u; if (z1) a = "0"; if (z2) b = "0"; }}
int main(int argc, char** argv) { bool csv = argc > 1 && string(argv[1]) == "csv";
// ① 题面那三档 const long long INT_MAXV = 2147483647LL; long long sum20 = 2000000000LL; // 20% 档:a, b ≤ 10⁹ ⇒ 和 ≤ 2×10⁹ double intMargin = 100.0 * (double)(INT_MAXV - sum20) / (double)sum20; unsigned long long llMax = 9223372036854775807ULL; double llRatio = (double)llMax / 2e18; // 40% 档:和 ≤ 2×10¹⁸
// ③ 位数上界:小范围全枚举 int bad = 0, hitPlus1 = 0, total = 0; for (int a = 0; a <= 999; a++) for (int b = 0; b <= 999; b++) { string sa = to_string(a), sb = to_string(b), sc = addStr(sa, sb); size_t mx = max(sa.size(), sb.size()); total++; if (sc.size() != mx && sc.size() != mx + 1) bad++; if (sc.size() == mx + 1) hitPlus1++; }
// ② 四个档位的触发计数 int tAlign[4] = {0, 0, 0, 0}, tCarry[4] = {0, 0, 0, 0}, tTrim[4] = {0, 0, 0, 0}, tLl[4] = {0, 0, 0, 0}; for (int mode = 0; mode < 4; mode++) for (unsigned s = 1; s <= 300; s++) { string a, b; gen(s, mode, a, b); string c = addStr(a, b); size_t mx = max(a.size(), b.size()); // ★ 左对齐会错的条件是**两层**:位数不同 **而且**短的那个不是 0 // (短的是 0 时,左对齐只是把 0 挪到了高位,加上去还是 0 ⇒ 它照样对) if (a.size() != b.size() && a != "0" && b != "0") tAlign[mode]++; if (c.size() > mx) tCarry[mode]++; // 最高位真的进了一位 if (a == "0" && b == "0") tTrim[mode]++; // 答案是 0 if (a.size() > 20 || b.size() > 20) tLl[mode]++; // 64 位整数读不进来 }
long long topOps = 501 + 1; // 顶格:最多 502 次一位数加法
if (csv) { printf("intMargin,%.2f\n", intMargin); printf("llRatio,%.2f\n", llRatio); printf("lenCases,%d\n", total); printf("lenBad,%d\n", bad); printf("lenPlus1,%d\n", hitPlus1); printf("topOps,%lld\n", topOps); for (int m = 0; m < 4; m++) printf("trigAlign%d,%d\n", m, tAlign[m]); for (int m = 0; m < 4; m++) printf("trigCarry%d,%d\n", m, tCarry[m]); for (int m = 0; m < 4; m++) printf("trigTrim%d,%d\n", m, tTrim[m]); for (int m = 0; m < 4; m++) printf("trigLl%d,%d\n", m, tLl[m]); return 0; }
printf("=== 题面那三档数据范围,各自是什么意思 ===\n"); printf("20%% 档 a, b <= 1e9 => 和 <= %lld,int 上限 %lld,**恰好够**,余量只有 %.2f%%\n", sum20, INT_MAXV, intMargin); printf("40%% 档 a, b <= 1e18 => 和 <= 2e18,long long 上限 %llu,余量 %.2f 倍\n", llMax, llRatio); printf("100%% 档 a, b <= 1e500 => 501 位,64 位整数只有 19~20 位\n\n");
printf("=== 位数上界:len(a+b) 只可能是 max(la,lb) 或者它 + 1 ===\n"); printf("全枚举 a, b <= 999 共 %d 组:例外 %d 组;其中真的多出一位的 %d 组\n\n", total, bad, hitPlus1);
printf("=== 四个档位 × 每个 bug 的触发条件(各 300 轮)===\n"); printf("%-34s %6s %6s %6s %6s\n", "触发条件", "档0", "档1", "档2", "档3"); printf("%-34s %6d %6d %6d %6d\n", "位数不同且都不是 0(左对齐会错)", tAlign[0], tAlign[1], tAlign[2], tAlign[3]); printf("%-34s %6d %6d %6d %6d\n", "最高位真的进了一位(|| carry)", tCarry[0], tCarry[1], tCarry[2], tCarry[3]); printf("%-34s %6d %6d %6d %6d\n", "答案是 0(trim 删过头)", tTrim[0], tTrim[1], tTrim[2], tTrim[3]); printf("%-34s %6d %6d %6d %6d\n", "有数超过 20 位(64 位读不进)", tLl[0], tLl[1], tLl[2], tLl[3]); printf("\n顶格一次加法:最多 %lld 次一位数加法 —— 时限 1 秒,量级差得太远,秒表在这一章没用\n", topOps); return 0;}点「运行 ▶」看结果
| 写法 | 顶格代价 | 交上去 |
|---|---|---|
| 64 位整数 | O(1) | ✗ 40 分(题面写好的) |
| ★ 倒着存的一位一位加 | 最多 502 次一位数加法 | ★ AC |
| 压位(base 10⁹) | 56 次 | ★ AC,但一分钱不值 |
⚠ 秒表在这一章是没用的(第 44 章第 4 步):502 次加法和时限 1 秒之间 差了六七个数量级,量什么都是噪声。这一页从头到尾没有一个秒数,只有次数和位数。
⇒ 这道题真正要学的是三件和算法无关的事,而三件都只有一句话:
倒着存(否则右对齐要写成 n-1-i)、|| carry(最高位那一次进位)、
0 是合法输入(trim 的条件是 size() > 1)。
⚠ 而后两件,官方样例和顺手写的生成器加起来只挡得住一件。