☰
OJ题A==B?从字符串规范化到C++实现
2026/10/20 9:48:46 网站建设 项目流程

某高校OJ的题单里,编号104这道题,题面短得吓人:A == B ? 就五个字符加一个问号。我当时扫了一眼,心想这不就是个比大小吗,读两个 int 一判断,AC 不就到手了。结果第一次提交直接 WA,我盯着屏幕愣了半天。换成 double 读入再试,还是 WA。再换成 long double,依旧 WA。最后我才明白,这道题考的从来就不是数值比较,而是“两个字符串在数学上是否等价”。

今天就把这道题的完整思路、踩坑过程、最终 C++ 解法和复盘经验一次性写清楚。如果你是刚接触 OJ、在字符串处理上总吃亏的选手,这篇应该能帮你省下好几个晚上。

1. 题面只有一行,坑却藏在“相等”的定义里

1.1 第一版“标准解法”为什么会挂

我最初的代码长这样:

#include <iostream> using namespace std; int main() { long long a, b; while (cin >> a >> b) { cout << (a == b ? "YES" : "NO") << endl; } return 0; }

乍看完全没毛病。直到我试着在本地输入99999999999999999999 99999999999999999999,long long 早就溢出了,读进来的根本不是原始数值。再看输入里带着前导零的写法,比如0001和1,数值上明明相等,但转成整数之后也比较得出来,这条没问题。问题集中在两类数据上:

  • 超大数:C++ 内置整数类型长度有限,超出 long long 范围就废了。
  • 小数与末尾零:1.0和1.00、2.50和2.5这种写法,数学意义上完全相等,但作为字符串完全不等。如果改用 double 比较,又可能在精度上出问题,比如123.0000000000000001会被截断成123,然后错误地判成和123相等。

所以这题不能用数值型变量硬比,得把输入当字符串,做“规范化处理”之后再比较。

1.2 翻车几次之后,我总结出的真实规则

反复提交测试之后,我逐渐摸清了题目想要的行为。它要判断的不是 C++ 里==运算符意义上的相等,而是“两个数在数学书写上经过化简后是否完全一致”。

换句话说,下面这些写法都应该判为相等:

  • 1和1.0
  • 00123和123
  • 2.50和2.5
  • -0和0
  • +5和5
  • 0.000和0

而像1.2和1.3、12和1.2这种,当然不相等。

这其实就是要求我们写一个函数,把任意合法数字字符串统一成一种“标准形式”。只要两个字符串的标准形式一样,答案就是 YES,否则是 NO。

2. 字符串标准化:让“0.000”和“0”长成同一个样子

2.1 目标格式与几个必懂的等价关系

在做任何处理之前,先定好输出长什么样。我最后选择的目标格式是:

  • 正数不带+号。
  • 负数保留-号。
  • 整数部分没有前导零;如果值是 0,整数部分就是一个0。
  • 如果有小数部分,末尾不能有0;如果小数部分为空,就不写小数点。
  • -0要变成0。

举个例子,下面这些字符串规范化之后的结果我都测试过:

原始输入规范化结果
000123123
123.4500123.45
0.0000
-00
+55
-5.50-5.5
007.50007.5

把这些规则背下来,比直接背代码有用得多。因为所有后续逻辑都是在为这几条规则服务。

2.2 normalize 的完整流程与代码

我的做法是把数字拆成“整数部分”和“小数部分”分别清洗,最后再拼起来。这样比在一个字符串里来回删字符直观很多,也不容易漏边界。

流程分四步:

  1. 处理符号:开头的+直接去掉,-保留标记。
  2. 按小数点拆分字符串:没小数点的,小数部分置空。
  3. 清洗整数部分:去掉前导零,如果全是零就置为"0"。
  4. 清洗小数部分:从末尾去掉所有0,如果去完就为空串。

最后拼回整数部分 + 可选的小数部分,再处理一次-0的特殊情况。

对应 C++ 代码如下:

