ARTICLE DETAIL

资讯详情

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

LeetCode周赛无伤AK实战:从读题到代码的稳定性提升策略

LeetCode周赛无伤AK实战:从读题到代码的稳定性提升策略 上周六的 LeetCode 第 512 场周赛我侥幸拿到了国服 22 名并且难得地实现了“无伤 AK”即四道题全部一次提交通过没有罚时。这个成绩本身不算顶尖但“无伤”的过程却让我这个老选手感触颇深——不是感慨自己变强了而是清晰地感觉到解题的“战场”正在悄然改变。过去周赛的挑战主要在于“算法思维”和“编码实现”。但现在随着题目描述越来越长、场景越来越生活化、边界条件越来越隐蔽“阅读理解”和“细节把控”正成为决定胜负的关键甚至比想出一个巧妙的解法更重要。这次周赛的四道题几乎每一道都在考验选手的耐心和细心。一个词看错一个边界没想清就可能从“无伤”变成“罚时坐牢”。如果你也经常在周赛中因为看错题、漏条件而痛失好局或者感觉题目越来越“绕”那么这篇文章或许能给你一些不一样的视角。我不打算简单罗列题解代码而是想结合这次“无伤 AK”的实战经历和你深入聊聊面对越来越“狡猾”的 LeetCode 周赛我们该如何调整备赛策略从“读题”开始就建立优势并稳定地将思路转化为无 Bug 的代码。本文将包含完整的四道题目的思路分析、关键陷阱、代码实现以及赛后复盘。更重要的是我会分享一套我自己在用的、用于对抗“老年痴呆”和“读题吃力”的实战检查清单。这套方法能帮助你在紧张的比赛时间内最大限度地减少非算法性失误。1. 周赛趋势洞察为什么“无伤”越来越难在深入具体题目之前我们有必要先理解当前周赛出题的一个明显趋势题目正从“纯算法模板题”向“综合应用题”演变。这带来的直接影响是信息密度增加题目描述中夹杂了更多的背景故事、场景说明核心的算法约束条件可能分散在多个段落中。陷阱设计更隐蔽边界条件如数组为空、结果为0、整数溢出不再是摆在明面上而是需要你从场景中自行推导。实现细节要求更高即使算法思路正确如果在实现时对数据结构的API不熟、或者循环边界写错也会导致失败。以本次周赛为例没有用到特别高深的数据结构或算法最高到二分查找和前缀和但每一题都有“坑”。比赛的竞争在很大程度上变成了“谁更细心”的竞争。因此我们的备赛重心也应该从“狂刷难题”向“提升稳定性和熟练度”倾斜。2. 题目一找出输掉零场或一场比赛的玩家题目链接通常为第一题编号如 3042 之类具体以平台为准。题目大意给定一个整数n和一个二维整数数组matches其中matches[i] [winneri, loseri]表示一场比赛。需要返回一个长度为 2 的列表第一个列表是所有从未输过的玩家第二个列表是只输过一场的玩家。两个列表都需要按递增顺序排序。2.1 思路分析与关键陷阱这是一道典型的计数模拟题考察哈希表字典的基本使用。核心思路我们需要统计每个玩家的输场次数。因为题目只关心输场所以赢家如果没输过也会出现在结果中。我们只需关注loser。遍历matches数组用哈希表lose_count记录每个玩家的输场次数。同时为了找出“从未输过”的玩家我们需要知道所有出现过无论是赢是输的玩家。可以用一个集合all_players记录所有在matches中出现的玩家。最后遍历all_players如果该玩家不在lose_count中或其输场次数为 0则加入“从未输过”列表。如果该玩家在lose_count中的次数恰好为 1则加入“只输一场”列表。对两个列表进行排序后返回。关键陷阱与易错点玩家编号范围玩家编号是从1到n吗题目描述是1 winneri, loseri 10^5但n参数可能表示玩家总数或别的含义。仔细读题发现n在此题中可能没有直接用于限制玩家编号范围所有玩家信息应从matches中获取。这是一个常见的迷惑点不要默认玩家编号就是1..n。排序要求结果列表必须递增排序。这是一个非常常见的要求但紧张时容易忘记。从未输过的定义一个玩家只要在matches中出现过即使是作为赢家且没有输过就算“从未输过”。所以我们需要all_players集合。2.2 代码实现与注释from typing import List from collections import defaultdict class Solution: def findWinners(self, matches: List[List[int]]) - List[List[int]]: # 使用 defaultdict(int) 可以方便地计数默认值为0 lose_count defaultdict(int) # 使用集合记录所有出现过的玩家 all_players set() for winner, loser in matches: lose_count[loser] 1 all_players.add(winner) all_players.add(loser) never_lost [] lost_one [] for player in sorted(all_players): # 提前排序可以保证结果有序但分开排序更清晰 if lose_count[player] 0: never_lost.append(player) elif lose_count[player] 1: lost_one.append(player) # 按题目要求返回两个列表 return [never_lost, lost_one]代码要点使用defaultdict(int)简化计数逻辑。遍历时同时维护all_players集合。最后遍历已排序的all_players一次性构建两个结果列表。也可以先构建再分别排序。时间复杂度 O(N log N)主要来自排序其中 N 为不同玩家的数量。3. 题目二求出加密整数的和题目链接通常为第二题。题目大意定义一种加密操作对于一个整数x将其每个数字d替换为(d k) % 10其中k是一个密钥整数。现在给你一个整数数组nums和一个整数k要求返回数组中所有元素加密后的和。3.1 思路分析与关键陷阱这是一道简单的模拟题考察数字的逐位处理。核心思路定义一个函数encrypt(x, k)用于计算单个整数x加密后的结果。在函数内部可以将x转换为字符串然后遍历每个字符数字将其转换为整数加上k后对 10 取模再转换回字符最后拼接成新的字符串再转换回整数。或者通过数学运算逐位取出x的每一位数字进行处理。遍历nums数组对每个元素调用encrypt函数累加结果。关键陷阱与易错点负数处理题目中nums[i]和最终结果是否可能为负数仔细读题通常输入是非负整数但也要确认。本题通常规定为非负整数。前导零加密后数字的高位可能变成 0例如x123, k7加密后是890这没问题。但如果x100, k9加密后是?需要逐位计算(19)%100, (09)%109, (09)%109结果是099转换为整数是99。这里的关键是加密操作是逐位独立进行的不存在“前导零被忽略”的问题因为我们是先得到数字序列再组合成整数。用字符串处理可以自然地保留每一位。大数溢出Python 整数无溢出问题但如果是其他语言如 Java需要注意累加和可能超出 32 位整数范围应使用长整型。k可能很大(d k) % 10k可能远大于 10直接加k再取模是正确的。3.2 代码实现与注释from typing import List class Solution: def sumOfEncryptedInt(self, nums: List[int], k: int) - int: def encrypt(x: int) - int: # 将数字转换为字符串以便逐位处理 s str(x) encrypted_chars [] for ch in s: # 将字符转换为数字加密再转回字符 new_digit (int(ch) k) % 10 encrypted_chars.append(str(new_digit)) # 将加密后的字符列表拼接并转换回整数 return int(.join(encrypted_chars)) total 0 for num in nums: total encrypt(num) return total代码要点内部函数encrypt封装了加密逻辑使主逻辑清晰。使用字符串处理可以避免复杂的数学取位操作代码更易读。时间复杂度 O(N * L)其中 N 是数组长度L 是数字的平均位数。4. 题目三求出所有子序列的能量和题目链接通常为第三题难度提升。题目大意给定一个整数数组nums和一个整数k。定义数组的能量为如果数组所有元素的和能被k整除则能量为sum(nums)否则为 0。现在要求nums的所有子序列的能量之和。由于答案可能很大需要取模10^9 7。注意子序列不要求连续但顺序需要保持原数组中的顺序。空子序列的和为 0其能量也为 0因为 0 能被任何 k 整除但 sum0所以能量是 0。4.1 思路分析与关键陷阱这是一道动态规划DP结合模运算的题目是本周赛的核心难点。核心思路暴力枚举所有子序列共2^n个不可行n最大可能为10^5。关键转化我们并不关心子序列具体是什么只关心它的和模 k 的余数以及它的和。定义dp[i][r]表示考虑前i个元素时能够组成和模 k 余数为 r的所有子序列的原始和的总和注意是原始和不是模后的。状态转移对于第i个元素0-indexed值为num不选第i个元素dp[i1][r] dp[i][r]选第i个元素新的子序列和 旧子序列和 num。设旧余数为r新余数new_r (r num) % k。那么dp[i1][new_r] dp[i][r] (子序列个数) * num。这里(子序列个数) * num是因为每个包含当前num的子序列其和都增加了num。为了计算“子序列个数”我们可以同时维护另一个数组cnt[i][r]表示考虑前i个元素时和模 k 余数为 r 的子序列的个数。最终所有dp[n][0]的和即余数为 0 的子序列的原始和总和就是答案因为只有这些子序列的能量等于其和。初始状态dp[0][0] 0,cnt[0][0] 1空子序列。关键陷阱与易错点模运算所有加法、乘法操作都需要对10^97取模。状态定义dp存储的是“原始和的总和”而不是“模 k 后的和的总和”。这是因为能量等于原始和我们需要累加的是原始和。转移方程选当前元素时新增的和不仅仅是dp[i][r]还要加上cnt[i][r] * num。这是本题动态规划的核心难点。空间优化由于dp[i1]只依赖于dp[i]可以使用滚动数组将空间复杂度从 O(n*k) 优化到 O(k)。空子序列空子序列的和为0余数也为0它应该被计入cnt[0][0]1但其能量为0所以不影响最终结果因为dp[0][0]0。4.2 代码实现与注释from typing import List class Solution: def sumOfPowers(self, nums: List[int], k: int) - int: MOD 10**9 7 n len(nums) # dp[r]: 当前考虑下余数为r的所有子序列的原始和的总和 dp [0] * k # cnt[r]: 当前考虑下余数为r的子序列的个数 cnt [0] * k # 初始化空子序列和为0余数为0个数为1 dp[0] 0 cnt[0] 1 for num in nums: # 需要基于上一轮的状态进行转移所以先复制 new_dp dp[:] new_cnt cnt[:] for r in range(k): if cnt[r] 0: continue # 没有这个余数的子序列跳过 # 选择当前元素 num new_r (r num) % k # 子序列个数增加 cnt[r] new_cnt[new_r] (new_cnt[new_r] cnt[r]) % MOD # 原始和总和增加原来的和(dp[r]) 每个子序列都加num (cnt[r] * num) new_dp[new_r] (new_dp[new_r] dp[r] cnt[r] * num) % MOD dp, cnt new_dp, new_cnt # 最终余数为0的子序列的原始和总和即为答案 return dp[0] % MOD代码要点使用滚动数组dp和cnt空间复杂度 O(k)。内层循环遍历所有可能的余数r但通过if cnt[r] 0进行剪枝。转移时先更新new_cnt再更新new_dp因为new_dp的计算用到了cnt[r]上一轮的。所有加法和乘法操作后都立即取模。时间复杂度 O(n * k)在本题约束下n, k 通常不超过一定范围可以接受。5. 题目四找出有效子序列的最大长度题目链接通常为第四题压轴题。题目大意给定一个字符串s和一个字符串t。需要从s中找出一个最长的子序列使得这个子序列中不包含t作为子序列。返回这个最大长度。注意子序列定义同上。t作为子序列意味着在s的子序列中能按顺序找到t的所有字符。5.1 思路分析与关键陷阱这是一道字符串子序列匹配的变种题可以转化为动态规划或贪心结合二分查找最长递增子序列 LIS 思想。核心思路最暴力的想法是枚举s的所有子序列检查是否包含t复杂度无法接受。逆向思维我们要求的是不包含t作为子序列的最长子序列。那么如果一个子序列包含了t作为子序列它就不是有效的。如何判断一个子序列是否包含t这等价于在子序列中按顺序匹配t的每一个字符。如果匹配完了t的所有字符就说明包含了。因此我们可以考虑在s中尽可能少地匹配t的字符。我们希望找到一个最长的子序列使得在这个子序列中无法按顺序匹配出完整的t。这可以转化为在s中选一个子序列使得我们最多只能匹配到t的前m-1个字符假设t长度为m。因为一旦匹配到第m个字符就包含了t。一种高效的方法是预处理s中每个位置之后每个字母下一次出现的位置Next Position Array。然后进行动态规划。更巧妙的贪心二分思路我们维护一个数组dpdp[len]表示当匹配了t的前len个字符时在s的子序列中所需的最短前缀长度即在s中至少需要多长的前缀才能按顺序找到t的前len个字符作为子序列。这个dp数组是单调递增的。遍历s的每个字符c我们尝试更新dp。对于t中所有等于c的位置j如果我们已经匹配了前j-1个字符即dp[j-1]有定义那么我们可以用当前的位置i去更新dp[j]使其更小因为我们在更早的位置就匹配到了前j个字符。实际上这就是在维护一个“最小匹配位置”的数组。最终我们找到最大的len使得dp[len]有定义即len m那么这个len就是我们能匹配的t的最大前缀长度。而我们能选的最长子序列长度就是n因为我们可以通过跳过一些字符来避免匹配到第m个字符不我们需要更精确的计算。更直接的正向DP思路定义f[i][j]表示考虑s的前i个字符当前匹配到t的第j个字符时即已经匹配了t的前j个字符所能选出的最长有效子序列长度。状态转移不选s[i]f[i1][j] max(f[i1][j], f[i][j])选s[i]如果s[i] t[j]那么匹配状态可以推进到j1。但是如果j1 m即匹配完了t那么这个选择就是非法的因为子序列包含了t所以不能转移。如果s[i] ! t[j]那么匹配状态不变仍然是j。答案就是max(f[n][j])其中0 j m即最终没有完全匹配t。初始状态f[0][0] 0。这个 DP 是 O(n * m) 的如果m很大比如t很长可能会超时。但本题中t的长度可能有一定限制。关键陷阱与易错点子序列匹配的定义必须按顺序匹配但不要求连续。“不包含”的含义只要子序列中能按顺序提取出t的所有字符就算包含。即使子序列更长、中间插入了其他字符也算包含。空子序列空子序列是有效的它不包含任何字符串。算法选择需要根据s和t的长度范围选择合适的方法。如果m较小O(n*m) 的 DP 可行如果m较大可能需要更优的贪心方法。初始化与边界DP 的初始状态和非法状态处理要小心。5.2 代码实现与注释采用 O(n*m) DP 方法假设 m 不大class Solution: def maxSubsequenceLength(self, s: str, t: str) - int: n, m len(s), len(t) # f[i][j]: 考虑 s 的前 i 个字符当前匹配到 t 的第 j 个字符时的最长有效子序列长度 # 初始化为 -inf表示不可达状态 NEG_INF -10**9 f [[NEG_INF] * (m 1) for _ in range(n 1)] f[0][0] 0 # 空串匹配0个字符长度为0 for i in range(n): for j in range(m 1): if f[i][j] NEG_INF: continue # 1. 不选 s[i] f[i 1][j] max(f[i 1][j], f[i][j]) # 2. 选 s[i] if j m and s[i] t[j]: # 可以匹配 t 的第 j 个字符 if j 1 m: # 匹配完了 t非法不能选这个字符 pass else: f[i 1][j 1] max(f[i 1][j 1], f[i][j] 1) else: # 不匹配状态 j 不变 f[i 1][j] max(f[i 1][j], f[i][j] 1) # 答案最终匹配状态 j 不能等于 m即不能完全匹配取最大值 ans 0 for j in range(m): ans max(ans, f[n][j]) return ans代码要点f[i][j]中j表示已经匹配了t的前j个字符从0开始计数。jm表示完全匹配是非法状态。初始化所有状态为负无穷NEG_INF表示不可达。只有f[0][0]0是起点。状态转移时不选字符很简单。选字符时分为两种情况当前字符能推进匹配状态且推进后不能达到m或不能推进匹配状态。最终遍历j0..m-1取最大值。时间复杂度 O(nm)空间复杂度 O(nm) 可通过滚动数组优化为 O(m)。6. 无伤 AK 的实战检查清单通过以上四道题的详细分析你可以看到除了算法本身对题意的精确理解和细节的严密把控至关重要。下面是我在比赛中会默默过一遍的检查清单它帮助我减少了大量低级错误6.1 读题阶段 (3-5分钟)[ ]明确输入输出数据类型整数、字符串、数组、范围上下界、是否可能为负、是否可能为空。[ ]理解关键术语确认“子序列”、“子数组”、“能量”、“加密”等操作的精确定义。自己用1-2个小例子验证理解。[ ]识别边界条件数据范围极大/极小时如 n0, k0, 数组为空应该输出什么题目是否说明[ ]找出所有约束将题目中的“必须”、“不能”、“至少”、“至多”等条件用笔标记出来。6.2 思路设计阶段 (2-3分钟)[ ]暴力法是否可行先想最直接的解法评估复杂度这有助于理解问题本质。[ ]核心转化能否将问题转化为已知模型如DP、贪心、二分、图论转化过程是否有信息丢失[ ]状态定义如果用DP状态表示什么转移方程是否覆盖所有情况初始状态和最终答案如何对应[ ]复杂度估算在给定数据范围下你的算法能否在1-2秒内运行如果接近极限是否有常数优化空间6.3 编码实现阶段 (5-15分钟)[ ]变量命名使用有意义的变量名如dp,cnt,lose_count避免a,b,c。[ ]模块化将复杂逻辑封装成函数如encrypt提高可读性和可调试性。[ ]循环边界for i in range(n)还是range(1, n1)while l r还是用具体例子验证。[ ]初始化DP数组、累加和、最大值/最小值变量的初始值是否正确特别是-inf或0的选择。[ ]取模操作如果需要取模是否在所有加、乘、减注意负数操作后都正确取模模数MOD是否正确定义[ ]数据类型在Python中通常没问题但心里要清楚是否会溢出在其他语言中尤为重要。6.4 测试与提交前 (1-2分钟)[ ]自测样例至少用题目给的样例跑一遍确保输出完全一致。[ ]边缘测试在脑中或草稿上快速过一遍空输入、单个元素、最大值、最小值、全部相同元素等特殊情况。[ ]代码复审快速扫一遍代码重点看条件判断中的是否写成了循环后是否误用了循环变量数组索引是否可能越界返回值类型和格式是否正确如返回列表的列表[ ]心态检查如果这是一道“简单题”但你的解法看起来很复杂很可能想复杂了。暂停一下重新读题。7. 常见错误类型与排查指南即使有了检查清单比赛中仍可能出错。下表总结了周赛中高频的错误类型和第一时间排查方向错误类型典型表现可能原因排查步骤Wrong Answer样例通过提交失败1. 边界条件未处理。2. 题意理解偏差。3. 算法逻辑漏洞。1. 构造极小、极大、全零、反序等特殊用例测试。2. 重新逐句读题确认关键词。3. 用纸笔模拟算法流程寻找反例。Time Limit Exceeded运行超时1. 算法复杂度太高。2. 存在死循环。3. 输入读取效率低Python中少见。1. 分析代码最内层循环估算操作次数。2. 检查while循环终止条件。3. 考虑是否存在O(n²)嵌套循环能否优化。Runtime Error运行时崩溃1. 数组/字符串索引越界。2. 除以零。3. 递归过深。4. 空指针/未初始化访问。1. 检查所有索引特别是i-1,i1,n-1在边界处的行为。2. 检查作为除数的变量是否可能为0。3. 将递归改为迭代或增加递归深度限制Python可设置。Memory Limit Exceeded内存超限1. 使用了过大的数据结构如超大二维数组。2. 缓存了不必要的数据。1. 计算所需内存是否远超限制如n10^5时开int[n][n]。2. 尝试使用滚动数组、惰性计算、迭代代替递归。当遇到错误时不要慌张。按照上表定位问题类型然后结合第6节的检查清单从最可能的原因开始排查。通常重新仔细读题和构造极端测试用例能解决大部分 Wrong Answer 问题。8. 备赛策略与能力提升建议基于本次“无伤 AK”的经验和当前的周赛趋势我建议调整备赛策略在以下方面投入更多精力强化阅读理解训练每周至少精做2-3道题干较长的题目尤其是带场景描述的。练习时先不写代码而是用一句话概括问题并列出所有输入输出约束和边界条件。建立错误案例本将每次周赛或练习中因粗心、读错题导致的错误记录下来并注明错误原因和正确的理解。定期回顾形成条件反射。专题突破代替盲目刷题如果某类题如DP、贪心、字符串经常出错进行为期一周的专题训练。重点理解这类问题的通用思考框架而不是背模板。模拟赛环境练习定期在固定时间内如1.5小时完成一次虚拟竞赛。使用与正式比赛相同的环境如关闭自动补全训练时间管理和压力下的编码稳定性。复盘大于刷题做完一道题尤其是做错的题花比解题更多的时间去复盘。思考最优解是什么我为什么没想到有哪些陷阱下次如何避免LeetCode 周赛不仅是算法能力的比拼更是工程素养细心、严谨、稳健的试金石。随着题目设计越来越注重综合能力“无伤”完成比赛的价值甚至可能超过解出难题。希望这篇结合具体赛题和实战心得的文章能帮助你构建起更强大的竞赛防御体系。下次周赛期待你也能稳定发挥拿下属于自己的“无伤 AK”。
返回列表