多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用“买笔”学会分支结构:整除、取余与贪心思想的经典题

用“买笔”学会分支结构:整除、取余与贪心思想的经典题 题库编号 2059教材例 3.11题目叫“买笔”。这题我第一次看到的时候觉得实在有点“入门”拿 n 元钱去买钢笔、圆珠笔和铅笔单价分别是 6 元、5 元、4 元要求正好花完并且买到的笔总数尽量多最后输出三种笔各买多少支。可真正自己上手写再给别人讲明白之后我才意识到这题能被选进“分支结构”的经典例题不是没有道理的——它把整除、取余、分支判断和一点点贪心思想全揉在了一个只有十几行代码的小题里。这篇文章就用它来聊聊怎么把一道看似简单的水题做稳、做透并且把背后的思考方式留下来。适合看这篇内容的人主要有三类刚学完 if-else 想找练习的初学者准备信息学竞赛、担心自己边界处理不稳的学生以及想帮孩子讲清楚“为什么要这样分类”的家长。你会发现代码本身不复杂真正有价值的是那张四行分支表是怎么推出来的以及测试时哪些数据最容易暴露问题。1. 题目到底在问什么先把题面拆干净1.1 题干里的三个关键信息这道题的题面通常是这样班主任给了一笔钱让去买三种笔钢笔 6 元一支圆珠笔 5 元一支铅笔 4 元一支。要求用这笔钱正好买完并且买到的笔总数尽可能多最后输出钢笔、圆珠笔、铅笔的数量。读题的时候有三个词必须圈出来。第一个是“正好花完”。这意味着总价必须严格等于 n不能多也不能少。如果你算出总价 22 元但手里有 23 元那就不符合要求。第二个是“总数尽可能多”。在满足花完钱的前提下笔的支数要最大这就要求我们优先考虑便宜的那一支。第三个是输出格式。多数题面要求按“钢笔、圆珠笔、铅笔”的顺序输出但也有的教材反过来做题前一定要看清样例输出的顺序。很多初学者一上来就想着枚举钢笔买 0 支、1 支、2 支……圆珠笔买 0 支、1 支、2 支……循环套循环然后把符合条件的组合记下来再比较总数。对于这道题枚举也不是不能做但数据稍大一点就会慢而且代码会明显变长。教材把它放在分支结构这一章就是希望你能用更聪明的方法解决先分析清楚几种固定情况再用 if-else 去处理。1.2 为什么它是“分支结构”的经典例题从教材编排来看这道题通常出现在学生刚学完顺序结构、还没接触循环的时候。它要考察的知识点非常集中整数除法和取余、多分支判断、以及最基本的边界意识。整数除法和取余是这里的主角。你要先算出 n 除以 4 的商和余数然后根据余数的不同选择不同的调整方案。这个过程不需要循环不需要数组只需要三个分支就能覆盖所有情况。这种“先分类再处理”的思路正是分支结构章节想训练的思维习惯。到了后面学贪心算法、背包问题的时候你会发现这种“先按某种规则选一个基准方案再根据剩余量做微调”的套路会反复出现。所以别小看这道例题它其实是在给未来的算法思维打地基。1.3 输出顺序这种细节往往是失分重灾区我见过不少学生在本地运行没问题一提交就 WA最后发现就是输出顺序反了。比如题目要你输出“钢笔 圆珠笔 铅笔”你写成了“铅笔 圆珠笔 钢笔”。更麻烦的是如果测试数据恰好让三种笔数量形成对称关系样例过了但其他数据全错。我的建议是读题时先不看数值只看输出格式。然后在代码里把变量名和输出语句一一对应最好加上注释比如// g 表示钢笔(gangbi)y 表示圆珠笔(yuanzhubi)q 表示铅笔(qianbi)这种习惯看着琐碎但在竞赛里能帮你省下大量无意义的调试时间。2. 核心思路为什么“先买铅笔”一定没错2.1 用除法先锁死“最多能买几支笔”这道题最关键的一步不是急着写 if而是先想清楚一个问题总支数的上限到底是多少因为三种笔里最便宜的是铅笔4 元一支那么 n 元钱最多不可能超过 n 除以 4 的整数部分。换句话说设 q n / 4整除就算你全部买铅笔也只能买 q 支。想买 q1 支的话至少要花 4 × (q1) 元也就是 4q 4 元这显然大于 n。所以总支数的最优值就是 q。这个推导是整个题目的“定海神针”。它告诉我们无论后面怎么调整组合总支数都不会超过 q。既然如此我们只要保证所有分支下的总笔数都等于 q就已经是最优了不需要再去比较其他方案。这个“先定上界再找方案”的思路很多初学者会忽略。他们喜欢把所有可能性列出来然后人工对比。当数据量小的时候没问题但这不是算法思维。算法思维是先用数学把搜索空间压缩到最小再考虑怎么处理剩余的情况。2.2 余额只有四种情况一张表全看懂既然先买 q 支铅笔那么 n 元里剩下的钱就是 n - 4q也就是 n 除以 4 的余数 r。这个余数只可能是 0、1、2、3 四种情况。现在的问题变成了在已经买了 q 支铅笔的前提下怎么处理这 r 元注意r 元本身不够再买任何一支笔因为最便宜的铅笔也要 4 元。所以只能从已经买好的铅笔里“退掉”几支把省下来的钱和 r 凑在一起换成更高价的笔。下面这张表就是四种情况的完整处理方案余数 r铅笔数圆珠笔数钢笔数总价验证0q004q n1q - 1104(q-1) 5 4q 1 n2q - 1014(q-1) 6 4q 2 n3q - 2114(q-2) 5 6 4q 3 n我们来逐个验证。如果 r 0说明钱正好够买 q 支铅笔不需要做任何调整全买铅笔就是答案。如果 r 1全部买铅笔会剩下 1 元。最简单的做法是退掉 1 支铅笔手里就多了 4 元加上剩下的 1 元一共 5 元刚好可以买 1 支圆珠笔。于是铅笔变成 q - 1 支圆珠笔 1 支。如果 r 2退掉 1 支铅笔后手里是 4 2 6 元刚好够买 1 支钢笔。于是铅笔变成 q - 1 支钢笔 1 支。如果 r 3这一种最容易写错。退 1 支铅笔只凑出 4 3 7 元既买不了钢笔也买不了圆珠笔没有任何一支笔正好卖 7 元。所以必须退 2 支铅笔凑出 8 3 11 元然后用 11 元买 1 支钢笔6 元和 1 支圆珠笔5 元。于是铅笔变成 q - 2 支钢笔 1 支圆珠笔 1 支。2.3 换个角度记低价打底差价升级这张表看起来有四个分支死记硬背容易出错。我后来给学生讲的时候更愿意用“低价打底差价升级”的说法。铅笔是最便宜的基础款先无脑买 q 支。圆珠笔比铅笔贵 1 元钢笔比铅笔贵 2 元。现在看手里的余钱 rr 0不用升级。r 1把 1 支铅笔升级成圆珠笔刚好补上 1 元差价。r 2把 1 支铅笔升级成钢笔刚好补上 2 元差价。r 3把 1 支铅笔升级成钢笔补 2 元再把另 1 支铅笔升级成圆珠笔补 1 元总共补 3 元。这样理解的话整个逻辑就变成了一种“补差价”的操作每一步都有明确的价格依据。就算隔一段时间再回来写这道题也不太容易忘。这个思路也可以类比到生活中假设你点了 3 份 20 元的基础套餐手里还有 5 元优惠券以外的零钱你想把其中一份升级成 25 元的套餐另一份升级成 30 元的套餐那你就得算清楚每份套餐的差价。道理完全相同。2.4 用一个具体例子走一遍完整流程光看表还不行我们拿一个实际数字验证一遍。假设 n 23。第一步算 q 23 / 4 5余数 r 23 % 4 3。第二步查表。r 3 对应的是铅笔 q - 2 3 支钢笔 1 支圆珠笔 1 支。第三步验证总价6 × 1 5 × 1 4 × 3 6 5 12 23正好花完。总支数 1 1 3 5等于最优值 q。这个例子完整展示了“除 → 取余 → 分支 → 验证”的流程。你在考试或写作业的时候最好也在草稿纸上走过一遍这样的流程而不是直接敲代码因为手算能帮你提前发现逻辑漏洞。3. 动手实现C、Python 与边界处理3.1 C 参考代码带注释说完了思路代码其实就是把刚才那张表翻译成 if-else。下面是一份可以直接用的 C 写法#include iostream using namespace std; int main() { int n; cin n; // 如果题目保证 n 4 且一定有解这段可以省略 // 但加上边界判断更稳妥 if (n 4) { cout 0 0 0 endl; return 0; } int g 0; // 钢笔数量 int y 0; // 圆珠笔数量 int q n / 4; // 铅笔数量先用最便宜的打底 int r n % 4; // 剩余金额 if (r 1) { q--; // 退掉 1 支铅笔 y; // 加 1 支圆珠笔 } else if (r 2) { q--; // 退掉 1 支铅笔 g; // 加 1 支钢笔 } else if (r 3) { q - 2; // 退掉 2 支铅笔 g; y; // 加 1 支钢笔和 1 支圆珠笔 } cout g y q endl; return 0; }注意看代码里的顺序先用整数除法算出 q再用取余算出 r最后才进入分支。很多新手会把取余和除法混在一起算结果 q 被后面的分支修改之后又拿修改后的 q 去计算别的东西导致逻辑混乱。这里 q 在分支里会变所以最好在进入分支之前就把 r 固定下来。3.2 Python 参考代码更贴近手算Python 的写法更直观因为整数除法//和取余%本身就是两个清晰的运算符。顺便说一句Python 的divmod函数可以直接同时拿到商和余数写起来更紧凑n int(input().strip()) g 0 y 0 q, r divmod(n, 4) if r 1: q - 1 y 1 elif r 2: q - 1 g 1 elif r 3: q - 2 g 1 y 1 print(g, y, q)这里的逻辑跟 C 完全一致区别只在语法。Python 的divmod(n, 4)返回(n // 4, n % 4)一步拿到商和余数可读性非常好。如果你用的是 C也可以自己封装一个函数来模拟这个效果但没必要因为只有两行。如果你在用 Java思路也是一样的int q n / 4; int r n % 4;然后写同样的 if-else。语言本身不重要重要的是那张表。3.3 边界情况n 小于 4 或者无解时怎么办前面说过教材题面一般会保证 n 足够大且一定有解。但真实比赛和练习系统里评测数据偶尔会“调皮”。如果 n 1、2、3连一支最便宜的铅笔都买不了按上面的代码会算出负数铅笔数量。比如 n 2 时q 0r 2进入 r 2 的分支后 q-- 变成了 -1输出就错了。再比如 n 7q 1r 3查表要求 q - 2 -1也是负数。这说明 7 元本身无法“正好花完”买到 1 支钢笔和 1 支圆珠笔总价至少 11 元而全买铅笔又剩 3 元所以 n 7 本身就是无解的。稳妥的做法是在进入核心逻辑之前加一个简单判断if (n 4) { cout 0 0 0 endl; return 0; }至于 n 7 这类“能买笔但无法正好花完”的情况教材题面一般会保证输入数据一定有解不需要我们额外处理。但你要明白这不是代码逻辑的缺陷而是题目数据范围的限制。碰到真正需要考虑无解情况的变体题就需要用枚举法去判断了。3.4 测试用例覆盖所有分支的一组数据写完代码千万别急着提交。先手动跑一遍下面这组数据确保每个分支都被覆盖到输入 n期望输出覆盖分支40 0 1r 0q 150 1 0r 1q 161 0 0r 2q 1111 1 0r 3q 2231 1 3r 3q 51000 0 25r 0q 25我建议你在本地至少跑这 6 组数据。4、5、6 分别覆盖了余数为 0、1、2 且 q 最小的情况11 和 23 覆盖了余数为 3 的两种情况100 是一个较大整数用来验证 q 较大时输出仍然正确。如果你是通过评测系统做的题目可能还会遇到输出末尾换行的问题。C 的endl或\n都可以Python 的print默认自带换行一般不会错。但如果你在循环里输出多个答案就要注意空行和空格问题这里暂时不展开。4. 常见问题与排查技巧实录4.1 余数为 3 时为什么不是退 1 支铅笔买 1 支钢笔这是初学者最容易踩的坑。r 3 时有人会觉得退 1 支铅笔手里有 4 3 7 元虽然买不了钢笔但可以再想想别的办法。问题是7 元刚好卡在圆珠笔 5 元和钢笔 6 元之间没有任何一支笔卖 7 元而且 7 元也买不了两支铅笔8 元。所以退 1 支铅笔的方案必然导致钱花不完。退 2 支铅笔之后手里是 8 3 11 元恰好等于 6 5也就是钢笔加圆珠笔的组合。这个组合是唯一的因为如果买两支圆珠笔是 10 元会剩 1 元买两支钢笔是 12 元超了。所以 r 3 时必须退 2 支铅笔。我还见过一种错误写法r 3 时写q - 1; g;总价变成 4(q-1) 6 4q 2比 n 少 1 元不符合“正好花完”。这种错误在样例上很可能看不出来因为样例数字大差 1 元不仔细算根本发现不了。4.2 写了多个 if 而不是 else if后果很严重初学者容易把四个分支写成四个独立的 if像这样if (r 1) { q--; y; } if (r 2) { q--; g; } if (r 3) { q - 2; g; y; }这种写法的问题是如果 r 恰好等于 1第一个 if 会执行但第二个 if 的条件r 2仍然是独立的判断不会因为第一个 if 执行了就跳过。所以当 r 1 时第一个 if 执行完第二个、第三个 if 的条件都为假最终结果碰巧是对的当 r 2 时第二个 if 执行第一个、第三个不执行结果也对当 r 3 时第三个 if 执行前两个不执行结果还是对。那么是不是多个 if 也没关系问题出在如果代码写得复杂比如你在分支里再次修改了 r 的值或者把if条件写成了r 0、r 1这类范围条件多个 if 就会连续触发数量被反复修改。为了保证逻辑清晰、可扩展建议还是用if - else if结构让四个分支互斥。还有一个更隐蔽的坑如果你在分支里先修改了 q然后又不小心把 r 的计算放到了分支之后比如int q n / 4; int r n % 4; // 如果这里又对 n 做了修改r 就不对了要记住r 必须在分支之前一次性算好后续不要再改变 n 或 r 的值。4.3 变量命名混乱导致输出错位我见过有人把钢笔命名成p铅笔命名成q然后输出的时候顺序搞混。C 的变量名本身没有约束叫什么都行但人很容易被字母误导。比如p到底是 pen钢笔还是 pencil铅笔一个实用的习惯是中文拼音缩写 注释。g表示钢笔gangbiy表示圆珠笔yuanzhubiq表示铅笔qianbi。如果你的输入法习惯用拼音这种命名比pen、pencil更直观。当然也可以直接用pen、ballpen、pencil只要你清楚就行。还有一种情况题目要求输出“钢笔 圆珠笔 铅笔”但是有些学生习惯先定义铅笔最后输出时下意识写成q y g导致前两个测试点全错。这种错误不是算法问题纯粹是细心问题解决方法是提交前盯着输出语句看 3 秒或者把输出语句单独写成一行cout g y q endl;哪边是钢笔哪边是铅笔一目了然。4.4 如果单价变了这道题还能这样解吗“买笔”这个模型有一种很自然的扩展如果三种笔单价不是 6、5、4而是任意价格怎么办比如钢笔 7 元、圆珠笔 5 元、铅笔 3 元。这个时候依然可以先用最便宜的铅笔打底算出 q n / 3然后看余数 r。但问题是差价变得复杂了圆珠笔比铅笔贵 2 元钢笔比铅笔贵 4 元余数 1 元根本无法通过升级来补足。这时候你就不能只用三个 if 解决问题了。更通用的做法是枚举两层循环遍历所有可能的钢笔数 g 和圆珠笔数 y然后计算剩余金额能否恰好买整数支铅笔并比较总数量哪个最大。虽然复杂度是 O(n²) 级别但只要 n 不大完全可行。这个扩展值得想一想因为它能帮助你理解本题设计的巧妙之处6、5、4 这三个价格竞争关系刚好让差价集中在 1 元和 2 元于是余数的四种情况可以完美地用简单的分支覆盖。如果差价再复杂一点题目就会从“分支结构”升级成“枚举优化”或“动态规划”了。我从第一次做这道题到现在最大的体会是代码本身永远是最简单的部分真正有价值的是你心里有没有那张表。我后来再带学生做这道题都会让他们先在纸上把四种余数的情况完整列一遍再动键盘写代码。等表格填明白了代码三分钟就能敲完。顺便说个我自己以前犯过的低级错误第一次做这题时我把“钢笔、圆珠笔、铅笔”的输出顺序想当然写成了“铅笔、圆珠笔、钢笔”样例碰巧是对称的结果提交上去直接红了一片。从那以后我养成了两个习惯先读输出格式再做一组手算测试数据。这两个习惯看起来跟算法没关系但在实际做题时能救命的往往就是这种细节。这道题真正的收获其实不在分支写法而在“先定上界、再补余量”的思考方式后面你接触贪心算法时会一遍又一遍地遇到它。
返回列表