第495场周赛Q1和Q2我加起来打了不到十五分钟。不是因为我手速多快而是这两道题几乎是“模板级”的考法一道用哈希做数学配对一道用贪心做区间选择。这种题在周赛前两题里出现频率极高真正决定你能不能拿分的往往不是算法本身而是边界条件有没有想全。这篇复盘适合刚打周赛、或者一直卡在Q2过不去的朋友。我会把这两题的完整思路、代码、以及赛后整理出来的边界坑都写清楚顺便聊聊怎么把这种“套路题”变成稳定送分题。1. Q1复盘余数哈希把“能被k整除的和对”压成O(n)1.1 题面还原与第一反应先还原一下这场的Q1。题面大致是给定一个整数数组nums和一个整数k统计所有满足i j且(nums[i] nums[j])能被k整除的下标对数量。第一反应基本走两条路要么双重循环直接枚举所有下标对O(n^2)代码最简单但 n 稍微大一点就会超时要么先想“整除”这个条件能不能改写成更好查的形式这就引出了余数哈希。我打这场比赛时看到nums.length直接到10^5级别立刻放弃暴力。周赛Q1虽然简单但不等于无脑暴力它考的就是你能不能把数学条件转成哈希查询。如果第一反应是“这不就是两重循环”那大概率会在大数据量上吃一个 TLE。1.2 核心思路为什么只看余数就够了要判断(a b) % k 0不需要真的把a b算出来。模运算有一个很直观的性质(a b) % k (a % k b % k) % k。所以两个数的和的整除性只取决于它们各自对k取模后的余数。进一步说如果a的余数是r那么b的余数必须是(k - r) % k才能让两个余数相加后被k整除。注意这里我用的是(k - r) % k而不是k - r因为当r 0的时候需要的余数也是0而k - 0会得到k显然不对。用(k - r) % k可以统一处理余数为0和余数非0两种情况。剩下的事情就很简单了从左到右遍历数组对于当前元素x先查一下“能跟它配对的余数”之前出现过多少次把次数累加到答案然后把当前余数的计数加一。先查再更新是为了保证只统计i j的对不会把自己跟自己配对。1.3 完整代码与执行示例from typing import List from collections import defaultdict class Solution: def countPairs(self, nums: List[int], k: int) - int: cnt defaultdict(int) ans 0 for x in nums: r x % k need (k - r) % k ans cnt.get(need, 0) cnt[r] 1 return ans跑一个例子验证一下。假设nums [1, 2, 3, 4, 5]k 3模 3 后的余数依次是[1, 2, 0, 1, 2]。遍历到2时它的余数是2需要的余数是1之前1出现过一次所以找到配对(1, 2)遍历到最后一个5时余数是2需要的余数是1前面1出现过两次所以找到配对(1, 5)和(4, 5)。最终答案应该是 4 对(1,2), (1,5), (2,4), (4,5)和代码运行结果一致。时间复杂度 O(n)空间复杂度 O(k) 中不同余数的数量。如果k很大比如10^9也只需要哈希表里实际出现过的余数不用担心开数组空间爆炸。1.4 几个容易翻车的边界条件这种题翻车点非常固定。第一k 1的情况。所有数对k1取余都等于 0所以任意两个数相加都能被 1 整除答案应该是n * (n - 1) / 2。代码里的(k - r) % k在这种情况下得到(1 - 0) % 1 0逻辑完全正确不需要特判。但如果你写成k - r这里就直接炸了。第二负数取模。Python 的%会返回非负余数比如-7 % 3在 Python 里是 2而不是 -1所以 Python 代码不需要额外处理。但在 C 和 Java 里-7 % 3的结果是 -1如果你直接拿这个结果去查哈希表所有负数元素都会算错。C 的正确写法是(x % k k) % k。第三答案可能超过 int 范围。n 最大10^5时答案最大接近5 * 10^9所以 C 里要开long long。Python 无所谓但用 C 刷题的人很容易在这上面白给一次 WA。2. Q2复盘区间贪心为什么“最早结束”总是赢2.1 题意与暴力思路Q2是一道区间调度题。题面大致是给你一个二维整数数组intervals其中每个元素是[start, end]表示一个左闭右闭的区间。如果两个区间在数轴上有公共点包括端点重合就算重叠。问最少删除多少个区间能让剩下的区间两两都不重叠。这类题我在周赛里见过太多次。暴力做法很好想枚举删掉哪些区间然后检查剩余区间是否都不重叠复杂度直接爆表。稍微聪明一点会想到动态规划按左端点排序后定义dp[i]表示前 i 个区间中最多能保留多少个不重叠区间转移的时候往前找最后一个右端点小于当前左端点的区间时间复杂度 O(n^2)。但这个数据范围是10^5O(n^2) 必然超时。打这场比赛的时候我看完题大概十秒钟就在草稿纸上写了两个字贪心。因为“删掉最少的区间”和“保留最多的区间”是等价的而“最多不重叠区间数”正是经典的区间调度问题贪心就是标准解法。2.2 贪心策略与正确性直觉贪心策略一句话每次都选当前右端点最小的区间保留然后跳过所有跟它重叠的区间继续选下一个不重叠的、右端点最小的区间。为什么按右端点排序因为一个区间结束得越早它给后面的区间留下的空间就越大。保留一个右端点大的区间很可能会挡掉后面一连串本来都可以保留的区间。举个反例你就明白了。假设区间是[[1, 5], [2, 3], [4, 6]]。如果按左端点排序先看到[1, 5]为了保留它后面两个区间都没法保留了只能保留 1 个区间但如果按右端点排序先选[2, 3]然后[4, 6]跟它不重叠可以继续选最终保留 2 个区间。这就是右端点排序的价值。所以我的做法是把所有区间按右端点从小到大排序然后从头扫一遍维护“当前最后一个被保留区间的右端点”last_end。如果当前区间的左端点小于等于last_end说明它和已保留的最后一个区间重叠这个区间必须删掉否则就保留它并更新last_end为当前区间的右端点。2.3 完整代码两种排序写法按右端点排序的写法最直观from typing import List class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) - int: if not intervals: return 0 intervals.sort(keylambda x: x[1]) ans 0 last_end float(-inf) for l, r in intervals: if l last_end: ans 1 else: last_end r return ans还可以用另一种写法按左端点排序遇到重叠时删除右端点更大的那个区间也就是动态更新当前保留区间的右端点为较小值。这个思路同样正确代码如下from typing import List class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) - int: if not intervals: return 0 intervals.sort() ans 0 last_end intervals[0][1] for i in range(1, len(intervals)): l, r intervals[i] if l last_end: ans 1 last_end min(last_end, r) else: last_end r return ans两种写法的核心思想一致让当前保留区间的右端点尽量小给后面留空间。比赛时我更推荐第一种因为它只需要 sort 一次扫描逻辑也更简单不容易写错。拿一个典型例子走一遍intervals [[1,2],[2,3],[3,4],[1,3]]。按右端点排序后变成[[1,2], [2,3], [1,3], [3,4]]同一右端点顺序无所谓。先保留[1,2]last_end2[2,3]的左端点 2 小于等于 2删掉[1,3]的左端点 1 小于等于 2删掉[3,4]的左端点 3 大于 2保留。最终答案 2删除的是[2,3]和[1,3]剩下的[1,2]和[3,4]恰好不重叠。2.4 闭区间和开区间的魔鬼差异这个点是我最想提醒的。同样的题面如果把区间从“左闭右闭”改成“左闭右开”重叠判定条件会从l last_end变成l last_end。区别在哪左闭右闭区间[1,2]和[2,3]在数轴上有一个公共点 2所以它们算重叠左闭右开区间[1,2)和[2,3)没有公共点因为第一个区间的右端点 2 不包含在区间里第二个区间的左端点 2 包含但它们刚好擦边不算重叠。我见过很多人把模板背得滚瓜烂熟一看区间的题就写if l last_end结果遇到左闭右闭就 WA。这场Q2明确写了左闭右闭所以一定要用。以后读题时看到区间端点带不带中括号必须先确认清楚再决定用哪个不等号。区间类型重叠条件示例左闭右闭[l, r]l last_end[1,2]和[2,3]重叠左闭右开[l, r)l last_end[1,2)和[2,3)不重叠很多题解默认用开区间遇到闭区间题时顺手抄了这就是那种“看起来逻辑没问题但就是过不去”的典型原因。3. 从这场出发拆一下周赛前两题的常见套路3.1 Q1考的是“数学性质哈希”周赛Q1的定位是让大多数人能做出来但又不至于太无脑。最常见的出题模式就是题目给一个看似需要枚举的条件实际上通过一个数学性质把它转成哈希查询。比如“和能被k整除”看余数“差等于k”看频次“两数乘积是完全平方数”看质因数分解后的奇偶性本质上都是同一类思路——把每个元素映射成一个“可比较的键”然后用哈希表统计键的出现次数。我发现很多朋友打Q1时容易陷入“模拟题面”的思维题面说枚举就枚举说遍历就遍历。更好的习惯是在看到数据范围后先问自己一句有没有办法只遍历一次如果这个条件有“配对”的味道那大概率就是哈希。3.2 Q2考的是“排序贪心”周赛Q2最常考的算法基本就是贪心而且贪心之前通常要排序。因为排序可以把复杂的关系变成一种确定的顺序让贪心选择变得可证明。区间类、任务调度类、最少操作类都是这个套路。判断一道题能不能用贪心核心看一步当前局部的最优选择是否一定不会影响后续选择。区间调度就是这样右端点最小的区间选了不会吃亏因为它结束最早。反过来如果每一步的局部最优可能影响全局那贪心就要谨慎了这时候往往得想动态规划。3.3 时间分配和做题顺序的小建议我打周赛有一个习惯前两题给自己卡一个总时间线。读题 2 分钟内完成如果 Q1 超过 10 分钟还没思路先看看是不是理解错了题面Q2 超过 20 分钟没思路果断先跳过做 Q3回头再补。因为周赛排名看的是总得分和时间死磕一道题是最亏的。这场比赛我实际执行的节奏是Q1读题30秒写代码2分钟跑用例1分钟Q2读题1分钟排序贪心4分钟处理边界2分钟。总共不超过15分钟。这不是因为我反应快而是这些套路见得太多了。4. 本场碰到的边界坑赛后统一整理4.1 C/Java里的负数取模Q1对大部分Python玩家来说没什么坑但我用C打周赛的朋友有人直接 WA 了。原因就是 C 里负数取模结果还是负数导致哈希表里存了一个错误的余数。C 版核心要写成class Solution { public: long long countPairs(vectorint nums, int k) { unordered_mapint, int cnt; long long ans 0; for (int x : nums) { int r (x % k k) % k; int need (k - r) % k; if (cnt.count(need)) { ans cnt[need]; } cnt[r]; } return ans; } };以后只要看到 C 题解里出现x % k直接套哈希先检查负数。这个坑非常隐蔽因为示例用例通常全是正数本地跑得飞起一交就错。4.2 答案可能爆intQ1答案的理论上限是n * (n - 1) / 2。当n 10^5时答案大约 50 亿远超 32 位 int 的范围。C 里如果ans用int哪怕算法全对最终结果也会溢出变成负数。这是周赛很常见的“隐藏陷阱”题目本身不难但用了大数组就要求你考虑更大类型。建议周赛代码里所有计数型变量只要涉及平方量级一律用long long省得每次都要重新估算范围。4.3 空数组和单元素数组Q2如果intervals为空答案直接是 0。很多模板里用intervals[0]做初始化结果空数组直接越界。更稳的写法是先把空数组判断掉或者像我第一种代码那样last_end初始化为float(-inf)这样空数组也能走完循环返回 0。单元素区间也是一样不管区间本身是什么不需要删除任何区间答案 0。用第一种写法自然得到 0不需要特判。4.4 比赛里不要轻信“暴力能过”我看到有评论说 Q2 自己写了双重循环 DP自信能过结果10^5数据直接 TLE。周赛是个很现实的场景正确但复杂度不够的代码和没写出来没有本质区别。建议每次提交前先看数据范围n 1000可以接受 O(n^2)n 10^5至少要往 O(n log n) 想n 10^6基本只能 O(n) 或 O(n log n) 且常数要小。周赛Q1Q2的常见数据范围基本都在10^5上下所以“暴力一眼过”的机会越来越少。5. 复盘后的三个变体拿来练手刚好5.1 把Q1改成乘积模kQ1如果从“和能被k整除”改成“乘积能被k整除”余数哈希就不能直接用了。因为(a % k) * (b % k) % k 0并不简单等价于某个余数配对。比如k 6数2和3的余数分别是 2 和 32 * 3 6刚好整除但两个数都不是 6 的倍数。更通用的做法是取每个数和k的最大公约数然后统计gcd(x, k)的频次再找两个数使得gcd(x, k) * gcd(y, k)是k的倍数。这个变体比原题难一个档次适合拿来验证自己是不是真的理解透了模运算的本质。5.2 给区间加上权重Q2如果每个区间都有一个价值要求删除区间的总价值最小贪心就不再成立了。因为一个价值很大的区间可能和很多低价值区间重叠贪心很容易把它误删。这种加权版本的正确做法是按右端点排序后做动态规划dp[i]表示前 i 个区间能获得的最大总价值转移时找到最后一个和当前区间不重叠的区间。复杂度可以做到 O(n log n)但已经不是简单贪心能解决的。5.3 同类调度题可以串起来区间调度是周赛的常青树我建议把下面这几类题放一起刷一遍。题目形态常用方法思考角度最多不重叠区间数按右端点排序贪心结束得越早越优最少删除区间使不重叠总区间数减最多不重叠数把删除问题转成保留问题最少会议室数量差分数组或最小堆看成同一时刻并行区间数区间合并按左端点排序后扫描维护当前合并范围的右端点这几种题表面长得像解法完全不同但底层的区间思维是贯通的。把这组题刷明白以后遇到区间类题目基本不会慌。从第495场这场来看Q1和Q2并不算难但它们把周赛最核心的两个套路又考了一遍哈希配对找数学性质贪心排序做区间取舍。如果你也想稳定拿下前两题可以试着把这类题各刷五道然后在赛后第一时间复盘自己卡在哪一步是思路没打开还是边界没想全。刷多了你会发现所谓“手速快”其实只是见过足够多套路之后的自然反应。