0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2142,日期见页头。两边不一致时信原站。
题目描述
给定两个正整数 a, b,求 a − b 的值。
输入格式
输入共两行,每行一个正整数,分别为 a, b。
输出格式
输出一行一个整数,表示 a − b 的值。
如果 a − b < 0,请输出负号。
数据范围
对于 20% 的数据,a, b 在 long long 类型的存储范围内;
对于 100% 的数据,0 < a, b ≤ 10¹⁰⁰⁸⁶。
时限 1 秒,内存 125 MB(128000 KB)。
输入输出样例
输入
2 1
输出
1
官方只给了这一组:2 − 1 = 1。⚠ 第 4 步会看到,它把五个待测版本全放过了。
1★★★ 这道题拆掉的,是本章那个 sub() 顶上写着的那句前提
第 44 章第 7 步那份 sub() 的注释第一行就写着:
调用方保证
a >= b。
而这道题的题面没有这句保证 —— 它反而专门写了一行
「如果 a − b < 0,请输出负号」。
⇒ ★★ 照抄那个模板,就是把一个前提已经不成立的模板搬进考场。
说清楚它算了什么:a < b 时最后一位的借位没人接,
于是它算出来的恒等于 (a − b) mod 10^len(a)(10 的补数)——
2 − 5 => 它打 7 (不是 −3)
12 − 34 => 它打 78 (不是 −22)★ 所有表现都是白送的推论:a >= b 的每一轮它一个字都不会错
⇒ 它是「偶尔才错」型,抓获率大约就是「a < b 的概率」,约一半(实测四档 133 / 149 / 111 / 149)。
⇒ ★★★ 第 43 章那三道题刚把这句话演了一遍: 记一个模板的时候,要把它的前提一起记下来 —— 前提失效的那天,模板不会报错。 换成高精度,这句话一个字都没变。
// ✗ 第 ① 版:64 位整数直接减 —— 题面明码标价的 20 分//// · 20% 的数据:a, b 在 long long 的范围内 ⇒ **这一版全对**;// · 100% 的数据:a, b ≤ 10^10086 ⇒ **10087 位**,而 long long 只有 19 位 —— 差 530 倍位数。//// ⇒ 和[上一道 P1601](/sol/p1601/) 是同一件事的第二次:// **出题人写的那几行分档,就是「暴力值多少分」的答案。**// ⚠ 但这道题的第一档只值 20 分(P1601 那道值 40 分)——// **同一个偷懒的写法,在两道相邻的题上值的钱不一样。**//// ⚠ 用 long long(不是 unsigned)是因为这道题的答案**可以是负数**,// 而这一版在小数据上是真的对:负号由 printf 自己打出来。// 顶格时 `cin >>` 读失败 ⇒ 两个变量都被置成 LLONG_MAX ⇒ 打出 0(不是崩溃)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
long long a = 0, b = 0; cin >> a >> b; cout << a - b << "\n"; return 0;}点「运行 ▶」看结果
上一道 P1601 的第一档写着「20% + 40%」⇒ 64 位整数值 40 分;
这道题只有「20% 的数据 a, b 在 long long 范围内」⇒ 它值 20 分。
⇒ ★ 「先交一发 64 位的」值多少分,是「那个写法 × 那道题的分档」的属性,
不是那个写法的属性。(顶格 10¹⁰⁰⁸⁶ 是 10087 位,long long 只有 19 位 —— 差 530 倍。)
2★ 正解:多出来的那一层全是「和算法无关」的活
| 要做的事 | 一句话 |
|---|---|
| ① 比大小 | 先比位数,位数相同再逐位比 —— ⚠ 直接用 string 的 < 是字典序,不是数值序 |
| ② 交换 + 记负号 | a < b 时算 b − a,前面补一个 - |
| ③ 去前导零 | 1000 − 999 = 0001 → 1 |
④ ⚠ a == b |
答案是 0,不能打成 -0 —— 第 ② 步那个判断写成 <= 就会 |
★ 而 ④ 这一格值得单独说一句:题面写的是 0 < a, b ——
那句话管的是输入,没管输出。a == b 时答案就是 0,而且完全合法。
⇒ ⚠ 和上一道 P1601 正好错开:
那道题 0 是合法的输入(0 ≤ a, b),这道题 0 是合法的输出。
两道相邻的题,两个 0 长在不同的地方。
// P2142 高精度减法 —— ★ 正解:先判谁大,再减,最后补符号//// ★★ 这道题和上一道([P1601](/sol/p1601/))差的不是难度,是**前提**:// [第 44 章第 7 步](/ch/44-bignum/)那个 sub() 的注释里写着一句// 「**调用方保证 a >= b**」—— 而这道题的题面**没有这个保证**,// 还专门写了一行「如果 a − b < 0,请输出负号」。// ⇒ 照抄本章的 sub() 就是把一个**前提已经不成立的模板**搬过来(p2142NoSwap.cpp 就是它)。//// 所以正解比模板多了整整一层,而这一层全是「和算法无关」的活:// ① 比大小:**先比位数,位数相同再逐位比**(⚠ 直接用 string 的 `<` 是错的,见 p2142Lex.cpp);// ② 小的减大的时候交换两个数,并且记下要打一个负号;// ③ 减完去前导零(1000 − 999 = 0001 → 1);// ④ ⚠ `a == b` 时答案是 `0`,**不能打成 `-0`**(第 ② 步那个判断写成 `<=` 就会,见 p2142NegZero.cpp)。//// 复杂度 O(n),n ≤ 10087(题面 a, b ≤ 10^10086)⇒ 一万位的减法,眨眼。
#include <bits/stdc++.h>using namespace std;
/** −1 / 0 / 1。★ 位数不同时长的那个大,位数相同时才轮到逐位比 */static int cmpNum(const string& a, const string& b) { if (a.size() != b.size()) return a.size() < b.size() ? -1 : 1; if (a == b) return 0; return a < b ? -1 : 1;}
/** 高位在前的减法,调用方保证 a >= b;结果已去掉前导零 */static string subStr(const string& a, const string& b) { string y = string(a.size() - b.size(), '0') + b, c(a.size(), '0'); int borrow = 0; for (size_t k = a.size(); k-- > 0;) { int cur = (a[k] - '0') - (y[k] - '0') - borrow; borrow = 0; if (cur < 0) { cur += 10; borrow = 1; } // ★ 借位是一条链,borrow 要带着走 c[k] = char('0' + cur); } size_t i = 0; while (i + 1 < c.size() && c[i] == '0') i++; // ★ 1000 − 999 = 0001 → 1 return c.substr(i);}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
string a, b; if (!(cin >> a >> b)) return 0;
int r = cmpNum(a, b); if (r == 0) cout << "0\n"; // ⚠ 这一支必须单独走,否则会打出 -0 else if (r > 0) cout << subStr(a, b) << "\n"; else cout << "-" << subStr(b, a) << "\n"; return 0;}点「运行 ▶」看结果
3★★★ 验算走一条和减法完全无关的路:把答案加回去
正解算出 c = a − b 之后,下面这句应该逐字节成立:
|c| + min(a, b) == max(a, b)★ 这条路和减法一行代码都不共享:加法只有进位、没有借位, 没有比大小、没有符号、没有交换。 ⇒ 「验算要走一条和算法完全无关的路」的又一次。
实测:四个档位共 1200 组,对不上 0 组。
⚠⚠ 而这个「0 组」配了自检才敢写 —— 把同一段验证器拿去验那个
「照抄本章 sub()」的版本:在 a < b 的 542 组里,它抓出 542 组,一组不漏。
⇒ ★★ 「报『0 次 / 找不到』之前,先拿一个已知错的东西验证这段代码是活的」。
4⚠ 官方那唯一一组样例:五个待测版本全放过
| 版本 | 样例输出 | 为什么放过 |
|---|---|---|
| ★ 正解 | 1 | |
| ✗ 64 位整数 | 1 | 两个数只有一位 |
✗ 照抄 sub() |
1 | ★ 这一组恰好 a > b ⇒ 它的前提在样例上是成立的 |
| ✗ 字典序比大小 | 1 | 两个数等长 ⇒ 字典序和数值序是同一个序 |
| ✗ 不去前导零 | 1 | 答案 1 位,一个前导零都没有 |
✗ <= ⇒ -0 |
1 | a ≠ b |
⇒ ★★ 这是「官方样例是个一测就死的过滤器」那条规律的极端一头: 这道题只给了一组样例,而且五个坑它一个都问不出来。 ⚠ 和上一道 P1601 正好对照 —— 那道题给了两组,其中一组挡住了一个。
5★★★ 生成器:最后两个 bug 的旋钮,不在任何一条数据范围里
| 档位(每档 300 轮) | ✗ 64 位 | ✗ 照抄 sub() |
✗ 字典序 | ✗ 不去前导零 | ✗ <= ⇒ -0 |
|---|---|---|---|---|---|
| 0 ★ 顺手写的(位数各自随机 1~12) | 0 | 133 | 123 | 9 | ★ 精确的 0 |
| 1 ⚠ 两个数等长 | 0 | 149 | ★ 精确的 0 | 70 | 3 |
| 2 ★ 两个数离得近(1/3 完全相等) | 0 | 111 | 精确的 0 | 178 | 99 |
| 3 ★ 顶格(10080~10086 位) | 300 | 149 | 124 | 15 | 精确的 0 |
★★★ 三条读得出来的结论:
- ★★★ 最后两个 bug 的旋钮是同一个 —— 「两个数有多近」,而它不在任何一条数据范围里。
题面只写了
0 < a, b ≤ 10¹⁰⁰⁸⁶:位数、正负、上界全写了, 唯独「两个数会不会挨得很近」一个字都没有。 ⇒ 顺手写的那一档里,「不去前导零」只有 9 / 300、「-0」是精确的 0; 而把两个数造得很近之后是 178 和 99。 ⇒ ★★ 「抓不到时别加轮数,去想那条线在哪儿、然后照着它造」 —— ⚠ 而这一次那条线得自己想出来,题面不会提醒你。 - ★★ 「等长」这个旋钮同时给字典序那个 bug 做了自检 —— 等长时字典序和数值序是同一个序(一行能证)⇒ 档 1、档 2 都是精确的 0; 而把位数放开,它当场 123 / 124。 ⇒ 同一个动作干两件事:证明那段对拍是活的 + 称出「位数不同」才是它的命门。
- ★★ 那二十格「触发 ≡ 抓获」一个不差 —— 和上一道 P1601 的十六格连起来, 这一轮一共 36 格全中。而这不是运气:高精度的每个 bug 都能写成一句精确的等式, 写得准之后 ≡ 是必然的。(第 35 章 P4147 那次写不成 ≡, 是因为那个 bug 有两种效应叠在一起。)
6★ 哪一版就已经能过了
// P2142 解析页上所有数字的出处。./p2142Count [csv]//// 三件事:// ① 四个档位里每个 bug 的**触发条件**各满足了多少轮(拿去和对拍的抓获数逐格对);// ② ★★★ **验算走一条和减法完全无关的路:把答案加回去** ——// `|a − b| + min(a,b)` 应该逐字节等于 `max(a,b)`。// 加法和减法在代码上一行都不共享(一个只有进位、一个只有借位),// 所以这不是「拿减法验减法」。// ⚠⚠ 而这个「0 组对不上」**配了自检才敢写**:同一段验证器拿去验那个// 「照抄本章 sub()」的版本,当场抓出一大片([报 0 之前先证明这段代码是活的](/sol/p1094/))。// ③ 顶格一次减法要做多少次一位数减法。#include <bits/stdc++.h>using namespace std;
static int cmpNum(const string& a, const string& b) { if (a.size() != b.size()) return a.size() < b.size() ? -1 : 1; if (a == b) return 0; return a < b ? -1 : 1;}
static string subStr(const string& a, const string& b) { string y = string(a.size() - b.size(), '0') + b, c(a.size(), '0'); int borrow = 0; for (size_t k = a.size(); k-- > 0;) { int cur = (a[k] - '0') - (y[k] - '0') - borrow; borrow = 0; if (cur < 0) { cur += 10; borrow = 1; } c[k] = char('0' + cur); } size_t i = 0; while (i + 1 < c.size() && c[i] == '0') i++; return c.substr(i);}
/** 完全无关的那条路:只有进位,没有借位 */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);}
/** 「照抄本章 sub()、不判大小」那一版算出来的东西 —— 只用来给验证器做自检 */static string noSwap(const string& a, const string& b) { string y = b; if (y.size() > a.size()) y = y.substr(y.size() - a.size()); y = string(a.size() - y.size(), '0') + y; string c(a.size(), '0'); int borrow = 0; for (size_t k = a.size(); k-- > 0;) { int cur = (a[k] - '0') - (y[k] - '0') - borrow; borrow = 0; if (cur < 0) { cur += 10; borrow = 1; } c[k] = char('0' + cur); } size_t i = 0; while (i + 1 < c.size() && c[i] == '0') i++; return c.substr(i);}
static void gen(unsigned seed, int mode, string& a, string& b) { mt19937 rng(seed * 1000003u + 20260904u); int lo = mode == 3 ? 10080 : 1, hi = mode == 3 ? 10086 : 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 || mode == 2) 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); if (mode == 2) { unsigned same = rng() % 3u; b = a; if (same != 0) { unsigned k = 1u + rng() % 3u; for (unsigned t = 0; t < k && t < a.size(); t++) { unsigned d = rng() % 10u; b[b.size() - 1 - t] = char('0' + d); } if (b[0] == '0') b[0] = a[0]; } } else { b = one(lb); }}
int main(int argc, char** argv) { bool csv = argc > 1 && string(argv[1]) == "csv";
int tSwap[4] = {0}, tLex[4] = {0}, tTrim[4] = {0}, tEq[4] = {0}, tLl[4] = {0}; int backBad = 0, backCases = 0, selfCaught = 0, selfCases = 0;
for (int mode = 0; mode < 4; mode++) for (unsigned s = 1; s <= 300; s++) { string a, b; gen(s, mode, a, b); int r = cmpNum(a, b); if (r < 0) tSwap[mode]++; // NoSwap 会错 bool lexSaysLess = (a < b); // 字典序 if (lexSaysLess != (r < 0)) tLex[mode]++; // 字典序判反了 if (r == 0) tEq[mode]++; // NegZero 打 -0 if (a.size() > 19 || b.size() > 19) tLl[mode]++; string big = r >= 0 ? a : b, small = r >= 0 ? b : a; string c = r == 0 ? string("0") : subStr(big, small); if (r != 0 && c.size() < big.size()) tTrim[mode]++; // 答案位数变少 ⇒ 有前导零
// ★★★ 反着验:把答案加回去 backCases++; if (addStr(c, small) != big) backBad++; // ★ 自检:同一段验证器去验那个已知错的版本 if (r < 0) { // a < b 时它才错 selfCases++; string w = noSwap(a, b); if (addStr(w, b) != a) selfCaught++; } }
long long topLen = 10087;
if (csv) { for (int m = 0; m < 4; m++) printf("trigSwap%d,%d\n", m, tSwap[m]); for (int m = 0; m < 4; m++) printf("trigLex%d,%d\n", m, tLex[m]); for (int m = 0; m < 4; m++) printf("trigTrim%d,%d\n", m, tTrim[m]); for (int m = 0; m < 4; m++) printf("trigEq%d,%d\n", m, tEq[m]); for (int m = 0; m < 4; m++) printf("trigLl%d,%d\n", m, tLl[m]); printf("backCases,%d\n", backCases); printf("backBad,%d\n", backBad); printf("selfCases,%d\n", selfCases); printf("selfCaught,%d\n", selfCaught); printf("topLen,%lld\n", topLen); return 0; }
printf("=== 四个档位 × 每个 bug 的触发条件(各 300 轮)===\n"); printf("%-40s %6s %6s %6s %6s\n", "触发条件", "档0", "档1", "档2", "档3"); printf("%-40s %6d %6d %6d %6d\n", "a < b(照抄本章 sub 会错)", tSwap[0], tSwap[1], tSwap[2], tSwap[3]); printf("%-40s %6d %6d %6d %6d\n", "字典序把大小判反了", tLex[0], tLex[1], tLex[2], tLex[3]); printf("%-40s %6d %6d %6d %6d\n", "答案位数变少(前导零)", tTrim[0], tTrim[1], tTrim[2], tTrim[3]); printf("%-40s %6d %6d %6d %6d\n", "a == b(会打出 -0)", tEq[0], tEq[1], tEq[2], tEq[3]); printf("%-40s %6d %6d %6d %6d\n", "有数超过 19 位(64 位装不下)", tLl[0], tLl[1], tLl[2], tLl[3]);
printf("\n=== 验算:把答案加回去(|a−b| + min == max)===\n"); printf("%d 组里对不上的:%d 组\n", backCases, backBad); printf("★ 自检:同一段验证器去验「照抄本章 sub()」那一版 —— %d 组里抓出 %d 组\n", selfCases, selfCaught); printf("\n顶格:%lld 位的减法,%lld 次一位数减法 —— 时限 1 秒,秒表在这一章没用\n", topLen, topLen); return 0;}点「运行 ▶」看结果
| 写法 | 顶格代价 | 交上去 |
|---|---|---|
| 64 位整数 | O(1) | ✗ 20 分 |
| ★ 比大小 + 一次借位扫描 | 10087 次一位数减法 | ★ AC |
⚠ 秒表在这一章是没用的:一万位的减法和时限 1 秒之间差着好几个数量级。 这一页从头到尾没有一个秒数。
⇒ 这道题真正的四个关卡:比大小要比位数、结果为负要打负号、
结果要去前导零、结果为 0 不能打 -0。
四个里只有第二个跟「减法怎么算」有关,另外三个是格式和边界。
⚠ 而官方样例一个都挡不住,顺手写的生成器只挡得住两个。