ARTICLE DETAIL

资讯详情

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

USACO白银组2019年12月真题解析:数学周期、树上倍增与图论建模

USACO白银组2019年12月真题解析:数学周期、树上倍增与图论建模 如果你刷过几套近年的USACO白银组真题会发现2019年12月这一场非常典型——三题里既有一道看起来像“数数题”、实际上考数学周期的MooBuzz也有一道把很多Silver选手拦在门口、需要树上倍增的Milk Visits还有一道只要建图方向对了就能轻松拿下的Moocast。这套题没有堆砌冷门算法但每一道都踩中了备战白银组时最常见的思维盲区不会找规律、不会拆路径、不会判方向。这篇解析就是冲着这三个盲区来的。我会把三道题的完整推导过程、代码骨架、易错点和考后复盘整理出来适合三类人看刚从铜组升上来的选手、在白银组卡了挺久想找突破口的同学以及准备冲黄金但基础还不够稳的人。如果你只是想看看这套题长什么样直接看第一部分的全景表就够了。1. 2019年12月白银组整体画像三题正好测三种能力先给这套题画个像。USACO白银组每场三道题一般按难度递增排序但2019年12月这场的特点在于它并没有把难度全部堆在数据结构或图论上而是用三道题材完全不同的题目分别考察了“找规律”“树上查询”“有向图建模”这三个能力点。1.1 三题考查点速查表题号核心题面推荐解法主要知识点难度感受Problem 1 MooBuzz跳过3或5的倍数求第N个数找15循环节O(1)计算数学、周期、long long入门级但易错Problem 2 Milk Visits树上路径查询是否存在某种颜色的节点前缀和 LCA树上倍增DFS、深度、倍增表三题中最难Problem 3 Moocast从1号牛开始求广播最多能覆盖几头牛建单向边 BFS几何距离、图论建模、BFS中等方向易错从表格能看出来这套题没有考线段树、平衡树这些进阶数据结构最“硬核”的也就是LCA。但恰恰是这个LCA把Silver和Gold之间的断层给照出来了——很多选手能在铜组靠暴力拿到不错的名次但到了白银组第一题稍微绕一下第二题只要没学过树上倍增就会崩第三题则是对“有向边”敏感度的试金石。1.2 这套题更适合什么阶段的选手如果你刚过铜组我建议先把第一题和第三题吃透第二题可以先放一放如果你已经在白银组刷了一段时间那么第二题就是你判断自己是否具备“冲黄金基础”的标尺。我当时刷这套题的时候感受最深的是它给了你充分的暴力空间但又在每个暴力方案的边界处挖了坑。比如MooBuzz直接循环也能出结果但N到10^9就死给你看Milk Visits每次查询DFS路径也能得分但N和Q都到10^5就必然超时Moocast如果按无向图做并查集样例都不一定能过。换句话说这套题不是考你会不会背模板而是考你在“最省力的错误做法”和“稍微动脑的正确做法”之间如何选择。2. MooBuzz看似在数数其实是把周期从暴力里找出来2.1 题目核心与暴力为什么会超时题面很短FJ从1开始数数但跳过所有3或5的倍数。数出来的序列是1, 2, 4, 7, 8, 11, 13, 14, 16, 17, 19, 22, 23, 26, 28, ...问第N个数是多少N最大可以到10^9。很多人的第一反应是写个while循环一边枚举自然数一边判断这个数是不是3或5的倍数不是就计数计数到N就输出。这个思路在N很小的时候没问题但撑不住大数据。我们来算一下复杂度自然数里大约每15个中有7个是3或5的倍数8个不是所以第N个数大约出现在N * (15/8)附近。当N10^9时答案约等于18.75亿你的循环要跑将近20亿次判断。在USACO单测2秒的限制下这几乎是必超时的。所以这道题的价值就是逼你从“模拟”转向“找规律”。2.2 15个一循环的完整推导先想一个问题为什么每15个数会形成循环因为3和5的最小公倍数是15。整数x是不是3的倍数只取决于x mod 3是不是5的倍数只取决于x mod 5。而判断“x mod 3”和“x mod 5”只需要看x mod 15因为15是3和5的公倍数。所以在长度为15的连续整数区间里被3或5整除的数的分布模式完全一样。我们把1到15这15个数过一遍筛1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 保留 保留 删 保留 删 删 保留 保留 删 删 保留 删 保留 保留 删保留下来的是1, 2, 4, 7, 8, 11, 13, 14正好8个。所以整个序列等价于每15个数输出8个周而复始。接下来就是把这个周期转换成公式。设用户询问第N个数从1开始计数long long block (N - 1) / 8; // 前面完整经过了几个周期 int idx (N - 1) % 8; // 本周期内取第几个保留数 long long ans block * 15 keep[idx];其中keep数组就是上面那8个保留数int keep[8] {1, 2, 4, 7, 8, 11, 13, 14};验证一下N1时block0idx0ans1N8时block0idx7ans14N9时block1idx0ans16。和序列完全对得上。这里有个很隐蔽的细节为什么是(N-1)而不是N因为边界条件。当N8时如果直接用8/81、8%80会算出16但序列第8项明明是14。用(N-1)之后block和idx都从0开始就自然地匹配上了。2.3 实现细节与易错点代码非常短但几个细节决定了你能不能一遍过第一结果必须用long long。答案大约在N*15/8量级N10^9时可以到18.75亿超过int的21.47亿上限。USACO的坑在于它不会提醒你等你一个int拿到WA才发现已经晚了。第二不要用浮点。有些同学会写ans (long long)(N * 1.0 / 8 * 15)这类式子一旦涉及浮点就可能出现精度误差尤其是在大数和边界场景下。老老实实用整数除法和取模。第三如果对周期不放心可以用暴力对拍。我刷题时习惯写一个10行的小暴力函数枚举前1000项再和周期公式生成的结果比对确认完全一致之后再交。这个习惯能帮你挡住90%的边界错误。这类“求序列第N项”的题在USACO里出现过很多次通用套路就是三步找一个最小周期暴力枚举一个周期里的合法项再用block和remainder换算。MooBuzz只是最基础的演示后面很多类似题都能用这套思路套。3. Milk Visits树上路径查询的正确打开方式3.1 如果只会暴力DFS这道题能拿多少分这道题是2019年12月白银组的分水岭。题目大意是一棵N个节点的树每个节点上有一头牛颜色要么是G要么是H。给出Q次询问每次询问两个节点a、b之间是否存在颜色为c的节点。N和Q都能到10^5。如果每次询问都从a走到b边数最坏是N-1复杂度就是O(NQ)10^10次操作跑完基本等于比赛结束。所以这条路走不通必须预处理一些信息。这道题在Silver级别的意义就很特殊它明面上是白银组题目标准解法却涉及LCA最近公共祖先而LCA通常被归到Gold入门知识里。很多同学第一次刷这套题时会在第二题卡两小时就是因为没接触过树上倍增。3.2 前缀和 LCA 的公式到底怎么来的先建立一个直观感如果我们能快速知道“从根节点到任意节点u的路径上有多少个G节点、多少个H节点”那么任意两点a到b路径上的颜色数量就能用前缀和推导出来。定义gcnt[u] 从根到u的路径上包含u本身G节点的数量 hcnt[u] 从根到u的路径上包含u本身H节点的数量 depth[u] u的深度根的深度记为1设l是a和b的最近公共祖先LCA。那么路径a到b其实由两部分组成a向上走到l再向下走到b。注意这里真正重复的部分是“根到l”这一段因为gcnt[a]里包含了根到l的贡献gcnt[b]里也包含了根到l的贡献而实际路径a到b并不需要根到l这段。所以路径上的G节点数量是pathG gcnt[a] gcnt[b] - 2 * gcnt[l] (color[l] G)这个式子怎么理解gcnt[a] gcnt[b]把“根到l”算了两遍所以要减去两遍gcnt[l]。但gcnt[l]里包含了l节点本身如果l是G节点减去两遍意味着l的贡献被完全减掉了可路径上明明要经过l一次所以要把它加回来。同样的道理pathH hcnt[a] hcnt[b] - 2 * hcnt[l] (color[l] H)回答询问时如果询问的颜色是G就看pathG是否大于0如果询问的是H就看pathH是否大于0。这里有一个重要的约定gcnt和hcnt必须定义为“包含当前节点u本身”。如果不包含公式最后一步就不用加color[l]了但很多人实现时概念混了导致公式始终差一个1很难排查。接下来问题就剩下如何快速求任意两个节点的LCA。对Silver选手来说最直观的是树上倍增。预处理阶段从根节点做一次DFS记录每个节点的父节点指针同时计算出depth、gcnt、hcnt。用倍增思想预处理一个二维数组up[u][k]表示从u节点向上跳2^k步到达的节点。up[u][0]就是u的父亲。查询LCA时如果depth[u] depth[v]先把u向上调到和v同深度。如果此时u和v相同说明v就是LCA。否则从大到小枚举k只要up[u][k]不等于up[v][k]就把u和v同时往上跳。结束后u和v的父节点就是LCA。第3步的原理是为了让u和v跳到LCA的两个子节点如果up[u][k]和up[v][k]相同说明跳过头到了LCA上面这个k不能采用只有跳完的节点不同才说明两者仍然在LCA两侧可以放心跳。3.3 代码骨架与三个容易踩的坑下面的代码用C给出N最大10^5因此logN大约17取LOG18或者20都行建议开大一点避免边界问题。#include bits/stdc.h using namespace std; const int MAXN 100005; const int LOG 18; vectorint g[MAXN]; char col[MAXN]; int depth[MAXN], up[MAXN][LOG]; int gcnt[MAXN], hcnt[MAXN]; void dfs(int u, int p) { up[u][0] p; depth[u] depth[p] 1; gcnt[u] gcnt[p] (col[u] G); hcnt[u] hcnt[p] (col[u] H); for (int v : g[u]) { if (v ! p) dfs(v, u); } } int lca(int u, int v) { if (depth[u] depth[v]) swap(u, v); int diff depth[u] - depth[v]; for (int k LOG - 1; k 0; k--) { if ((diff k) 1) u up[u][k]; } if (u v) return u; for (int k LOG - 1; k 0; k--) { if (up[u][k] ! up[v][k]) { u up[u][k]; v up[v][k]; } } return up[u][0]; } int main() { // 读入n, q // 读入颜色字符串下标从1开始 // 读入n-1条边建双向邻接表 // dfs(1, 0) // 预处理up[i][k] up[ up[i][k-1] ][k-1] // 每次询问cin a b c; // int l lca(a, b); // int pathG gcnt[a] gcnt[b] - 2 * gcnt[l] (col[l] G); // int pathH hcnt[a] hcnt[b] - 2 * hcnt[l] (col[l] H); // 根据c判断输出Y或N }这里还有三个坑值得专门说一下。第一DFS时一定要用父节点参数p来防止走回头路。树是无向图如果只用vis数组也不是不行但父节点判重更简洁。一不小心把树的边当有向图读会漏掉一半节点。第二up数组的预处理必须在DFS完成之后做。因为up[u][k]依赖up[u][k-1]而up[u][0]是在DFS里确定的。你可以用循环预处理也可以直接在DFS返回后统一处理。顺序错了会出现随机值。第三读入询问的颜色字符时注意字符和换行符。如果你用cin a b c通常没问题如果用scanf要小心%c会读入空格或换行建议在%c前面加一个空格。如果你还没有学过LCA这道题就是一个极好的入门素材。虽然在Silver组里碰到LCA有点超纲但USACO的题目从来不管知识点划分它只关心你能不能解决这个问题。3.4 这题在Silver阶段的定位提前接触Gold知识点很多教练会把Milk Visits当作Silver和Gold之间的桥梁题。因为Silver正式知识点里没有要求你掌握LCA但这道题如果不用LCA就需要用DFS序加离线技巧反而更绕。所以USACO官方在出这道题时某种意义上就是在告诉你树上路径类问题很快会出现在你的晋级路上不如现在就开始接触。我的建议是如果你现阶段还在补DFS和BFS先不要在这题上耗太久看明白前缀和公式就好如果你的目标是下一次晋级黄金那就把树上倍增彻底练熟之后做Gold的Tree DP和路径统计题会顺很多。4. Moocast建图方向错了整题就没了4.1 为什么是“单向边”很多Silver选手折在这里Moocast的题面描述了一群牛每头牛有一个坐标和一个“功率”可以把消息传给距离不超过自己功率的牛。消息可以通过牛与牛之间接力传播。问从1号牛出发最多能有几头牛收到消息。这个题最大的坑就藏在“方向”里牛i可以传给牛j并不代表牛j也能传给牛i。因为两头牛的功率不一定相同。举个简单例子牛1的功率是1牛2的功率是100两头牛距离5。牛2能传到牛1但牛1传不到牛2。如果从牛1出发它只能收到自己的消息牛2根本不会理它。很多同学一看到“传播”“覆盖”本能地想到并查集把所有能互相通信的牛合并成一个连通块然后统计所在块的大小。这在无向边场景下是对的但这题是有向边并查集只适合处理“双向可达”强行套上去就会把上面例子的答案算成2。这道题的建模思路应该是把每头牛看成图上的一个点牛i有一条指向牛j的有向边当且仅当牛i到牛j的欧氏距离不超过牛i的功率。然后从1号节点出发做BFS或DFS数一数能访问到多少个节点。4.2 O(N²)建图 BFS 的实现要点N的范围在1000级别所以直接枚举所有有序对(i, j)建图是完全可行的复杂度O(N²)。这个数据范围的设计就是允许你用暴力建图。距离比较时强烈建议用平方比较不要用sqrtlong long dx x[i] - x[j]; long long dy y[i] - y[j]; if (dx * dx dy * dy p[i] * p[i]) { g[i].push_back(j); }原因是浮点开方既慢又有精度风险而整数比较在坐标和功率不超过10^9时完全够用。注意这里dx*dx可能达到10^18级别所以要开long long否则两个int相乘直接溢出。下面是一个完整的实现思路#include bits/stdc.h using namespace std; const int MAXN 1005; long long x[MAXN], y[MAXN], p[MAXN]; vectorint g[MAXN]; bool vis[MAXN]; int main() { int n; cin n; for (int i 1; i n; i) { cin x[i] y[i] p[i]; } for (int i 1; i n; i) { for (int j 1; j n; j) { if (i j) continue; long long dx x[i] - x[j]; long long dy y[i] - y[j]; if (dx * dx dy * dy p[i] * p[i]) { g[i].push_back(j); } } } // BFS from 1 queueint q; q.push(1); vis[1] true; int cnt 0; while (!q.empty()) { int u q.front(); q.pop(); cnt; for (int v : g[u]) { if (!vis[v]) { vis[v] true; q.push(v); } } } cout cnt endl; return 0; }注意一个细节计数变量cnt从0开始每从队列弹出一个节点就加1。起点1号牛本身也会被弹出所以最终答案自然包含了起点自己。有些同学会先cnt1再把1号牛入队也没错但容易在后面重复计数。如果题目改成了“从任意一头牛出发最多能让多少头牛收到消息”那就要对每个起点跑一次BFS总复杂度O(N*(N²N²))在N1000时大概10亿级别C优化后勉强能跑但不是最优雅的方案。更优的做法是用bitset做传递闭包这里不展开Silver阶段能拿稳定满分即可。4.3 这类“可达性”题目的通用思考顺序Moocast本质是一个有向图可达性问题。以后遇到类似的题材我的思考顺序通常固定为四步先判断边是有向还是无向。看题目条件里“A能到达B”是否等价于“B能到达A”只要题干出现功率、能力、权限这类不对称条件几乎都是有向。再决定用并查集还是BFS/DFS。无向连通需求用并查集有向可达需求用图的遍历。接着看数据范围决定建图方式。N小直接O(N²)N大则需要排序剪枝或用更高级的数据结构。最后考虑起点个数。单起点做一次遍历多起点通常要对每个起点处理必要时用状态压缩或bitset优化。这套顺序不只适用于USACO很多OI竞赛题里的图论题都能按这个框架快速定位思路。5. 复现这套题的完整流程与赛后复盘5.1 模拟赛时间分配建议如果你把这场真题当作模拟赛来做我建议按“20分钟、40分钟、80分钟、剩余检查”的节奏分配第一题MooBuzz属于快速拿分题20分钟足够包括理解题意、写出公式、对拍验证。第二题Milk Visits是最难的一题给80分钟是因为它要写DFS、倍增表、前缀和三个部分任何一个环节出错都要花时间排查。第三题Moocast给40分钟主要时间花在建图的边界处理上。我见过不少人的时间分配是反的在第一题暴力写了一个小时在第三题纠结并查集最后第二题只能交个暴力。USACO是按测试点给分的宁可第二题只拿部分分也要保证第一题和第三题全绿。5.2 三题放在一起暴露的知识短板赛后复盘时我建议你用这张对照表检查自己到底缺什么题目你卡住的点对应短板MooBuzz没想到15周期数学归纳与最小周期意识MooBuzzint溢出数据范围感知不足Milk Visits不会求LCA树上倍增算法缺失Milk Visits前缀和公式推导不清树上路径与贡献拆分能力Moocast按无向边处理有向/无向建模判断Moocast距离用sqrt写法数值稳定性意识如果三题里有超过两项命中那就说明你现在并不缺“某个模板”而是缺一套系统的读题和建模流程。这时候继续大量刷题效率反而不高不如停下来把每个知识点的小专题做一做再回来重刷这套题。5.3 从这套题往后银组升金组该补什么把2019年12月这场吃透以后接下来的训练方向就很明确了第一个方向是二分答案。Gold组很多题都长着“求最小可行值”或者“最大化某条件”的脸二分会成为你第一个必须熟练的工具。第二个方向是线性DP和背包。Silver升GoldDP几乎是必考项至少要能把最长上升子序列、0/1背包、区间DP的朴素写法秒出来。第三个方向是最短路和最小生成树。Gold组图论题比Silver多很多Dijkstra、Kruskal、拓扑排序都是基础。第四个方向是树上的更多操作。Milk Visits让你接触了LCA后续Gold组还会出现树的直径、重心、树上差分、树形DP这些都可以顺着LCA这条线继续学。我个人复盘这套真题时最大的体会是它看似只是一套Silver题实际上每道题都替你指了一条往后要走的路。第一题引你学会找规律而不是硬算第二题引你进入树上倍增和路径问题第三题引你养成建模前先判方向的习惯。如果你能把这三道题背后的思维方式变成自己的默认反应那这套题刷一遍的价值远大于囫囵吞枣刷二十道同类题目。下一次模拟赛不妨就按这个标准来要求自己。
返回列表