#include <iostream> #include <string> using namespace std; string normalize(string s) { // 1. 处理正负号 bool neg = false; if (!s.empty() && (s[0] == '+' || s[0] == '-')) { neg = (s[0] == '-'); s = s.substr(1); } // 2. 拆出整数部分和小数部分 size_t dot = s.find('.'); string intPart = s; string fracPart; if (dot != string::npos) { intPart = s.substr(0, dot); fracPart = s.substr(dot + 1); } // 3. 整数部分去前导零 size_t firstNonZero = intPart.find_first_not_of('0'); if (intPart.empty() || firstNonZero == string::npos) { intPart = "0"; } else { intPart = intPart.substr(firstNonZero); } // 4. 小数部分去末尾零 while (!fracPart.empty() && fracPart.back() == '0') { fracPart.pop_back(); } // 5. 拼装最终结果 string res = intPart; if (!fracPart.empty()) { res += "." + fracPart; } // 6. -0 统一归零 if (res == "0") { neg = false; } if (neg) { res = "-" + res; } return res; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; while (n--) { string a, b; cin >> a >> b; cout << (normalize(a) == normalize(b) ? "YES" : "NO") << '\n'; } return 0; }

这里面最容易写错的就是find_first_not_of返回npos的情况。比如输入是"000",整数部分全是零,函数找不到非零字符,这时候必须手动把整数部分置为"0",否则后面 substr 会直接崩或者给出完全错误的结果。

3. 用测试用例驱动排查:这些边界数据最容易 WA

3.1 直接抄走的测试用例清单

我后来在本地做了一个小测试库,每次改完代码先跑一遍,确认没有回归再提交。下面这份清单基本覆盖了这道题九成以上的坑:

输入 A输入 B期望输出
11.0YES
11.00YES
01.101.1YES
000123123YES
0.0000YES
-00YES
+55YES
-5.50-5.5YES
9999999999999999999999999999999999999999YES
123.456123.4560YES
.50.5YES
1.1YES
10.01NO
-1.21.2NO
00.1NO

重点留意最后三组。10.0和1看着都有零,但规范化后一个是10,一个是1,绝不能因为“都有 0”就判等。.5这种开头就是小数点的输入很多新手没见过,但拆分逻辑天然能处理成0.5。1.这种小数点在末尾、后面没数字的输入,我的实现里fracPart本身就是空串,最后拼回来就是1,也正确。

3.2 两个最容易阴到人的数据组:-0 与 02.50

我第一次处理-0的时候,想着反正符号位不会影响数值大小,直接忽略负号判断不就行了?结果真有一组-0和0的用例,而且必须输出YES。但如果不做任何处理,-0规范化之后还会带着减号,和0比较就会输出NO。

解决办法就一条:数值部分规范化后如果得到"0",不管原来是不是负数,统一把负号丢掉。这就是代码里if (res == "0") neg = false;的由来。

02.50这种情况则要同时考验整数部分和小数部分的清洗是否彻底。先按小数点拆成02和50,整数部分去前导零变成2,小数部分去末尾零变成5,拼起来是2.5。如果哪一步没做,比如只去了整数部分的前导零,忘了清小数末尾零,就会得到2.50,和标准形式2.5对不上,输出就错了。

调试的时候我习惯在 normalize 函数里临时加一行输出,把每次处理后的中间结果打出来。比如输入02.50,能看到逐步变成02 | 50、再变成2 | 5、最终是2.5。这一步对理解字符串处理题的坑特别有用。

4. C++ 写法细节与最终 AC 代码的易错点

4.1 写这题代码时的三个常见错误

很多人卡在 WA 上不是因为思路不对,而是 C++ 字符串函数的细节没拿捏住。我整理三个高频错误:

  • find_first_not_of('0')返回npos时直接substr,这在整数部分全是零的情况下会返回空串,之后拼出来的结果就缺了"0"。必须显式判断。
  • 把整数部分和小数部分混在一个字符串里删字符。比如直接从"100.000"里从后往前删零,一不小心就把整数部分的0也删了,得到"1",这是错的。拆成两部分处理就没这个问题。
  • 没有区分“一组数据读到底”和“多组数据直到 EOF”的输入方式。我给的代码先读 N 再循环处理;如果你的 OJ 是直接多组输入直到文件尾,把while(n--)改成while(cin >> a >> b)就行。提交前先确认题目要求的输入格式。

