资讯动态

OJ题A==B?从字符串规范化到C++实现

发布时间:2026/10/20 9:48:58 来源:尧图企业网站定制
某高校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 99999999999999999999long long 早就溢出了读进来的根本不是原始数值。再看输入里带着前导零的写法比如0001和1数值上明明相等但转成整数之后也比较得出来这条没问题。问题集中在两类数据上超大数C 内置整数类型长度有限超出 long long 范围就废了。小数与末尾零1.0和1.00、2.50和2.5这种写法数学意义上完全相等但作为字符串完全不等。如果改用 double 比较又可能在精度上出问题比如123.0000000000000001会被截断成123然后错误地判成和123相等。所以这题不能用数值型变量硬比得把输入当字符串做“规范化处理”之后再比较。1.2 翻车几次之后我总结出的真实规则反复提交测试之后我逐渐摸清了题目想要的行为。它要判断的不是 C 里运算符意义上的相等而是“两个数在数学书写上经过化简后是否完全一致”。换句话说下面这些写法都应该判为相等1和1.000123和1232.50和2.5-0和05和50.000和0而像1.2和1.3、12和1.2这种当然不相等。这其实就是要求我们写一个函数把任意合法数字字符串统一成一种“标准形式”。只要两个字符串的标准形式一样答案就是 YES否则是 NO。2. 字符串标准化让“0.000”和“0”长成同一个样子2.1 目标格式与几个必懂的等价关系在做任何处理之前先定好输出长什么样。我最后选择的目标格式是正数不带号。负数保留-号。整数部分没有前导零如果值是 0整数部分就是一个0。如果有小数部分末尾不能有0如果小数部分为空就不写小数点。-0要变成0。举个例子下面这些字符串规范化之后的结果我都测试过原始输入规范化结果000123123123.4500123.450.0000-0055-5.50-5.5007.50007.5把这些规则背下来比直接背代码有用得多。因为所有后续逻辑都是在为这几条规则服务。2.2 normalize 的完整流程与代码我的做法是把数字拆成“整数部分”和“小数部分”分别清洗最后再拼起来。这样比在一个字符串里来回删字符直观很多也不容易漏边界。流程分四步处理符号开头的直接去掉-保留标记。按小数点拆分字符串没小数点的小数部分置空。清洗整数部分去掉前导零如果全是零就置为0。清洗小数部分从末尾去掉所有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. 用测试用例驱动排查这些边界数据最容易 WA3.1 直接抄走的测试用例清单我后来在本地做了一个小测试库每次改完代码先跑一遍确认没有回归再提交。下面这份清单基本覆盖了这道题九成以上的坑输入 A输入 B期望输出11.0YES11.00YES01.101.1YES000123123YES0.0000YES-00YES55YES-5.50-5.5YES9999999999999999999999999999999999999999YES123.456123.4560YES.50.5YES1.1YES10.01NO-1.21.2NO00.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. 从 AB 延伸这类“简单题”的通用套路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。把代码删了隔两天重新写一遍。第一次能写对靠的是记忆第二次还能写对才算真正理解了解题的逻辑。说实话这种基础题越早吃透后面做字符串处理题就越顺。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