从最长递增子序列变种题看Blind Mock Interview如何突破刷题思维

发布时间:2026/8/21 11:50:58
从最长递增子序列变种题看Blind Mock Interview如何突破刷题思维 上周一个刚拿到面试机会的朋友深夜发来消息说他卡在了一道“最长递增子序列”的变种题上。他刷了LeetCode上这道题的题解背了动态规划的模板但面试官稍微改了下条件问“如果允许最多删除一个元素最长递增子序列能有多长”他当场就懵了。他问我“为什么刷了那么多题感觉都会了一遇到新题还是没思路”这个问题太典型了。很多人把LeetCode刷题等同于“背答案”把周赛排名当作“段位”却忽略了面试官真正想考察的底层能力问题拆解、模式识别和代码实现。这恰恰是“Blind Mock Interview”这种形式试图训练的核心。当项目标题里出现“Round2 开始敲代码”时它暗示的已经不是一个简单的练习而是一个从“看懂”到“能写”再到“能在压力下清晰表达”的实战模拟阶段。很多人对Mock Interview模拟面试有误解以为就是找个朋友互相问几道题。真正的Blind Mock Interview尤其是进行到“Round2”这个阶段模拟的是面试中最真实也最残酷的部分面试官给你一个你没见过的问题描述Blind你需要当场Mock在白板或共享编辑器里从理解问题、沟通思路、到写出可运行代码开始敲代码的全过程。这和你自己默默刷题、随时可以看答案的体验完全是两回事。今天我们就以“LeetCode Blind Mock Interview | Round2 开始敲代码”这个场景为锚点深入聊聊如何超越“刷题家”思维真正为技术面试做好准备。我会结合“最长递增子序列”这类经典问题的变种拆解从拿到题目到交出代码的完整思考框架。1. “Blind”的真正含义面试不是开卷考试很多人害怕“Blind”盲面是因为习惯了LeetCode的“开卷”环境——你可以搜索、可以看讨论区、可以调试无数次。但面试是“闭卷”的。这种压力测试考察的远不止算法知识。1.1 从“识别原题”到“定义新问题”当你自己刷题时大脑的工作模式是“哦这是‘最长递增子序列’用动态规划状态定义是dp[i]表示以i结尾的LIS长度状态转移是……” 这是一种模式匹配。你匹配的是题目描述和你记忆中的标签。但在Blind Mock Interview中面试官可能会刻意避免使用LeetCode上原封不动的描述。比如他可能不会说“求最长递增子序列”而是说“给定一个整数数组请找出一个最长的子序列使得这个子序列是严格递增的。哦对了我们允许你从原数组中删除至多一个元素这个子序列可以不连续。” 这时你的第一反应不应该是去回忆“LIS模板”而是重新定义问题。关键动作用自己的话复述并确认问题。“好的我理解一下。我们有一个数组nums。目标是找到一个最长的、严格递增的子序列。这个子序列可以从原序列中删除0个或1个元素后得到。子序列不要求连续但必须保持原数组的相对顺序。我这样理解对吗”这个动作至关重要确保理解一致避免你花了20分钟解决了一个错误的问题。争取思考时间在复述的过程中你的大脑已经在开始组织思路。展示沟通能力面试官能看到你协作和澄清需求的能力。1.2 跳出“模板陷阱”为什么背的答案不管用以“最长递增子序列LIS”为例。标准的动态规划解法时间复杂度是O(n²)二分查找优化解法是O(n log n)。这些是基础知识必须掌握。但面试官如果加上“允许删除一个元素”这个条件这道题的性质就变了。它不再是经典的LIS问题而是一个基于LIS思想的衍生问题。你的思考路径应该是暴力枚举不可行但有助于思考最直接的想法是尝试删除数组中的每一个位置i然后在剩下的n-1个元素的数组上求LIS。这需要O(n)次LIS计算总复杂度O(n² * log n)或更高显然不是面试官想要的。寻找子问题既然允许删除一个元素那么最终的最优子序列要么没删除任何元素标准LIS要么删除了一个元素。如果删除了元素k那么最优子序列实际上是由k左边的某个递增子序列和k右边的某个递增子序列拼接而成且拼接处满足递增关系。转化为已知问题这引导我们想到可以预处理两个数组prefix[i]以nums[i]结尾的、在子数组nums[0...i]中的LIS长度。suffix[j]以nums[j]开头的、在子数组nums[j...n-1]中的LIS长度。 那么如果我们考虑删除元素k那么通过k可能形成的最长子序列长度就是max(prefix[i] suffix[j])其中i k j且nums[i] nums[j]。如果不删除任何元素答案就是max(prefix[i])。你看这个思考过程没有一步是直接套用“LIS模板”能得到的。它需要你理解LIS的核心定义状态和转移。将新约束删除一个元素转化为对原有模型的扩展。通过预处理将问题分解避免重复计算。这就是“Blind”面试要考察的在无法直接匹配记忆模板时你拆解和重构问题的能力。2. “Mock”的核心价值演练的是表达而不仅是解题模拟面试Mock最大的好处是给你一个安全的环境去犯“过程性错误”而不是“结果性错误”。结果错了代码有bug有时可以接受但过程错了思路混乱、沟通不畅几乎一定会导致失败。2.1 从“闷头写”到“边说边写”在Round2中“开始敲代码”不是一个孤立的动作。它应该嵌入在一个持续的对话中。一个糟糕的流程听完题目沉默2分钟。突然说“我想到了用动态规划。”开始埋头写代码写了15分钟。说“写完了。”面试官问“能解释一下你的思路吗” 你开始对着代码倒推。一个更好的流程澄清问题如前所述。提出初步思路“我首先想到的是暴力方法枚举删除每个元素但复杂度太高。我们需要更优的方法。这个问题允许删除一个元素这让我想到可以将问题分解为‘删除点’左侧和右侧两部分。我们可以尝试预处理每个位置作为结尾和开头的LIS长度然后尝试组合。”讨论复杂度与可行性“预处理需要O(n²)或O(n log n)时间组合需要O(n²)时间。空间复杂度是O(n)。这个复杂度在n1000时是可以接受的。您觉得这个方向可以吗”获得反馈后开始实现“好的那我先来实现预处理的函数。我会先写一个标准的、O(n²)的LIS动态规划函数来计算prefix数组这样逻辑更清晰。如果需要优化我们稍后再讨论二分查找版本。”边写边解释“这里我初始化一个prefix数组长度是n全部为1。因为每个元素本身就是一个长度为1的递增子序列……现在我开始双层循环如果nums[j] nums[i]我就可以更新prefix[i] max(prefix[i], prefix[j] 1)。”处理边界和核心逻辑“现在来计算suffix数组。思路类似但从右往左遍历……最后组合部分如果不删除答案是max(prefix)如果删除位置k我们需要找到所有满足i k j且nums[i] nums[j]的(i, j)对用prefix[i] suffix[j]更新答案。这里可以用一个二重循环来实现。”这个流程中面试官始终知道你在想什么你也在持续验证自己的想法是否正确。即使最后代码有小瑕疵面试官也看到了你清晰的、结构化的思维过程。2.2 处理“卡住”的瞬间在Mock中你一定会遇到卡住的时刻。这时如何应对比立刻想出答案更重要。不要长时间沉默如果思考超过30秒没有任何进展主动说出来。“我目前的想法遇到了瓶颈我正在考虑是否可以通过换个状态定义来解决……”回溯到简单情况“让我们先不考虑删除元素就求普通LIS。现在加入删除一个元素我们看看最简单的数组[1,2,3,4,5]和[5,4,3,2,1]会有什么变化。”请求提示在真实面试中适时请求提示是明智的。“我目前的想法是预处理前后缀但在组合时似乎需要O(n³)的循环。您觉得有没有可能通过某种数据结构比如有序集合来优化这个查找过程” 这表明你意识到了问题所在并正在寻求更优解。Mock Interview就是用来练习这些“软技能”的。把这些互动变成你的肌肉记忆。3. “开始敲代码”从思路到可靠实现的跨越很多人的算法思路是“空中楼阁”一到实现就漏洞百出。“开始敲代码”这个阶段是区分“想明白了”和“能做出来”的关键。3.1 实现不是翻译是设计决策的延续继续以我们修改后的LIS问题为例。即使思路清晰了实现时仍有大量细节需要考虑决策1预处理函数的选择写一个calculate_lis_lengths(nums)函数返回一个数组dp其中dp[i]是以nums[i]结尾的LIS长度。是用O(n²)的DP还是O(n log n)的二分Mock面试建议先实现逻辑清晰的O(n²)版本。向面试官说明“为了保证代码可读性和正确性我先用O(n²)的DP实现预处理。如果数组长度很大我们可以再优化为二分查找版本。” 这展示了你的权衡能力——正确性优先于过早优化。决策2后缀数组的计算后缀数组suffix[i]表示以nums[i]开头的LIS长度。最直观的方法是反转数组然后对反转后的数组计算“以结尾的LIS”然后再反转回来。但直接在原数组上从右向左做DP也是可以的。实现细节从右向左遍历suffix[i]初始为1。内层循环j从i1到n-1如果nums[i] nums[j]则更新suffix[i] max(suffix[i], suffix[j] 1)。注意这里是suffix[j] 1因为j在i的右边以j开头的序列可以接在i后面。决策3组合逻辑的实现这是最易错的部分。我们需要考虑删除点k。如果不删除ans max(prefix)。如果删除k我们需要找到所有满足i k j且nums[i] nums[j]的(i, j)对。最直接的是三重循环遍历k遍历i遍历j复杂度O(n³)。优化对于固定的删除点k我们可以遍历所有i (0 i k)对于每个i我们希望找到所有j (k j n)且nums[j] nums[i]取最大的suffix[j]。这可以通过预处理来实现对于每个k我们可以预先计算一个right_max[k]数组其中right_max[k][val]表示在k右侧元素值大于val的所有位置中最大的suffix值。但这需要离散化和复杂的数据结构。面试中的务实选择向面试官说明“最直接的组合方法是O(n³)的循环这对于n1000可能勉强能过10^9运算但不够好。一个优化思路是对于每个删除点k我们维护一个右侧元素值到最大suffix的映射在遍历i时快速查询。这可以将复杂度降到O(n² log n)。由于时间关系我先实现O(n³)的版本确保逻辑正确然后我们可以讨论优化方案。” 这展示了你的分析能力并且没有在实现上卡死。3.2 代码结构、命名与测试即使是在白板上写伪代码良好的习惯也能加分。def longest_increasing_subsequence_with_one_deletion(nums): 允许删除至多一个元素后的最长递增子序列长度。 n len(nums) if n 1: return n # 1. 预处理prefix[i] 以nums[i]结尾的LIS长度 prefix [1] * n for i in range(n): for j in range(i): if nums[j] nums[i]: prefix[i] max(prefix[i], prefix[j] 1) # 2. 预处理suffix[i] 以nums[i]开头的LIS长度 suffix [1] * n for i in range(n-1, -1, -1): for j in range(i1, n): if nums[i] nums[j]: suffix[i] max(suffix[i], suffix[j] 1) # 3. 计算答案 ans max(prefix) # 不删除任何元素的情况 # 尝试删除每个位置k for k in range(n): # 寻找i k j 且 nums[i] nums[j] 的 (i, j) 对 for i in range(k): for j in range(k1, n): if nums[i] nums[j]: ans max(ans, prefix[i] suffix[j]) return ans注意点函数命名和注释清晰地说明了函数功能。边界处理考虑了数组长度小于等于1的情况。变量命名prefix,suffix,ans含义清晰。逻辑分离将预处理和组合逻辑分开结构清晰。写完代码后一定要用简单例子测试print(longest_increasing_subsequence_with_one_deletion([1,2,3,4,5])) # 应返回5删除0个 print(longest_increasing_subsequence_with_one_deletion([5,4,3,2,1])) # 应返回1删除1个后还是1 print(longest_increasing_subsequence_with_one_deletion([1,3,2,4])) # 应返回4 删除2得到[1,3,4] # 更复杂的例子[10, 9, 2, 5, 3, 7, 101, 18] # 标准LIS是[2,5,7,101]或[2,5,7,18]长度为4。 # 如果删除5得到[10,9,2,3,7,101,18]LIS可以是[2,3,7,101]或[2,3,7,18]长度还是4。 # 如果删除3得到[10,9,2,5,7,101,18]LIS是[2,5,7,101]或[2,5,7,18]长度还是4。 # 似乎没有提升。需要设计一个能体现删除优势的例子[1, 100, 2, 3, 4] # 标准LIS是[1,2,3,4]或[1,100]长度为4或2不对[1,100]是递增但[1,2,3,4]更长长度是4。 # 删除100后数组为[1,2,3,4]LIS长度就是4。和原来一样。 # 一个更好的例子[5, 1, 2, 3, 4] # 标准LIS是[1,2,3,4]长度为4。 # 删除5后数组为[1,2,3,4]LIS长度还是4。没有变化。 # 真正需要删除的例子是[3, 1, 2, 4, 5] # 标准LIS是[1,2,4,5]或[1,2,3,4,5]? 不对3在1前面所以不能同时有3和1,2。LIS是[1,2,4,5]长度为4。 # 删除3后数组为[1,2,4,5]LIS是[1,2,4,5]长度还是4。 # 看来这个“删除一个元素”对严格递增的LIS提升有限。面试官可能考察的是思路本身。通过测试你可能会发现这个问题的某些边界情况或者意识到自己解法的问题。在Mock中这个过程同样需要向面试官展示。4. 从“Round2”到“终面”构建你的系统性刷题与面试框架Blind Mock Interview的Round2标志着你从“知识积累”进入了“能力整合”阶段。要系统性地准备好不能只依赖随机练习。4.1 构建你的“解题反应堆”不要按题号顺序刷题而是按问题模式和思维方法来组织。模式层Pattern掌握20-30个核心算法模式如双指针、滑动窗口、二分查找、BFS/DFS、回溯、动态规划、贪心、并查集、前缀和、单调栈等。对于每种模式精刷3-5道经典题做到能默写。变体层Variation针对每个模式找它的常见变体。例如动态规划经典最长递增子序列LIS。变体1最长连续递增子序列LCIS。变体2最长数对链LIS in 2D。变体3俄罗斯套娃信封问题LIS with sorting。变体4如本文允许删除/插入一个元素的最长子序列。融合层Fusion练习融合多个模式的题目。例如一道题可能同时需要“前缀和哈希表”或者“单调栈动态规划”。当你拿到一道新题Blind你的思考应该是“这道题核心是在求什么它和哪个经典模式最接近它增加了什么约束条件这个约束条件如何改变了状态定义或转移方程”4.2 Mock Interview的实战清单在进行每一次Mock或自我模拟时严格按照以下清单执行开场2分钟[ ] 复述问题确认理解。[ ] 询问数据范围、输入输出格式、边界条件空数组、负数、超大数等。[ ] 举1-2个简单例子验证理解。思路阐述3-5分钟[ ] 提出最直观的暴力解法并分析复杂度。[ ] 提出优化方向联系到已知算法模式。[ ] 描述核心数据结构与算法步骤。[ ] 分析时间与空间复杂度。[ ] 询问面试官“这个思路您看可以吗”代码实现10-15分钟[ ] 从主函数开始定义清晰的函数签名和注释。[ ] 先写骨架再填充细节。[ ] 边写边讲解释关键行。[ ] 使用有意义的变量名。[ ] 优先保证逻辑正确再考虑优化。测试与检查3-5分钟[ ] 用给定的简单例子走一遍代码。[ ] 设计边缘用例测试空、单元素、全递增、全递减、有重复。[ ] 检查循环边界还是。[ ] 讨论可能的优化空间时间/空间。4.3 针对“最长递增子序列”及变体的专项准备以LIS为例一个系统的准备应该是基础模板熟练掌握O(n²) DP和O(n log n)贪心二分两种写法理解其本质区别一个求长度一个求序列。维度扩展二维LIS俄罗斯套娃信封问题先排序化为一维LIS。带权LIS每个元素有权重求最大权重的递增子序列。计数LIS求最长递增子序列的个数。输出具体序列而不仅仅是长度。操作扩展本文讨论的类型允许删除最多k个元素后的最长递增子序列。允许插入最多k个元素后的最长递增子序列。求最长递增子序列且相邻元素之差不超过某个值。思维工具预处理前缀数组和后缀数组。将“删除”操作转化为对两个子段的拼接问题。使用线段树或树状数组优化在值域上的查询用于优化组合部分。当你经过这样的系统训练再遇到“Blind”的变种题时你大脑中激活的不是一道题的答案而是一个可扩展的解决方案网络。你知道从哪里开始分析有哪些工具可以尝试以及如何与面试官协作来逼近最终答案。回到开头我朋友的那个问题。他缺的不是对LIS模板的记忆缺的是这套“拆解-联系-重构”的思维框架以及在高压力下清晰表达和迭代思路的能力。而这些正是通过一次又一次认真的“Blind Mock Interview”可以锤炼出来的。Round2“开始敲代码”敲下的不只是算法答案更是你作为一个问题解决者的思考路径和职业素养。把每一次模拟都当成真实面试把每一道陌生题目都当作构建你思维框架的积木这才是刷LeetCode和准备面试的长期主义。