还有一个容易被忽视的小点:iostream默认和 C 标准 IO 同步,数据量大的时候cin会慢得离谱。ios::sync_with_stdio(false);和cin.tie(nullptr);这两行建议直接背下来,绝大多数 C++ OJ 选手的模板里都有,能省不少时间。

4.2 完整代码与注释版

我在前面已经贴过一版完整代码。这里再补充一段注释更细的版本,方便你直接对照着理解每一行的作用:

#include <iostream> #include <string> using namespace std; string normalize(string s) { bool neg = false; // 处理符号:+ 直接丢掉,- 记录为负数 if (!s.empty()) { if (s[0] == '+') { s = s.substr(1); } else if (s[0] == '-') { neg = true; s = s.substr(1); } } // 以小数点为界,拆成整数部分和小数部分 size_t dotPos = s.find('.'); string intPart = (dotPos == string::npos) ? s : s.substr(0, dotPos); string fracPart = (dotPos == string::npos) ? "" : s.substr(dotPos + 1); // 整数部分去前导零:全零的情况下保持 "0" size_t pos = intPart.find_first_not_of('0'); if (pos == string::npos) { intPart = "0"; } else { intPart = intPart.substr(pos); } // 小数部分去末尾零 while (!fracPart.empty() && fracPart.back() == '0') { fracPart.pop_back(); } // 拼装 string result = intPart; if (!fracPart.empty()) { result += "."; result += fracPart; } // -0 / -0.00 一律变成 0 if (result == "0") { neg = false; } if (neg) { result = "-" + result; } return result; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; while (n--) { string a, b; cin >> a >> b; if (normalize(a) == normalize(b)) { cout << "YES\n"; } else { cout << "NO\n"; } } return 0; }

这个版本在判断符号时用了更长的分支写法,逻辑上更直白,适合新手。有能力的话可以直接用早先那种两行判断的简写版。

5. 从 A==B 延伸:这类“简单题”的通用套路

5.1 字符串规范化在其他题目里的身影

做完这题之后我最大的收获,不是 AC 本身,而是意识到 OJ 里有一整类题目都在考“字符串规范化”:

  • 高精度加法:两个超大整数相加,第一步就是把字符串反转过来、去掉前导零,再按位相加。
  • 版本号比较:比如"1.0"和"1"要视为相等,做法就是按点拆分,每段去前导零,再逐段比较。
  • IP 地址合法性判断:除了检查数字范围,还经常要处理"192.168.001.1"这种带前导零的写法。
  • 浮点数排序:数字字符串直接按字典序排是错的,必须先统一格式,再比较或排序。

这套思路可以浓缩成一句口诀:凡是“表达形式不同但数学意义相同”的题,核心就是定义一种标准形式,把所有输入都折算到标准形式再比较。

5.2 我的做题习惯:先列等价关系,再写代码

经过这道题,我给自己定了一个很笨但有效的流程:看到这类题,先不着急写代码,拿纸把题目要求的“相等关系”列出来。比如这题我就手写了一堆:1 == 1.0、0.00 == 0、-0 == 0、007 == 7。列完之后,代码怎么写基本就清楚了,normalize 函数其实就是把这些等价关系翻译成程序逻辑。

提交之前,把列出来的等价关系全部转成测试用例,跑一遍本地程序。这一步能拦住至少一半的隐蔽问题。别嫌麻烦,真到了赛场上反复 WA 的时候,本地有一批可复现的测试数据,排查效率能翻好几倍。

如果你也想练这类题,我的建议是不要只满足于 AC。把代码删了,隔两天重新写一遍。第一次能写对靠的是记忆,第二次还能写对,才算真正理解了解题的逻辑。说实话,这种基础题越早吃透,后面做字符串处理题就越顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询