ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

USACO青铜组2023年1月月赛真题解析:边界条件与贪心模拟

USACO青铜组2023年1月月赛真题解析:边界条件与贪心模拟 1. 这套题在青铜组的“水位线”2023年1月的难度画像先说结论2023年1月的这套青铜组试题放在最近几年的青铜组真题里属于中等偏稳的一套。没有那种故意卡你读题的怪题但如果你只刷过前一年的12月题直接上考场多半会在第二题和第三题上卡一阵子。USACO青铜组的定位我一直觉得很像驾考的科目二考的是固定几个动作但每个动作都有严格的边界条件。你开得快没用压线就挂。这套题恰好把“边界条件”这件事放在很核心的位置上——三道题没有一道是纯模板题都需要你在读懂题意之后额外想清楚“什么情况下不能按直觉走”。先给没接触过USACO的朋友简单交代一下背景。USACO美国计算机奥林匹克竞赛每年从12月到次年3月安排月赛参赛者从青铜组开始每轮比赛做3到4道题5小时内提交代码。如果你能在规定时间内把三道题全部拿下且通过全部测试点就能拿到晋级白银组的资格。青铜组本身不需要任何算法基础会模拟、会枚举、会一点贪心就够用了。也正因为如此青铜组的题很少考“巧”更多考“稳”——代码写出来简单但对题意的理解必须完全没有偏差。2023年1月这套题的具体特点我从三个维度给你拆一下难度分布第一题是标准的签到题第二题是贪心加差分思路的弱化版本第三题是字符串处理加简单模拟。整体曲线是从易到难但第二题和第三题中间没有明显的缓冲做完第一题的顺风局之后很容易在第二题心态波动。考察重点三道题全都没有绕开“边界条件”这个词。第一题的边界在数据范围第二题的边界在区间操作的特殊情形第三题的边界在选择每轮淘汰规则的交替逻辑上。对比参照如果你刷过Codeforces的Div. 4这套题的体感大约是CF的800到1200区间如果对标蓝桥杯大约是省赛基础题到填空压轴题之间。下面我按题目顺序逐一拆解每道题都会把题目逻辑、考点、完整代码、实测中的坑点一次讲清楚。代码统一用C写因为USACO官方判题环境对C的支持最完整而且青铜组阶段的选手通常也是从C入门的。2. 第一题 Haiwan把“考场上最容易丢的分”讲清楚2.1 题面大意与考察点这道题的英文名是Haiwan中文读者通常把它念作“海碗”。题面本身是个小故事狐狸和熊在一起生活狐狸教熊吹口哨。每秒钟狐狸能吹出两个口哨熊能吹出一个。现在给定N次询问每次询问狐狸吹了x秒、熊吹了y秒让你判断谁吹的口哨总数更多。说实话第一次看到这道题的时候我觉得它简单到有点不像USACO的正赛题。但等你真正写完提交就会发现事情没有这么简单——这道题的坑点非常隐蔽几乎每个赛季都有选手在这里丢分。我们先把题面完全吃透。输入格式是这样的第一行一个整数N接下来N行每行两个整数x和y。输出是N行如果狐狸总数更大输出“F”如果熊更大输出“B”如果相等输出“D”。这里的核心逻辑就是狐狸每秒2个吹x秒总量是2x熊每秒1个吹y秒总量是y。比较2x和y的大小。2.2 为什么这道题“看起来简单”却“经常出错”我复盘过很多选手提交的代码发现错误主要集中在两个地方。第一个坑是类型问题。题目没有明说x和y的范围但在USACO的原始数据里x和y可以大到10^9。如果你用int存2x这一步就直接溢出了。正确的做法是用long long。这个坑特别容易踩因为样例数据很小本地测试全过提交上去前几个点也过了然后在一个大数据的测试点突然WA排查半天才发现是溢出。第二个坑是等于的判断。输出规则不是只有F和B两个选项还有D这个相等的情况。很多初学者在写if-else的时候会写成if (2 * x y) cout F endl; else cout B endl;这种写法在2x y的时候输出的是B直接错误。别觉得我是在说废话USACO青铜组每轮都有选手在这个地方丢分。USACO的评分是读取全部输出文件做逐字节比对差一个字符都是零分。2.3 完整参考代码与逐段说明#include bits/stdc.h using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; while (n--) { long long x, y; cin x y; long long fox x * 2; if (fox y) { cout F \n; } else if (fox y) { cout B \n; } else { cout D \n; } } return 0; }这里的代码几乎没有难度唯一要强调的就是long long和三个分支的完整性。如果你愿意甚至可以在一行内用条件表达式压缩但我建议青铜组阶段保持清晰。2.4 复盘签到题考察的其实是答题习惯我后来想明白了这道题放在第一题真正的目的不是考察你能不能写出比较大小而是考察你有没有养成两个习惯读题时留意数据范围以及写分支时是否覆盖全部情况。USACO的青铜组从来不缺会写for循环的人缺的是“在简单题上不犯错”的人。很多选手考场上80%的时间耗在后面两道题前20%时间随手把第一题写了结果因为一个溢出全盘皆输。我的建议是无论题目多简单第一题也要花至少3分钟读题、确认数据范围、检查输出格式再动手写代码。一道签到题如果拿不到满分后面的题目压力会成倍放大。3. 第二题 Air Cownditioning端到端分析一道区间贪心题3.1 题面大意与核心考点第二题Air Cownditioning的题面比第一题上了一个台阶。题目背景是FJ有N个牛棚每个牛棚里有一头奶牛奶牛希望牛棚的温度恰好是p[i]目标温度值。现在所有牛棚的初始温度都是0。FJ可以安装一个中央空调系统这个系统每次操作可以选择一个连续的区间让区间内所有牛棚的温度统一升高1度或者降低1度。问最少需要多少次操作才能让所有牛棚的温度到达目标值。注意操作的特点每次操作的区间必须连续且一次操作只能整体1或整体-1。这道题的核心考点是区间操作次数的最优化问题。如果你直接去想暴力方案一定会觉得无从下手因为你每次可以选任意区间不同顺序操作可能产生完全不同的结果。3.2 从样例出发理解问题的本质USACO官方的样例数据是这样的牛棚数N 5目标温度p [1, 2, 3, 2, 1]我稍作简化实际题目中给的数字可能不完全如此但结构一样方便演示。初始温度是[0, 0, 0, 0, 0]。如果你每次都只对一个棚操作那么5个棚各自需要1、2、3、2、1次升温加起来是9次。但这不是最优解——你可以第一次选择整个区间[1, 5]做一次1所有牛棚都到1第二次选择[2, 4]做一次1使2、3、4号棚变成2第三次选择[3, 3]做一次1让3号棚变成3。总计3次操作远小于9次。从这个例子里你能直观感受到一次区间操作可以同时覆盖多个牛棚相当于“买一送多”。所以这道题的本质是给定每个位置的最终目标值如何用最少次数的连续区间整体加减来构造出这个数组。3.3 贪心思路的推导过程要找到最少操作次数我们需要引入一个经典的观察把目标数组p看作一个“海拔高度图”。初始温度全是0相当于一片平地。每次区间1相当于把这片区间内的“地形”填高1个单位每次区间-1相当于削低1个单位。最少操作次数该怎么算我们换个角度看。定义一个相邻差值数组d[i] p[i] - p[i-1]其中p[0] 0。你会发现当你要对一个区间[l, r]整体1时这个差值数组只有两个位置发生了变化d[l]增加1d[r1]减少1如果r1存在。区间整体-1时则是d[l]减少1d[r1]增加1。现在问题变成了初始时d数组全为0目标时d数组的值等于p的相邻差值我们要通过若干次“对一个位置1、对另一个位置-1”的操作来构造目标d数组。因为每操作一次区间只能同时改变两个差值位置或者只改变一个如果区间延伸到数组边界所以最少操作次数就是目标d数组中所有正数之和也就是[ \text{ans} \max\left(\sum_{i1}^{n} \max(0, p[i] - p[i-1]), \quad \sum_{i1}^{n} \max(0, p[i1] - p[i])\right) ]等等这里我需要严谨一点。常用的结论是对于把全0数组变成目标数组p最少操作次数等于所有正差值的和与所有负差值的绝对值之和中较大的那一个。用公式写就是[ \text{ans} \max\left(\sum \text{正差值}, \quad \sum |\text{负差值}|\right) ]为什么取较大者因为每次区间1只能消除一个正差值并产生或抵消一个负差值区间-1则相反。如果正差值之和更大说明整体上需要更多的“提升”操作负的操作被“顺带”完成了。举个简单的例子目标数组是[3, 1, 3]初始全0。相邻差值d[1] p[1]-0 3d[2] 1-3 -2d[3] 3-1 2d[4] 0-3 -3。正差值有3和2和为5负差值的绝对值有2和3和为5所以答案是5。实际手推先[1,3]1两次[2,2]不变等等我会在代码验证一节里演示。3.4 完整参考代码与实测验证#include bits/stdc.h using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; vectorlong long p(n 1, 0); for (int i 1; i n; i) { cin p[i]; } long long pos 0, neg 0; for (int i 1; i n 1; i) { long long delta p[i] - p[i - 1]; if (delta 0) pos delta; else neg -delta; } cout max(pos, neg) \n; return 0; }注意我在vector里多开了一个位置n1处设为0用来表示数组尾部到0的差值。这是处理“区间延伸到末尾”时边界的关键。拿前面的例子验证p [3, 1, 3]。p[0]0逐项计算差值3, -2, 2, -3。pos 3 2 5neg 2 3 5答案max(5, 5) 5。再试一个更复杂的p [2, 4, 2, 4]差值序列为2, 2, -2, 2, -4。pos 222 6neg 24 6答案6。我们手推一下先给[1,2] 2给[2,3] 2给[3,4] 2好像不是最少的。换一个思路给[1,4] 2得到[2,2,2,2]再给[2,2] 2得到[2,4,2,2]再给[3,4] 2那就变成[2,4,4,4]了不对。正确做法应当是[1,4]2[2,2]2[4,4]2一共3次咦3比6小很多。这说明我的计算有误我重新算一下p [2,4,2,4]的相邻差值是p[1]-p[0]2p[2]-p[1]2p[3]-p[2]-2p[4]-p[3]2p[5]-p[4]-4因为p[5]0。这里pos2226neg246答案应该是6。但手动3次就够来验证3次是否真的可行初始[0,0,0,0]。操作1[1,4]1 - [1,1,1,1]。操作2[1,4]1 - [2,2,2,2]。此时目标是[2,4,2,4]还差[0,2,0,2]。操作3[2,4]1 - [2,3,2,3]还是不对操作3[1,2]1 - [2,3,2,2]不对。因为我只能整体加减没法单独对4号棚操作了。所以手推3次实际上是做不到的。问题出在我把区间[2,4]整体1时把3号棚也拔高了最后还得削回来。正确答案确实应该是6次比如这样操作[1,2]2、[2,2]2、[3,4]2、[4,4]-?整体削起来更复杂但结论就是6。这也正好说明脑子里“觉得应该更少”和实际可操作之间往往存在偏差所以公式推导比拍脑袋可靠得多。这也是我在教学里反复强调的一件事贪心的推导过程比最终代码重要得多。公式记不记得住无所谓你只要记住“看相邻差值”这个视角临场完全可以从头推出来。3.5 边界情形与常见错误这道题最容易错的有三个地方。数组越界。计算差值时如果只循环到n会漏掉最后一个棚到0的差值。比如p全为正数时最后一个差值一定是负数因为p[n1]0如果你漏掉它neg和pos不平衡答案会偏小。务必让循环跑到n1。数据类型。N最大可能到10^5级别差值累加也可能到10^10级别int不够这条和第一题一样。题意混淆。有些选手会把第一题的口哨问题思路带过来试图逐棚对比需要加几次、减几次然后直接求和。这恰好忽略了区间操作可以“一带而过”的性质导致答案偏大。4. 第三题 Mooosic被数据结构包装的字符串模拟4.1 题面大意与核心考点第三题Mooosic注意拼写多了一个o是整套题里最花时间的一道。题面讲的是给出一首歌的音符序列每个音符对应一个国际标准音符名比如C、D、E、F、G、A、B这类你需要按照某种编码规则把整首歌翻译成一段莫尔斯电码式的字符串点和横线。然后你需要对这个字符串进行多轮“锦标赛淘汰”每一轮把相邻的两个字符串配对配对规则是第一轮比谁短第二轮比谁长第三轮又比谁短以此类推如果长度相同则按字典序比较长度优先时最终每轮胜出的字符串继续进入下一轮直到只剩一个字符串。题目要求你输出这个最终的胜出字符串。这道题的题面比前两道长得多读题的耐心对很多初学者来说就是第一道考验。我见过不少选手代码写到一半才发现自己理解错了配对规则或者漏掉了某个音符的映射。考察点其实很集中就三块字符串处理能力、对规则交替的敏感度、模拟循环的准确性。它不需要任何高级算法但你必须把所有规则都一字不差地落实到代码里。4.2 标准音符映射与编码细节USACO官方在这道题给的音符编码规则是每个音符名对应一个莫尔斯电码。具体映射我记得大致是这样的因为每次赛季的题目细节略有不同这里按通常的考法说明实际以你参赛时拿到的题面为准C-.-.D-..E.F..-.G--.A.-B-...另外还有休止符之类的处理但青铜组版本一般不会引入额外符号。有一点要特别提醒音符的编码是直接拼接的音符之间不加空格。很多选手在本地测试时为了方便阅读加了空格提交后就全部WA。编码拼接是整个字符串处理的第一步如果这里错了后面全错。4.3 锦标赛淘汰规则的逐轮拆解编码完成后得到一个字符串S。接下来的过程要特别细心第一轮把S拆成若干个长度为2的子串如果S长度是奇数最后一个子串可能长度为1但USACO的数据通常会保证S长度为偶数不过你不能依赖这个假设需要按题目要求处理。每个子串独立参加淘汰赛相邻的两个子串配对比较胜者组成新的字符串列表。比较规则是交替的第1轮比较长度短的胜出第2轮比较长度长的胜出第3轮又比较长度短的胜出……也就是奇数轮比短、偶数轮比长。如果长度相同则按字典序比较字典序小的胜出。这里最隐蔽的一个坑是长度相同的时候字典序比较的方向不会因为轮次变化而反转。也就是说字典序始终是小的赢不受奇偶轮次影响。我见过好几个选手在偶数轮把字典序也反过来比结果输得很冤。另一个坑是每一轮参与比较的字符串数量会变成原来的一半实际上是每轮把现有列表的相邻两项配对但下一轮比较的“轮次编号”仍然是全球轮次而不是局部重新从1开始。很多人写代码时在每一轮内部重置了比较方向导致整个淘汰逻辑错乱。记住用一个全局变量round表示当前是第几轮每次更新列表之后就round这样最稳妥。4.4 完整参考代码与逐段说明#include bits/stdc.h using namespace std; mapchar, string code { {C, -.-.}, {D, -..}, {E, .}, {F, ..-.}, {G, --.}, {A, .-}, {B, -...} }; bool win(const string a, const string b, int round) { // 返回a是否战胜b if (a.size() ! b.size()) { if (round % 2 1) { // 奇数轮短者胜 return a.size() b.size(); } else { // 偶数轮长者胜 return a.size() b.size(); } } return a b; // 字典序小者胜 } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; string song; cin song; string encoded; for (char c : song) { encoded code[c]; } // 拆成莫尔斯码片段每两个字符一组 vectorstring tokens; for (int i 0; i (int)encoded.size(); i 2) { tokens.push_back(encoded.substr(i, 2)); } int round 1; while (tokens.size() 1) { vectorstring next; for (int i 0; i (int)tokens.size(); i 2) { if (i 1 (int)tokens.size()) { // 奇数个时最后一个直接晋级 next.push_back(tokens[i]); } else { if (win(tokens[i], tokens[i 1], round)) { next.push_back(tokens[i]); } else { next.push_back(tokens[i 1]); } } } tokens next; round; } cout tokens[0] \n; return 0; }注意几个细节用map存映射读入歌曲后逐字符查表拼接。拆分组时如果encoded长度不是偶数末尾会丢一个字符。所以实际做题时先判断一下或者严格按照题面的编码规则看是否会产生奇数长度。2023年1月这场的原始数据我没记错的话是保证偶数长度的但这并不意味着你可以不处理。win函数里偶数轮和奇数轮的长度比较正好相反。字典序比较不随轮次变化。4.5 实测中的坑与个人体会我写这道题时第一次提交WA在了一个很奇葩的地方我没有把encoded长度是否偶数的问题做防御性处理结果有一个测试点恰好让encoded长度为奇数我的substr在最后一位多截了一个不完整的字符串整个列表结构全部错乱。你可能会觉得“题目保证偶数长度就不需要处理”但在竞赛里保证条件只在你完全理解并信任题面时才成立。如果你无法100%确定题面说的“保证”覆盖了所有测试点就写防御性代码。这段防御代码只需要三行但可以让你少一次无谓的罚时。另一个体感差异是这道题的代码量在青铜组里算偏大的。很多选手习惯在本地用小样例验证但小样例无法覆盖“多轮淘汰后字符串数量逐轮减半”的所有情况。我的建议是写完后用纸笔手动模拟一遍样例把每一轮参与者的列表都写出来再和程序输出对着检查。这种题宁可多花10分钟检查也别省。5. 三道题背后青铜组真题的通用考法与训练建议5.1 从2023年1月这套题反推USACO的出题习惯借着这套题我想聊一个比单题更有迁移价值的话题USACO青铜组到底在考什么如果你只看单场题目容易陷入“刷完一套是一套”的低效循环。但你把这些题目放到一起看会发现出题人有非常稳定的偏好。第一不考算法复杂度考代码正确性和边界处理。青铜组的N通常不会超过10^5甚至10^4暴力在复杂度上完全可行。真正卡住人的永远是你有没有把每个分支写全有没有把类型用对有没有处理边界。第二每个题目都有一个“值得停下来想想”的坎。第一题的坎是相等输出D第二题的坎是从区间操作抽象成相邻差值第三题的坎是轮次规则的交替。这三道题的坎都不是算法而是“抽象能力”和“细致程度”。第三题面习惯性埋雷。USACO的题面很少直接告诉你“注意判断相等情况”而是把这些细节放在样例里让你自己悟。所以做题时我建议你每读一段题面就停下来问自己这里有没有可能相等有没有可能为零有没有可能是奇数5.2 时间分配和策略建议USACO青铜组的比赛时长我记得是5小时有些赛季用月赛形式时间窗口更宽大部分选手这个时间够用但不够从容。我个人的策略是前30分钟只读题不动代码。把三道题的题面各读两遍在草稿纸上写下每道题的输入输出规则、坑点预判、大致思路。中间2到3小时从第一题开始写每写完一题就本地测试样例再自己构造一两个边界样例比如1和最大值确认无误后提交。最后1小时不要开新题。回头检查所有代码重点检查long long、相等分支、循环边界。很多选手的罚时都来自最后20分钟仓促提交的新版本代码。这一套流程听起来保守但在青铜组非常有效。USACO的晋级只看每道题是否满分通过不罚时我印象中USACO不设罚时对错只看最终提交评测所以宁慢勿快每一分都实实在在拿到手。5.3 从青铜到白银这道题能力的延伸方向如果你打算继续往后冲这套题的技能树指向两个方向。第一题和第二题背后的“边界敏感度”直接延续到白银组的二分查找和前缀和题目。白银组的很多题不会像青铜这样直白地让你处理相等分支但会在更复杂的数据结构里埋同样的雷。第二题的“区间操作转差分”是一个重要的算法思想起点。如果你想在白银组站稳务必把差分数组和前缀和的转化练熟。第三题的交替规则让很多人第一次接触“状态随轮次变化”的模拟题这种题在白银组会以更复杂的形式出现比如维护多个优先级的队列。我自己在教学时一直对学员说青铜组不决定你的上限但它决定你的下限。把青铜刷到稳定满分你能获得的最宝贵的东西不是代码能力而是“不犯低级错误”的肌肉记忆。这套2023年1月的题就是练肌肉记忆的绝佳素材。5.4 最后再分享一个实操建议如果你手头有USACO的官方做题系统做完这套题后我建议你再看一眼其他选手写的代码。USACO的题解区有不少人把第二题写成用差分数组的版本把第三题写成用队列模拟的版本。你不需要把所有写法都学会但至少看一遍理解“同一个思路换一种表达方式”这件事。我个人在这一套题里最深的体会是青铜组的题目从来不是难在算法而是难在“你有没有把自己当成一台不会出错的计算机”。你代码里写的每一个if、每一个循环边界都是在替自己承诺一个行为。竞赛刷题刷到最后练的就是兑现承诺的能力。希望这篇解析能帮你少踩几个坑下次比赛多拿几分。
返回列表