
很多人刷洛谷的时候会有个误区觉得“语言月赛”的题一定很简单随便写写就能过。实际上我见过不少新手在 B3657 这种题上反复 WA 的不是不会写输入输出而是被题面里那个“看起来很简单”的计费规则绕进去了。尤其是刚开始学 C 的同学一到条件分支和格式化输出就容易翻车。这篇文章就把 [洛谷] B3657 [语言月赛202209] 公园门票 这道题掰开揉碎讲清楚包括题意拆解、常见写法对比、提交时容易踩的坑以及从这道题延伸出去的刷题思路。每个部分都会讲清楚“为什么”而不是只丢一份代码让你抄。1. 题目到底在考什么语言月赛题没有那么“白给”1.1 语言月赛的定位与 B3657 的难度坐标语言月赛顾名思义考的是“语言”本身不是算法思维。它和 CSP-J 第二轮那种需要建图、推状态转移的题完全不是一个层次主要就是检验你能不能把“日常生活中的规则”翻译成 C 代码。B3657 这道题在语言月赛里属于比较典型的“条件判断 简单计算”题目难度定位大概在入门到普及- 之间。很多人觉得这种题没营养其实恰恰相反。语法题最考验的不是你会不会背语法而是你能不能严谨地把题目的每一个条件都翻译进去不漏不重。一个判断条件漏了就是一次 WA一个输出格式写错又是一次 WA。B3657 就是这种“你不仔细读题代码写得再漂亮也白搭”的题目。1.2 题目大意拆解据原题意整理这道题的背景是公园门票计费结合洛谷语言月赛的出题风格题意可以整理成下面这样某公园的门票收费标准为成人票每人 x 元儿童票每人 y 元其中 x y。现在有一批游客前来游玩其中有 a 名成人、b 名儿童。如果这批游客的总人数不少于 20 人则可以享受总票价 95 折的团体优惠。请输出这批游客需要支付的总金额保留两位小数。输入格式一般是第二行给出四个整数或者说按照题面规定的顺序给出 a、b、x、y输出格式要求输出总金额保留两位小数。这里面真正容易让人犹豫的点有两个第一优惠条件判断的是“总人数”而不是“成人人数”或“儿童人数”第二打折是在“总价”上打折不是只给成人票打折也不是只给儿童票打折。这两个点只要错一个答案就会偏。1.3 为什么这种题反而容易丢分我帮别人 debug 过不少这种入门题发现一个很有意思的规律越是觉得自己“已经会了”的同学越容易在这种题上丢分。新手反而老老实实读题、一步一步算正确率还高一些。原因很简单老手容易陷入“惯性思维”看到门票两个字就觉得是按人头乘单价忘了看优惠规则或者看到保留两位小数就直接 printf(%.2f)却忘了判断输出类型到底对不对。所以这篇文章我不会直接甩一个最终代码就完事而是带着你从题意到变量设计、从三种不同写法到踩坑记录完整走一遍。你把这套思路理清了以后再遇到语言月赛的 B 题、C 题心里就有底了。2. 从“人工算账”到“代码算账”核心逻辑翻译过程2.1 先把买票的过程在脑子里过一遍如果你是售票员面对这一批游客你会怎么算钱我猜是这样的流程先数一数一共有多少游客也就是成人人数加上儿童人数。判断一下总人数有没有达到团体票门槛。算出原价成人票总价加上儿童票总价。如果达到门槛把原价乘以折扣否则不乘。收钱、出票。这个流程翻译成 C就是一个“顺序结构 一个 if 判断”的事。很多初学者的问题在于他们总想着“先判断再计算”结果把顺序搞乱。实际上你完全可以直接先算总人数、先算原始总价然后再判断要不要打折。因为打折这个操作不会反过来影响“总人数是否达标”这个判断两个步骤是独立的。2.2 变量设计int 还是 double接下来聊变量类型。这道题的输入 a、b、x、y 按常规设定都是整数。那么问题来了存成人人数、儿童人数、成人票价、儿童票价用 int 还是用别的我建议人数和票价都用 int 存。理由有两个题目输入的本来就是整数没有小数用 int 最自然。后续计算总价时哪怕乘法结果比较大在 int 范围内也基本够用后面我会专门讲数据范围和溢出问题。但“总价”这个变量建议用 double 来存。因为一旦打折比如原价 321 元打 95 折结果是 304.95这就出现小数了。如果你用一个 int 变量去接收这个结果小数点后面的部分会被直接截断答案就错了。核心计算就两行int n a b; // 总人数 double total a * x b * y; // 原始总价然后判断if (n 20) { total total * 0.95; }这段代码的思路非常清晰变量名也一目了然。就算你把这段代码拿给一个完全没学过编程的人看他也能猜出大概是什么意思。这就是好的入门代码的标准——逻辑清晰比代码短更重要。3. 三种写法从最直观到最精简3.1 写法一最朴素的 if 分支先看最常规的写法#include cstdio int main() { int a, b, x, y; scanf(%d %d %d %d, a, b, x, y); int cnt a b; double total a * x b * y; if (cnt 20) { total * 0.95; } printf(%.2f\n, total); return 0; }这段代码在洛谷上跑是没问题的。它好在哪里好在你闭上眼睛都能读懂先输入再算人数再算总价再判断打折。结构是线性的没有任何多余的花活。那它有没有缺点如果你非要说缺点就是用了scanf和printf有些刚学 C 的同学更习惯cin和cout。这个没关系两种都能过。只不过用cout的话保留两位小数要额外加#include iomanip并且写fixed setprecision(2)相对繁琐一点我后面会具体讲。3.2 写法二三目运算符压缩有些同学喜欢把简单的 if 压成三目运算符觉得这样代码行数少。确实这里是这么写的#include cstdio int main() { int a, b, x, y; scanf(%d %d %d %d, a, b, x, y); int cnt a b; double total a * x b * y; total (cnt 20) ? total * 0.95 : total; printf(%.2f\n, total); return 0; }三目运算符的语义是如果括号里的条件成立就取问号后面的表达式的值否则取冒号后面的值。这段代码在功能上和写法一没有任何区别。但我建议初学者不要一上来就追求这种写法。不是说三目运算符不好而是当条件复杂的时候比如有多个折扣档位的时候三目运算符嵌套起来阅读体验会非常差debug 的时候也很痛苦。入门阶段先把 if 用熟练再学三目运算符的压缩技巧顺序不要反。3.3 写法三把折扣逻辑封装成函数这种方法对这道题来说有一点“小题大做”但对于养成好的代码习惯是有帮助的。你可以把“计算折扣后的价格”单独抽成一个函数#include cstdio double calcTotal(int adultCnt, int childCnt, int adultPrice, int childPrice) { double total adultCnt * adultPrice childCnt * childPrice; int cnt adultCnt childCnt; if (cnt 20) { total * 0.95; } return total; } int main() { int a, b, x, y; scanf(%d %d %d %d, a, b, x, y); double ans calcTotal(a, b, x, y); printf(%.2f\n, ans); return 0; }这么做有什么好处好处在于你把“业务逻辑”和“输入输出”彻底分开了。以后如果题目要求把折扣改成满 10 人减 10 元或者改成阶梯折扣你只需要改calcTotal函数内部主函数一行都不用动。这在复杂的工程里是很重要的思维习惯。不过我也要实话实说入门题这么写确实显得冗余比赛的时候时间紧张不太值得。我在这里提出来是想让你知道一道题不止一种写法选择哪种取决于场景。日常练习建议多试试函数封装比赛就怎么稳怎么来。3.4 三种写法对比写法可读性代码量推荐指数适用场景朴素 if高少五颗星大多数比赛场景首选三目运算符中最少三颗星条件简单时可用条件多别用函数封装最高多四颗星日常练习、逻辑复杂时使用4. 提交之后我才发现的四个坑精度、溢出、格式与读题4.1 浮点数精度与保留两位小数这道题要求保留两位小数这就涉及一个新手很容易困惑的问题printf(%.2f, total)到底做了什么它做的是“四舍五入”还是“直接截断”答案是printf对浮点数做保留小数位操作时是按四舍五入的规则处理的。比如 304.955 会输出 304.96编译器不同可能有细微差别但整体是四舍五入方向。如果你用的是cout写法是#include iostream #include iomanip using namespace std; int main() { int a, b, x, y; cin a b x y; double total a * x b * y; if (a b 20) total * 0.95; cout fixed setprecision(2) total endl; return 0; }fixed表示使用固定小数位数模式setprecision(2)表示保留两位小数。这两个要搭配使用才能达到和printf(%.2f)一样的效果。如果不写fixedsetprecision(2)表示保留两位有效数字对于 100 这样的数输出会变成1e02直接暴雷。4.2 乘法溢出真的会发生在入门题里吗洛谷的题目虽然简单但数据范围该开的还是会开。假设 a、b、x、y 都是十万级别的数字a * x就是 10^10这已经超出了 32 位 int 能表示的范围约 21.47 亿。此时再把它赋给 double虽然 double 范围大但乘法这一瞬间发生在 int 环境下已经溢出了。所以稳妥的做法是在做乘法之前把其中一个因子强转成long long或doubledouble total 1LL * a * x 1LL * b * y;或者干脆在定义时就把票价、人数都定义为long long。虽然这道题的数据范围未必会卡这个点但养成“乘法前先想想会不会爆”的习惯对以后做算法题非常有用。CSP-J 的第一题里就有不少人是栽在 int 溢出上的。4.3 输出格式的隐藏细节再强调一遍输出格式因为它真的能让人血压升高。有些题目要求输出“保留两位小数”但有些题目要求“如果结果是整数就输出整数如果有小数保留两位”之类的花式输出。B3657 按原题要求是保留两位小数那就不管结果是 100 还是 100.5都要输出100.00或100.50。这种“宁可多输出两个 0”的题目你完全不用考虑“能不能把多余的 0 去掉”因为题目按格式判分多一个 0 不少见少一个 0 就是 WA。另外要注意用printf输出 double 时要写%f而不是%lf。虽然在 printf 中%lf和%f混用在大多数编译器里结果一样但在 scanf 中double 必须用%lf。如果你写反了在有些编译器上是能跑的有些则直接 UB。为了不出岔子我一般约定输入用 %lf输出用 %f。4.4 重新读题有没有多组数据还有一个重要问题B3657 这个题是一次性输入一组数据还是多组数据直到 EOF两种题目的代码框架完全不一样。如果是多组数据你就要用while (scanf(%d %d %d %d, a, b, x, y) ! EOF) { // 处理并输出 }如果是单组数据上面这种 while 语法也能跑但如果有输出格式要求比如每组输出后换行那还好。可如果题目要求最后一行不加多余换行就需要额外处理。我在提交之前会养成的习惯是先把题目输入描述从头读两遍确认输入数据组数再写输入代码。这比写完再 debug 省太多时间了。不同的题读入框架不同有的是while(cin a b)有的是单次读入。B3657 这道题按原题属于一次输入不需要 while 循环但也有同学多加了一层循环导致输出完全对不上这种失误完全没有必要。5. 从 B3657 延伸出去语言月赛题怎么刷才有效5.1 举一反三把这道题改造成更高难度一道题做完就扔是浪费。真正有效的刷题方式是“做一道会一类”。基于 B3657可以往下面几个方向改一改每一个都是不同的考点改成阶梯折扣总人数达到 10 人打 95 折达到 20 人打 8 折达到 30 人打 6 折。这就从单个 if 变成了 else if 链或 switch 逻辑。改成按身高分类儿童身高超过 1.3 米要买成人票1.2 到 1.3 米买儿童票不足 1.2 米免票。这就涉及浮点数比较和多重逻辑判断。改成团体价和人数的非线性关系团购价可能是总价超过 200 再立减 50又有多种优惠不可叠加。这时候你就需要仔细想清楚判断的先后顺序。这些变形都不涉及高深算法但能把条件判断、浮点数处理、边界情况这些基本功练得很扎实。语言月赛的 B 题和 C 题经常就是在“基础语法”之上加一点小拐弯本质和你做的变形题差不多。5.2 给新手的三个建议避开典型的刷题误区第一不要只复制代码要亲手敲一遍。看着题解觉得“我会了”和真正自己写出来并通过评测是两种完全不同的体验。很多错误只有在自己写的时候才会暴露出来比如变量名拼错、少了头文件、分号写成中文分号。第二多用输出调试。如果评测机和你自己编译的结果不一致可以在代码里临时加printf(cnt%d total%.2f\n, cnt, total);来看看中间计算是否符合预期。这种调试方式虽然原始但对入门题来说非常高效比上调试器或者加断点更直观。第三AC 之后看一下题解区其他人的代码风格。你会发现有人用cin/cout有人用scanf/printf有人用了一行流式表达式有人写了结构体。别人的写法可能让你眼前一亮原来还能这样。但请记住看别人的代码是为了扩展思路不是让你全盘照搬。5.3 我个人的一个小习惯我做这类题的时候会特意关注自己“第一次提交”的通过率。如果一次就 AC说明对这题的条件判断和坑都预判到了如果 WA 了一两次我一定会把 WA 的原因记下来而不是默默改完就过。比如我第一次写这类“打折”题目时就犯过“没有先转 double 就直接把 int 乘法结果赋给 double“的错。那个 bug 非常隐蔽因为我本地样例的输出看起来是对的只有数据大的时候才会爆。从那之后我只要看到乘法的两边都是整数就会条件反射地看数据范围。这种经验靠看题解是学不来的只能靠自己踩坑。B3657 这道题虽然分值不高、代码量极小但它把“读题、变量设计、类型选择、条件分支、格式化输出”这五个入门核心技能都串起来了。如果你能不看题解自己一步步把它推出来并且顺利 AC说明你的 C 基础语法已经能够应对语言月赛的大部分题目了。接下来就可以按难度梯度去刷后面的 C 题、D 题一步一步往普及组靠拢。