ARTICLE DETAIL

资讯详情

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

力扣周赛总卡题?用分治思维拆解算法难题,突破刷题平台期

力扣周赛总卡题?用分治思维拆解算法难题,突破刷题平台期 打完一场力扣周赛很多人会有一种感受题目似乎都见过但该做出来的题没做出来做出来的题也说不清自己是怎么想到解法的。排名一出来看一眼分数关掉页面下一场继续。这种状态持续很久刷题量上去了周赛成绩却稳在一个平台期。我越来越觉得问题不在于你刷了多少题而在于你脑子里有没有一套“稳定的问题拆解框架”。周赛 514 这类场次恰好适合用来思考一个底层能力——分治。分治不是一个具体的算法模板它是一套把未知问题拆成已知问题的思维方式。如果你能真正从分治出发去审视算法题你会发现很多“新题”其实是你已经会做的题的组合变形。这篇文章不会告诉你“周赛 514 的某题该怎么做”因为针对任意一场比赛直接背题解是最低效的学习方式。我更想围绕“分治”这个角度拆解它为什么是算法能力的基石怎么在周赛中快速判断一道题能不能分治以及如何把它沉淀成一套可复用的刷题和复盘框架。1. 先想清楚周赛卡住的不是代码而是缺少一套“问题分层”的思维1.1 很多人打完周赛只记住了题却没有记住“怎么想到的”周赛和平时刷题最大的区别是时间压力。平时你可以花一小时琢磨一道题周赛里一道中等题如果十分钟没有思路很多人就开始慌了。于是你会看到两种典型表现第一种靠题量堆积。看到题目长得像某个做过题就往上套模板套不上就放弃。第二种靠灵感和状态。状态好的时候能连过两题状态差的时候连读题都读不明白。这两种表现其实指向同一个问题你的解题过程没有分层。你跳过了“判断题型”和“拆解问题”这两步直接试图从“看到题目”跳到“写出代码”。而现实是算法题最难的部分往往不是写代码本身而是建立从问题到解法的路径。分治思维恰好能补上这一步。它不是让你遇到所有题目都用递归拆两半而是强迫你回答三个问题这个问题能不能拆成若干个规模更小的同类问题子问题的解能不能独立求解而不互相依赖子问题的解能不能合并成原问题的答案一旦你开始这样追问你其实就在对问题做“分层”。这种分层能力比背一百个模板都重要。1.2 分治是少数几个能跨题目复用的思维框架算法世界里有很多技巧比如滑动窗口、双指针、单调栈。这些技巧很实用但往往只适用于特定场景。分治不一样它更像是一个“元框架”归并排序是分治快速排序是分治最近点对是分治逆序对统计是分治最大子数组和也可以用分治。更重要的是分治思维能帮你在看到一个完全陌生的题时不直接进入代码搜索模式而是先问一句这题的解能不能由子问题的解拼出来这种能力放到周赛里尤其宝贵。周赛的题目不会全是原题但大部分题都建立在已知的算法骨架上。如果你能快速识别一道题里的分治结构你就等于把一个“新题”还原成了几个“旧题”的组合。所以从分治出发思考周赛不是为了让你变成只会递归的选手而是为了让你拥有一种“把复杂问题降维”的能力。这种能力才是周赛成绩能持续上升的真正杠杆。2. 分治不是递归也不是二分而是一套“拆开 - 解决 - 合并”的决策流程2.1 分治的完整定义三个步骤和一个前提先给一个朴素但不失效的定义分治就是把一个规模为 n 的问题拆成若干个规模更小的同类子问题递归求解之后再把子问题的解合并成原问题的解。标准步骤是三步分解Divide把原问题拆成若干个更小的子问题通常是对半分。解决Conquer递归地求解子问题。如果子问题足够小直接求解。合并Merge把子问题的解合并成原问题的解。但这三步成立的前提是子问题的解必须能够合并成原问题的解而且合并成本不能太高。如果不满足这个前提分治就不是一个好选择。举一个最常见的例子归并排序。对一个数组排序可以拆成对左半部分排序、对右半部分排序然后把两个有序数组合并。这里的“拆开”和“解决”都很自然关键在于“合并”这一步需要 O(n) 的时间而整个递归则可以做到 O(n log n)。这就是分治的经典范式。与之相对的如果一个问题拆成两个子问题后子问题的解几乎没法合并或者合并需要 O(n²) 甚至更高的成本那分治就会变成灾难。所以说分治不是一个“用了就一定好”的模板而是一个需要判断“拆开是否划算”的决策流程。2.2 分治和相邻概念的区别递归是形式二分是特例动态规划是另一条路很多人把分治、递归、二分这三件事混在一起这是刷题时非常常见的混乱点。递归是一种函数调用自身的写法分治可以用递归实现也可以用栈配合循环实现。递归只是分治的载体不是分治本身。二分查找看起来像分治因为每次也把问题砍掉一半。但二分查找的每一步只会进入一个子问题另一个子问题直接被丢弃。严格来说这更应该叫“减治”Decrease and Conquer而不是分治。分治要求所有子问题的解最终都要参与合并而减治只需要沿着一条路径走下去。动态规划和分治更像一对兄弟。两者都是把大问题拆成子问题直觉上非常接近。但动态规划处理的是“子问题重叠”的情况也就是不同的子问题之间共享更小的子问题所以需要用记忆化或自底向上的方式避免重复计算。分治处理的是“子问题相对独立”的情况子问题之间不需要共享中间结果。这组区别放在周赛里非常实用如果你发现拆出来的两个子问题有大量重叠那多半应该往动态规划方向想如果子问题之间完全不重叠合并逻辑明确那分治就是更合适的工具。一个简单的自检方式画出递归树如果递归树里不同分支会重复访问同一个节点说明大概率需要记忆化如果每个节点只被访问一次那才是干净的分治。2.3 为什么“分治”能成为底层思维分治的深层价值不在于它能让你的代码多写几行递归而在于它逼你把一个模糊的大问题转换成具体的小问题。很多人在周赛里卡住的真正原因不是题难而是他们从来没有把“求整段数组的最大值”这种表述转换成“左半部分的最大值、右半部分的最大值、跨中间部分的最大值”这样的结构。分治思维提供的就是这种转换能力。一旦你习惯了这种转换你看题的方式会变化。你会开始寻找“这道题里有没有一个可以拆分的东西”而不是“我背过的哪个模板能套上去”。3. 如何在周赛现场快速判断“这道题能不能分治”3.1 三个信号数据范围、拆分成本、合并成本周赛现场不可能让你花十分钟去判断题型。所以你需要一个快速的判断流程。我一般会做三件事第一看数据范围。如果 n 在 10^5 到 10^6 级别常见复杂度期望是 O(n log n) 或 O(n)。分治类算法的复杂度通常是 O(n log n) 或 O(n log² n)所以数据范围本身就是一种强提示。如果 n 只有 10^2 到 10^3那更多优先考虑 O(n²) 的模拟或动态规划分治反而未必是最优解。第二看问题是否能“半截解决”。如果一道题可以按位置、按区间、按集合把输入切成两半且每一半都构成一个同类子问题那它天然具备分治的基础。最常见的是数组区间类、二叉树类、平面点集类。反之如果问题涉及全局状态比如“所有元素共同影响结果”拆分就会很困难。第三估算合并成本。这是最容易被忽略的一步。很多人在比赛中想到分治写完了拆开和递归的代码最后才发现合并逻辑非常复杂或者合并需要 O(n²) 的时间直接超时。所以在决定用分治之前先用一句话描述“子问题的解怎么合并成原问题的解”。如果这句话说不清楚别着急写递归多半是题型判断错了。3.2 一道题如果不是分治强行分治会踩什么坑分治不是万能钥匙。下面的场景里强行使用分治大概率会出问题子问题之间有大量重叠正解是动态规划。例如求斐波那契数列你用朴素分治递归时间复杂度是指数级用记忆化或自底向上才是正确做法。问题本质上是在一个搜索空间里做决策不是区间或集合的拆分。比如“最长递增子序列”它的状态依赖不是简单的左右合并强行分治会导致非常复杂的合并逻辑。合并步骤会引入额外的高复杂度。比如某些求“区间内所有子区间性质”的题目如果合并时不得不枚举大量跨区间的组合复杂度就爆炸。在周赛里强行分治最常见的后果不是超时而是“写了大半才发现问题比想象中复杂”然后心态崩掉。所以我有一个个人原则分治只在合并步骤看起来足够清晰时才动手。如果合并的描述超过两句话先停下来重新判断题型。3.3 用题型卡片建立快速判断能力怎么提升题型判断速度我建议你建立自己的“题型卡片”。不需要多复杂一张卡片就三个区域题目特征描述这道题最显著的输入输出形式。疑似题型给出两到三个候选方向包括分治、动态规划、贪心、图搜索等。判断依据写清楚为什么优先选择其中一种为什么排除另外几种。举个例子。很多人常问“力扣腐烂的橘子是什么题型”。严格来说这是一道多源 BFS 或模拟扩散的题。但为什么有人会搞混题型因为它也涉及“分层扩散”——每一分钟把坏橘子周围的橘子感染这看起来有点像“分而治之”。但实际上腐烂扩散是全局状态同步更新的过程不符合“子问题独立求解再合并”的特征。真正适合做的是 BFS 层序遍历或队列模拟。这类题型卡片积累到一定数量后你再看一道新题本质上是拿新题的特征去匹配你脑中的卡片库。分治只是你卡片库里的一个基础类型但它的优先级很高因为很多数组、区间、树类题都会用到它。4. 周赛中常见的分治场景与代码骨架4.1 典型分治场景排序、逆序对、最近点对、表达式求值周赛里分治经常出现在下面几类问题中。排序与变形。归并排序、快速排序本身就是分治的入门题。比赛里很少直接让你写排序但会以排序思想为基础来变形比如统计逆序对、把数组组织成某种顺序等。区间统计类。比如求一个数组中“跨越中点的逆序对数量”这是经典分治。如果你需要统计区间内满足某种条件的配对数量而且左半区间和右半区间可以分别统计、最后再补上跨区间的部分那多半就是分治。最近点对。在平面点集中找距离最近的两个点是分治的经典问题。虽然周赛中出现的频率不高但它是理解“分治的合并步骤为什么重要”的最好教材。表达式求值。给定一个含加减乘除的表达式求所有可能加括号方式的结果。这种题非常适合分治按运算符拆成左右两个子表达式分别求值再合并结果。这也是分治和递归结合得比较自然的一类题。最大子数组和。这个题用动态规划做很简洁但分治也是一种有效的解法最大子数组要么完全在左半边要么完全在右半边要么跨越中点。跨中点的部分单独计算最后三者取最大。4.2 一个通用分治代码骨架如果你决定用分治代码骨架往往长这样def solve(problem): # 1. 基本情况问题规模足够小直接返回 if is_base_case(problem): return base_solution(problem) # 2. 分解把问题拆成若干个规模更小的子问题 sub_problems split(problem) # 3. 解决递归求解每个子问题 sub_results [solve(sub) for sub in sub_problems] # 4. 合并把子问题的解合并成原问题的解 return merge(sub_results)现实中你通常不会这样抽象地写因为不同题目的 split 和 merge 差别很大。但脑中有这个骨架可以让你在比赛里不会漏掉关键步骤。我见过很多人在比赛里写分治题最常犯的错误是忘记写 base case导致无限递归。base case 写得太大比如数组长度小于等于 10 就直接暴力求解这本身没问题但容易漏掉暴力逻辑里的边界。merge 函数里用了 O(n²) 的循环导致整体复杂度变成 O(n² log n)直接超时。递归深度过大Python 下没有设置sys.setrecursionlimit导致 RuntimeError。这些坑都不是算法思路问题而是工程习惯问题。平时练习时要刻意用自己的模板去套这几个步骤形成肌肉记忆比赛时才不容易翻车。4.3 分治的复杂度分析和主定理既然是周赛你还需要在动手之前快速估算分治是否可行。这时候有几个经验值很关键。如果一个规模为 n 的问题被拆成 a 个规模为 n/b 的子问题每次拆分和合并的复杂度是 O(n^d)那么整体复杂度通常可以分三种情况如果 a b^d结果是 O(n^d log n)。如果 a b^d结果是 O(n^d)。如果 a b^d结果是 O(n^(log_b a))。这是主定理的简化版本。我不建议你去背复杂的公式但至少要能处理最常见的场景拆成两个规模为 n/2 的子问题。如果合并过程是 O(n)整体是 O(n log n)这是归并排序。如果合并过程是 O(1)整体是 O(n)这是二分查找的变体。如果合并过程是 O(n²)整体大概率是 O(n² log n) 或更高通常不是好选择。在周赛里如果一个分治方案整体复杂度超过 O(n log² n)我通常会在动手前再犹豫一下看有没有更简单的做法。5. 分治思维怎么帮你降低“新题恐惧”5.1 新题不是没做过而是没分类周赛里最让人焦虑的时刻是看到一道题读了三遍心里仍然没有方向。这时候很多人会归因为“这题太新了”。但事实是你不需要做过原题你只需要能识别它属于哪个题型家族。如果你脑子里装的是“题目清单”那你永远只能做见过的题。如果你脑子里装的是“判断流程”那你看到任何新题都可以走一遍流程输入是什么输出是什么能不能拆分拆分后子问题独立吗合并成本高吗这就是分治思维带来的安全感。它不是让你一定找到最优解而是让你在最短时间内排除掉错误方向缩小搜索范围。5.2 用分治把未知问题映射到已知解法一个非常实用的技巧是拿到一道新题先试着把它“翻译”成自己熟悉的经典问题。比如这道题要求统计某种配对的数量能不能按中点一分为二分别统计左右再统计跨越中点的部分那就归约为“逆序对问题”。这道题要求求一段区间的最优值而且这个最优值能由左右区间的结果合并而来那就归约为“线段树”或“分治区间 DP”。这道题要求计算一个表达式的所有可能结果那就归约为“带缓存的递归分治”其中缓存对应的是重复出现的子表达式。翻译的过程就是分治思维在起作用。你不是在做新题你是在用一套识别框架把新题映射到旧题。5.3 周赛时间管理别在一道题上耗到失败周赛是一场有限时间内的决策游戏。分治思维还能帮你做时间管理。我现在遇到一道题如果五分钟内判断不出题型我会先写一个最朴素的暴力解法保证不空手而归。如果暴力解法跑通再考虑优化。如果暴力都写不出来我大概会标记为“题型识别失败”把它放到复盘阶段而不是在比赛里死磕。很多选手最大的问题不是不够聪明而是太想在一道题上证明自己结果浪费了做后面简单题的时间。分治思维教会你的是一种“分层决策”的习惯先判断再拆分最后动手。这种习惯一旦迁移到比赛中你会发现自己的心态稳定很多。6. 建立自己的刷题图谱和复盘框架6.1 从题目到题型的抽象如果每天刷题只是追求“过了”那刷一百道题和一题不刷没有本质区别。真正重要的是从每一道题里提炼出题型标签。我在刷题时会维护一个自己的知识图谱核心分类维度包括输入结构数组、字符串、链表、树、图、区间、点集。目标函数求最大值、最小值、方案数、是否有解、所有方案。核心算法分治、动态规划、贪心、图搜索、二分、双指针、数据结构优化。复杂度目标需要 O(n log n)、O(n)、O(n²) 还是可以暴力。分治在这个图谱里属于“核心算法”一个分支。但它的位置很特别因为很多看起来是动态规划的题也可以从分治的视角来推导很多看起来是数据结构的题底层也是分治思想比如线段树本身就是一种“离线分治结构”。6.2 一个可复用的周赛复盘三步法打完一场周赛不管成绩如何我都会花 20 到 30 分钟做一次复盘。复盘流程固定为三步。第一步记录题型判断过程。每一道题先不看题解写下自己在比赛时是怎么想的在哪一步卡住了。这一步的目标是找出“判断断层”就是题目特征到算法选择之间断了的那一环。第二步重写一遍最优解法。不看题解不复制别人的代码关掉榜单自己把最优解法重新写一遍。写不出来就默认自己其实还没有掌握它。第三步把题目归类到自己的题型卡片和知识图谱里。问自己三个问题这道题最核心的识别信号是什么如果下次看到类似信号我应该优先想到什么有没有和这道题共享同一算法骨架的其它题这套复盘方法的本质还是在用分治思维做自我诊断把“我没有做出题”这个大问题拆成“题型识别失败”“算法不熟”“实现细节出错”“复杂度估算错误”这些小问题然后针对不同的失败原因采取不同的改进措施。6.3 分治学习路径建议如果你刚接触分治不久建议你先不要把目标定在“周赛出三题”这么具体。你只需要围绕一个原则从最小可运行的分治示例开始逐步建立复杂度直觉。具体的路径可以是用归并排序和快速排序把分治的三个步骤和复杂度搞透。做几道能直接套用分治模板的题比如逆序对、最大子数组。故意把一道可以用分治做的题尝试用动态规划做一遍再换成用分治做一遍。对比两者的代码和复杂度体会什么情况适合分治什么情况适合动态规划。把分治、二分、递归这三个概念放在一起比较各自找几道代表题形成自己的“概念区分表”。进入周赛实战每场只关注一道可以用分治方式思考的题甚至不要求能做出来只要求能正确判断它是否适合分治。这个路径的核心是不追求数量追求判断准确率。判断准确率上去了写代码只是时间问题。7. 最后一个提醒分治是思维习惯不是银弹聊到最后我必须把边界写清楚。分治很强大但它不是万能的。它适合解决那些“可拆分、可独立求解、可合并”的问题。如果一个问题天然是全局性的或者拆开之后状态耦合严重分治就会显得笨重。你在周赛里要做的是不断扩充自己的判断库而不是把分治套在一切题目上。但我也要说分治思维的价值远不止用于解算法题。现实里的很多工作本质上都是“把一个大问题拆成可控的小问题逐步解决最后整合”。当你习惯了用分治的视角看问题你不仅会变会刷题也会更擅长拆解复杂任务、做技术方案设计、定位线上故障。从这个角度看从分治出发思考周赛最终得到的不是某一道题的答案而是一种能长期复用的思维方式。下次打开周赛页面时不妨先别急着读题。先记住一句话任何问题先问能不能拆再问怎么合。
返回列表