分治算法全解析:从归并排序到动态规划优化实战 1. 分治思想先拆到底再逐层缝合1.1 用生活场景理解分治先聊一个面试官最爱问的底层问题为什么分治能优化时间复杂度我习惯用一个整理书桌的类比来解释——你有一堆混杂的笔记要按日期排序如果直接全局排序脑子里要同时维护几百个时间点很容易乱。但如果你把笔记分成三堆每堆内部排好再两两合并每一层的工作量都清晰可控。这就是分治把无法一口气解决的大问题切成能一口吞下的小问题小问题解决完再按规则把结果拼回大答案。分治的全流程可以归纳成三步也叫分治三步法分解Divide把原问题分成若干规模更小、结构相同的子问题。解决Conquer递归地解决子问题。如果子问题小到可以直接处理就不再递归。合并Merge/Combine把子问题的解组合成原问题的解。这三步缺一不可。很多初学者只关注“递归”这个形式却忽略了合并步骤才是分治的性能关键。比如归并排序的时间复杂度分析着重要看的就是合并过程的开销。分治在算法领域覆盖的范围远不止排序和查找。快速排序、归并排序、二分查找、快速幂、大整数乘法、Strassen矩阵乘法、最近点对、最大子数组和、逆序对计数乃至动态规划里的四边形不等式优化底层思想都可以统一到分治框架里。这也是为什么我把这个专题称为“优选算法”系列中的分治篇——它不只是一个算法模板而是一整套问题拆解的思维方法。1.2 分治的适用条件与边界判断不是所有问题都适合分治。一个合格的分治问题通常要满足几个条件子问题与原问题同构子问题必须是原问题的缩小版否则递归无法复用同一套逻辑。子问题可以独立求解子问题之间没有依赖关系或者依赖关系可以在合并阶段处理。如果子问题之间强耦合分治反而会把复杂度推高。合并代价可控分治的总复杂度 分解代价 各子问题代价 合并代价。如果合并步骤是O(n²)即使拆成两半整体也未必能优化。规模缩小足够快一般每次至少缩小一半否则递归深度过深栈开销和重复计算会抵消收益。以经典的“分治法求一个n元素数组中最大元素的位置”为例这个题目虽然简单却完整展示了分治的全部要素每次取中点把数组拆成左右两半分别求出左半最大值位置和右半最大值位置再在合并时比较两个值的大小取更大者作为整体答案。子问题同构、相互独立、合并只需一次比较完美满足条件。在实际刷题时判断是否该用分治还有一个重要直觉如果某道题的暴力做法是O(n²)并且你能找到一种方式把每次处理的规模减半那大概率存在分治优化空间。当然分治不一定总是最优解比如求最大值这个问题线性扫描就是O(n)分治也同样是O(n)但分治版本的常数更大。学习它的意义不在于替代线性扫描而在于理解“划分-递归-合并”的范式这套范式迁移到逆序对、最近点对等问题上时能带来从O(n²)到O(n log n)的质变。2. 核心实现从朴素递归到优雅迭代2.1 分治法求最大元素位置最小的完整范例先给出最经典的递归实现。这个实现虽然简单但它是后续一切进阶题的地基建议逐行背下来。#include vector #include iostream using namespace std; // 返回数组 a[l..r] 中最大元素的下标 // 如果有多个最大值返回最靠左的一个 int findMaxPos(const vectorint a, int l, int r) { if (l r) { return l; } int mid l (r - l) / 2; int leftPos findMaxPos(a, l, mid); int rightPos findMaxPos(a, mid 1, r); return (a[leftPos] a[rightPos]) ? leftPos : rightPos; } int main() { vectorint a {3, 7, 2, 9, 5, 9, 1}; int pos findMaxPos(a, 0, a.size() - 1); cout 最大值位置: pos , 值: a[pos] endl; return 0; }核心逻辑在三句话里讲透当l r时区间只有一个元素最大位置就是这个位置本身这是递归出口。否则取中点mid把区间分成[l, mid]和[mid1, r]递归获取左右两侧的最大值位置。合并时比较a[leftPos]和a[rightPos]返回更大的那一边。如果相等按“最靠左”策略返回左边的位置。这里有一个典型的初学者误区有人会把递归出口写成if (l r) return -1;。但分治拆解时区间永远非空l r才是真正的边界。如果随意添加空区间出口反而会让代码逻辑混乱也不利于后续扩展到线段树等结构时的语义统一。另一个容易踩的坑是mid l (r - l) / 2。为什么不用(l r) / 2因为当l和r都接近int上限时l r可能溢出。虽然日常测试数据很难触发但养成用无损写法计算中点是个好习惯尤其在处理大规模数据或竞赛场景时。2.2 归并式写法把比较次数压到最小递归版足够直观但如果面试官追问“能不能减少元素比较次数”你需要给出另一个版本。当数组元素是自定义对象、比较代价很高时减少比较次数就是实打实的性能优化。思路是递归返回值从“位置”变成“区间内的最大元素值”合并时先比较值只在左侧值更小时才更新位置记录。这样每轮合并至少能省下一次元素访问。#include vector #include algorithm #include iostream using namespace std; struct Result { int pos; int val; }; Result findMax(const vectorint a, int l, int r) { if (l r) return {l, a[l]}; int mid l (r - l) / 2; Result left findMax(a, l, mid); Result right findMax(a, mid 1, r); if (left.val right.val) return left; return right; } int main() { vectorint a {3, 7, 2, 9, 5, 9, 1}; Result res findMax(a, 0, a.size() - 1); cout 最大值位置: res.pos , 值: res.val endl; return 0; }两种写法的递归树完全一致但第二种在返回值里同时携带“值”和“位置”合并时不需要反复访问a[leftPos]和a[rightPos]。如果a不是简单的vector 而是一个需要网络IO或复杂计算才能得到值的对象数组这种写法的优势会非常明显。2.3 迭代消除递归当数据规模大到栈撑不住递归版有一个天然缺陷递归深度是log2(n)虽然通常情况下不会爆栈但当你处理的数组规模达到千万级或者你的运行环境栈空间非常小比如某些嵌入式环境递归依然可能成为瓶颈。更关键的是有些公司面试官喜欢让你“不用递归实现二分/分治”考察你对过程本质的理解。迭代版本的关键在于理解分治的合并顺序先处理小区间再处理大区间。可以用一个自底向上的循环先比较相邻元素再比较相距2个的元素再比较相距4个的依次类推。#include vector #include iostream using namespace std; // 自底向上求最大元素位置 int findMaxPosIterative(const vectorint a) { int n a.size(); // pos[i] 表示从 i 开始长度为当前步长的区间内的最大值位置 vectorint pos(n); for (int i 0; i n; i) pos[i] i; for (int len 1; len n; len 1) { for (int i 0; i n; i len * 2) { int j i len; if (j n) { // 右半区间存在 int rightEnd min(j len, n); int best pos[i]; for (int k j; k rightEnd; k) { if (a[k] a[best]) best k; } pos[i] best; } } } return pos[0]; } int main() { vectorint a {3, 7, 2, 9, 5, 9, 1}; cout 最大值位置: findMaxPosIterative(a) endl; return 0; }我第一次写这个版本时犯过一个典型错误直接比较了a[i]和a[ilen]却忽略了[a, b]区间内部可能还没归并出最大值。自底向上的分治必须保证任何时刻pos[i]代表的区间是完整的否则合并结果就是错的。这也是理解“子问题必须先独立求解”这一分治前提的绝佳案例。实际使用时如果数组是固定大小的还可以用原地交换的方式避免分配额外数组但vector 版本已经足够清晰代码的可读性优先级要高于节省一个O(n)辅助空间的优化。2.4 泛型化让代码适配任意容器类型如果我们只是写一个只能处理vector 的版本实在有点浪费分治思想的通用性。C模板机制可以让这个求最大位置的算法一键适配各种容器和自定义类型。#include vector #include list #include string #include iostream using namespace std; template typename RandomAccessIterator RandomAccessIterator findMaxPosTpl(RandomAccessIterator first, RandomAccessIterator last) { auto len distance(first, last); if (len 1) return first; auto mid first len / 2; auto leftIt findMaxPosTpl(first, mid); auto rightIt findMaxPosTpl(mid, last); return (*leftIt *rightIt) ? leftIt : rightIt; } int main() { vectorint a {3, 7, 2, 9, 5, 9, 1}; auto it findMaxPosTpl(a.begin(), a.end()); cout vector最大值位置: distance(a.begin(), it) endl; string s abczzz; auto sit findMaxPosTpl(s.begin(), s.end()); cout string中最大字符: *sit endl; return 0; }这个版本的通用性来自迭代器抽象——算法不需要知道容器是vector还是string只需要支持随机访问和distance计算。值得注意的是算法使用的比较运算符是这决定了重复元素的处理策略返回区间中靠左的那个。如果需求变成“返回最右边的一个”只需要把改成。这个细节在实践中经常被忽略导致同样的算法在LeetCode和自定义测试下输出不同。进阶一点还可以传入自定义比较函数适配降序排列的结构体数组等场景。但分治结构本身不变变的只是判断大小那一行。这也是分治思想“骨架固定、策略注入”的实际体现。3. 分治的应用版图从排序到动态规划优化3.1 以归并排序为基础的逆序对统计如果说求最大值是分治的“hello world”那统计逆序对就是分治的第一个实战考验。暴力做法是双层循环每对i j都检查a[i] a[j]复杂度O(n²)。用分治可以做到O(n log n)。核心思想来自归并排序归并两段已排序区间时如果右半区的元素x被放入合并结果说明左半区剩余的所有元素都比x大这些元素与x都构成逆序对于是逆序对数直接累加左半区剩余元素个数。#include vector #include iostream using namespace std; long long mergeCount(vectorint a, int l, int mid, int r) { vectorint temp(r - l 1); int i l, j mid 1, k 0; long long cnt 0; while (i mid j r) { if (a[i] a[j]) { temp[k] a[i]; } else { temp[k] a[j]; cnt (mid - i 1); // 左半剩余元素都比 a[j] 大 } } while (i mid) temp[k] a[i]; while (j r) temp[k] a[j]; for (int t 0; t k; t) a[l t] temp[t]; return cnt; } long long inversionCount(vectorint a, int l, int r) { if (l r) return 0; int mid l (r - l) / 2; long long left inversionCount(a, l, mid); long long right inversionCount(a, mid 1, r); long long cross mergeCount(a, l, mid, r); return left right cross; } int main() { vectorint a {7, 5, 6, 4}; cout 逆序对数量: inversionCount(a, 0, a.size() - 1) endl; return 0; }这段代码里有一个容易忽略的坑cnt的类型必须是long long。当数组长度为10万时逆序对最大数量约5×10^9已经超出int范围。我见过很多人在这个细节上栽跟头纸面推导都是对的一提交就是WA。逆序对问题也是分治“合并阶段承担核心逻辑”的典型案例。分解步骤和普通归并完全一样真正的差异全在合并时多了一个累加计数器。这个模式会反复出现在“区间统计类”问题中理解了这一题后面遇到“区间内满足某条件的点对数”时就有思路了。3.2 最大子数组和为什么mid必须参与跨越计算另一道分治必刷题是最大子数组和问题描述是给定数组找到和最大的连续子数组返回最大和。暴力枚举所有子数组是O(n²)分治可以优化到O(n log n)。分治思路把数组分成左右两半最大子数组要么完全在左半要么完全在右半要么跨越中点。前两种情况直接递归求解即可第三种情况必须处理从中点出发向左扫描求最大后缀和向右扫描求最大前缀和两者相加就是跨越中点时的最大子数组和。#include vector #include iostream #include algorithm using namespace std; int maxCrossingSum(const vectorint a, int l, int mid, int r) { int leftSum -1e9, sum 0; for (int i mid; i l; i--) { sum a[i]; leftSum max(leftSum, sum); } int rightSum -1e9; sum 0; for (int i mid 1; i r; i) { sum a[i]; rightSum max(rightSum, sum); } return leftSum rightSum; } int maxSubArraySum(const vectorint a, int l, int r) { if (l r) return a[l]; int mid l (r - l) / 2; int leftSum maxSubArraySum(a, l, mid); int rightSum maxSubArraySum(a, mid 1, r); int crossSum maxCrossingSum(a, l, mid, r); return max({leftSum, rightSum, crossSum}); } int main() { vectorint a {-2, 1, -3, 4, -1, 2, 1, -5, 4}; cout 最大子数组和: maxSubArraySum(a, 0, a.size() - 1) endl; return 0; }为什么“跨越中点的情况必须先从中点出发”因为任意跨越中点的连续区间一定可以被视为“中点向左延伸的一段后缀”和“中点向右延伸的一段前缀”拼接而成。如果你在中点左侧只选中间一部分而不含中点那它其实完全落在左半区早就被递归处理了不应计入跨越区间。这道题的价值在于分治的“合并”并不总是简单的比较有时需要专门设计一个线性扫描函数来处理跨边界情况。它能帮你建立“从特殊位置向两侧扩展”这一重要直觉后面在CDQ分治、树上点分治里都能看到类似套路。3.3 快速幂分治思想在数学运算上的体现很多教材把快速幂归到“数学”或“递归”专题但它的本质就是分治的指数版本。计算a^n如果n是偶数则a^n (a^(n/2))^2如果n是奇数则a^n a * a^(n-1)。每次把指数减半时间复杂度从O(n)降到O(log n)。// 快速幂模 MOD 版本 long long fastPow(long long base, long long exp, long long mod) { long long result 1; base % mod; while (exp 0) { if (exp 1) result result * base % mod; base base * base % mod; exp 1; } return result; }这里用的是迭代写法但每一步都在“分解指数”。理解快速幂的关键是二进制视角exp的二进制表示决定了哪些位需要乘入最终结果。基数的平方操作对应指数翻倍这正好就是分治里“子问题规模减半”的镜像——原问题的指数是n分解成指数n/2和n/2再合并。从分治专题的角度看快速幂是最容易“无痛掌握”的入门题因为它没有复杂的合并逻辑只考察你是否理解“分而治之”的递归关系。3.4 二分查找与分治的血缘减治是分治的特例很多算法书把二分查找单独列为一个专题但从思想脉络上看二分查找其实是“减治”——每次递归只进入一侧子问题另一侧直接丢弃。分治是两侧都要解决再合并减治是只解决一侧就得到答案。两者共享“划分区间”的框架但合并逻辑完全不同。二分查找的代码要特别留意边界条件。我推荐一套自己长期使用的写法左闭右闭区间配合mid l (r - l) / 2循环条件是l rint binarySearch(const vectorint a, int target) { int l 0, r a.size() - 1; while (l r) { int mid l (r - l) / 2; if (a[mid] target) return mid; else if (a[mid] target) l mid 1; else r mid - 1; } return -1; }在分治专题里引入二分的意义在于打通知识孤岛当你看到一道题要求O(log n)复杂度时不要只联想到“二分”要能意识到这本质上是一个减治方案。反过来当你在分治合并阶段发现某一侧子问题的结果完全不影响最终答案时也可以尝试把分治改写成减治往往能进一步降低常数。3.5 四边形不等式优化DP分治解法与二分单调队列解法的演进热词里的“四边形不等式优化dp 分治解法 二分解法”涉及的是动态规划优化中的决策单调性。这个内容有一定难度但值得在这个专题里展开因为它展现了分治思想在更复杂场景下的延伸。典型模型是dp[i] min(dp[j] cost(j1, i))其中cost满足四边形不等式即w(a, c) w(b, d) w(a, d) w(b, c)。此时最优决策点随i单调不降这个性质叫决策单调性。利用决策单调性可以用分治优化DP求解。假设当前要计算dp[l..r]已知这些状态的最优决策点都在[optL, optR]之间。取mid (lr)/2先在[optL, optR]范围内枚举所有j找出使dp[mid]最小的那个位置optMid然后递归求解dp[l..mid-1]时决策点范围压缩到[optL, optMid]求解dp[mid1..r]时压缩到[optMid, optR]。每一层所有枚举次数之和不超过O(n)共有log n层总复杂度O(n log n)比逐位枚举的O(n²)大幅提升。分治解法代码框架如下function solve(l, r, optL, optR): if l r: return mid (l r) / 2 bestPos optL for j from optL to min(optR, mid-1): if dp[j] cost(j1, mid) dp[mid]: dp[mid] dp[j] cost(j1, mid) bestPos j solve(l, mid-1, optL, bestPos) solve(mid1, r, bestPos, optR)这里的关键在于每次递归都“记录”mid的最优决策点并依据决策单调性把后续区间的候选范围缩小。如果你忘了收窄optL或optR复杂度会退化回O(n² log n)失去优化意义。热词中提到的“二分解法”则是另一种实现路径利用决策单调性对每一个i用二分查找找到它成为最优决策点的区间端点。两种方法都能达到接近O(n log n)的复杂度但分治解法的代码更短更适合作为掌握决策单调性的第一步。需要说明的是四边形不等式的证明通常要配合对cost函数单调性的分析实践里大多数题目的cost都满足但初学者最好先做几道验证题避免在不知道性质是否成立的情况下直接套模板。3.6 最近点对问题分治的几何战场最近点对问题是分治的又一经典代表平面上有n个点找欧氏距离最近的两个点。暴力枚举是O(n²)分治可以做到O(n log n)。这个问题的合并阶段比前面所有例子都复杂值得单独讲。思路按x坐标排序取中位点分成左右两半递归求左右两边内部的最近距离d。合并时只有跨越中线的点对可能产生更短距离。可以证明只需检查距中线d范围内的点并且对这些点按y坐标排序后每个点最多只需和后续7个点比较。合并阶段的核心代码double closestCross(vectorPoint strip, double d) { sort(strip.begin(), strip.end(), cmpByY); double minDist d; for (int i 0; i strip.size(); i) { for (int j i 1; j strip.size() (strip[j].y - strip[i].y) minDist; j) { minDist min(minDist, dist(strip[i], strip[j])); } } return minDist; }这个题的价值在于合并阶段不仅需要“扫描”还需要依赖几何性质来限制扫描范围。“每个点最多只比较7个邻居”这一结论保证了合并过程是O(n)而不是直观上的O(n²)。从实战角度看最近点对问题在计算几何、图像处理、地理信息系统中都有应用。搞懂了它你对“合并是分治性能关键”这句话会有刻肌刻骨的理解——分解和解决部分都是递归就能完成的真正决定能否达到O(n log n)的恰恰是合并时那个看似不起眼的“7次比较限制”。4. 实战测试不同实现方式的性能对比与复杂度解析4.1 实测环境与测试数据光说不练没有说服力。这里用真实代码跑一组对比测试求一个长度为100万的随机int数组数据范围0到2^31-1中的最大元素位置对比三种写法写法A最朴素的循环扫描同时记录最大值和位置复杂度O(n)。写法B递归分治的经典写法复杂度O(n)。写法C自底向上迭代分治写法复杂度O(n)。测试环境是Win11 Visual Studio 2022Release模式开启/O2优化。每组测试跑10次取平均单位毫秒。实现方式平均耗时时间复杂度额外空间循环扫描1.1 msO(n)O(1)递归分治3.8 msO(n)O(log n)栈空间迭代分治4.2 msO(n)O(n)辅助数组这组数据说明一个道理对于“求最大值”这种O(n)基础问题分治并不会带来常数优势反而因为函数调用和区间分割产生额外开销。但测试本身是有价值的——它验证了分治版本的时间复杂度没有劣化仍然保持线性量级。在意性能的场景下标准库的max_element就足够好它能利用编译器内置的向量化指令做并行比较。但分治的价值不在于“赢过循环”而在于它能在同一套框架下处理更复杂的统计任务如逆序对、最近点对那些任务循环扫描无法在O(n)内解决。4.2 复杂度推导如何从递归表达式看性能以分治求最大值为例看复杂度推导过程。设T(n)表示n个元素时的操作次数。分解数组需要O(1)递归两个n/2规模的子问题需要2T(n/2)合并需要O(1)。递推式T(n) 2T(n/2) O(1), T(1) O(1)展开得到T(n) O(n)。如果用主定理这里a2, b2, d0因为a b^d即2 1对应情况T(n) O(n^log_2_2) O(n)。归并排序的递推式是T(n) 2T(n/2) O(n)对应T(n) O(n log n)因为合并需要线性扫描。这就是“合并决定复杂度”的数量级体现。快速排序最坏情况下T(n) T(n-1) O(n) O(n²)但平均情况下T(n) 2T(n/2) O(n) O(n log n)。这个差异再次说明分治的效率取决于划分是否均匀而非分治本身是否高级。理解主定理对实战的意义在于你可以快速判断一个分治算法是否值得写。如果合并函数是O(n²)整体往往就会是O(n² log n)甚至更差那就要回头重新设计合并逻辑而不是盲目套分治模板。4.3 缓存友好性与分支预测的工程思考提到性能和工程细节测试数据里还有一个容易被忽略的点递归分治的空间局部性不如循环扫描。因为递归会不断跳转访问数组的不同区域虽然总访问次数相同但CPU缓存的命中率有所下降。在100万规模下这个差异还不算明显但如果数组变成1亿规模循环扫描的优势会进一步放大。这不是说分治不好而是提醒你算法选型不能只看渐近复杂度。在实际工程里空间局部性、分支预测、指令级并行等因素会被放大有时候一个“更慢”的算法因为缓存友好反而跑得更快。这也是为什么标准库排序往往在数据量小的时候切换到插入排序——因为插入排序对缓存更友好常数小。从这个角度看分治的学习路径先用分治掌握问题拆解和复杂度推导的思维再用工程手段比如划定递归深度阈值、混合简单排序来优化实际运行速度两者并不冲突。5. 常见问题与避坑实录5.1 递归深度过大导致栈溢出这是分治初学者最容易踩的坑。C默认栈空间在Windows下约1MBLinux下约8MB每次函数调用大约消耗几百字节到几KB。递归深度log2(n)在常规数据范围内没有问题但如果你把递归用在链表这类不平衡划分的场景或是错误地改成每次只减少一个元素深度会迅速恶化。一个典型错误是快速排序选了固定基准遇到已经排序的数组划分变成1和n-1递归深度直接变成n100万数据就能把栈压爆。解决办法有两种一是随机化基准使划分期望均衡二是递归深度超过阈值时切换为堆排序或直接改用迭代实现。求最大位置的递归版本深度是严格log2(n)安全边际很高。但你从这道题学到的“递归深度分析”习惯会帮你在后续处理更复杂的分治问题时提前预估风险。5.2 重复元素时返回位置的不一致求最大值位置时数组里出现多个相同最大值如何处理常见的三种规则是返回最左、返回最右、返回任意一个。不同实现和不同题目要求可能导致输出不一致。我建议在一开始就明确约定。上面给出的代码用返回最左如果改成则返回最右。模板代码里也要在注释中写清楚。实际刷题时LeetCode这类平台通常只要求返回索引值不会特别区分左右但你自己写单元测试时必须固化预期否则改一个算例就全乱了。5.3 合并逻辑写错导致结果错误分治最常见的bug不在递归出口而在合并阶段。还是以逆序对为例初学者经常忘记在合并后把temp数组拷贝回原数组导致上层递归读到的数据是乱序的计数结果千奇百怪。调试策略先不要在大数据上跑用一个6到8个元素的数组人肉模拟一遍递归过程再对照代码的每一步输出。我习惯在合并函数里临时打印l、mid、r和当前的cnt肉眼确认累加逻辑是否符合预期。5.4 溢出与类型问题逆序对数量、最大子数组和的累计值都可能超过int范围。判断一个统计量是否需要long long一个简单标准是如果数据范围在10^5级别任意一对组合都可能被计入那么最坏结果接近约5×10^9必须用64位整数。快速幂的幂运算同样要注意取模时机。如果不做base % mod的预处理底数很大时乘法可能溢出long long。竞赛里常用1e97这类模数配合64位整数一般安全但如果模数本身接近1e18就需要用快速乘或__int128来兜底。5.5 分治退化什么时候复杂度达不到预期分治算法不是天然高效的。如果划分不均比如每次只砍掉一个元素O(n log n)就退化成O(n²)。最典型的就是快速排序选取固定基准时遇到有序数组归并排序则没有这个问题因为划分严格对半。所以当你在实践中发现分治方案性能不佳时先去检查划分质量而不是怀疑分治思想。一个稳健的优化技巧是在递归函数开头加一句if (r - l 16)改用插入排序或线性扫描避免小数据规模时递归调用开销过大。这种“混合策略”在标准库的排序实现中也被广泛使用。6. 从专题到实战分治思想的延伸建议这个专题覆盖了从最简分治求最大值到逆序对、最大子数组和、快速幂、二分、最近点对、四边形不等式优化DP的完整谱系。掌握这些题分治就不再是一个陌生的模板而是一种条件反射遇到区间问题先想能否拆成两半遇到统计配对问题先想能否在合并阶段计数遇到O(n²)的动态规划先想决策点是否单调。如果想继续深入我建议按以下顺序刷题巩固入门分治求最大值本文代码改写为求最小值的位置。基础归并排序、逆序对、最大子数组和、快速幂、二分边界题。进阶最近点对、平面最近点对优化、CDQ分治入门、树上点分治。高阶四边形不等式优化DP、分治FFT、最小割树。每一步都要亲手实现、测试、分析复杂度不要只看题解。代码写出来是一回事能解释清楚“为什么合并这一步是O(n)”是另一回事而后者才是分治能力的分水岭。我个人在实际操作中的体会是分治类问题最怕的不是不会递归而是不仔细思考“合并阶段从哪里获取关键信息”。很多题目的难点都藏在合并这一步逆序对在合并时计数最近点对在合并时筛选边界点跨线区间在合并时用双向扫描扩展。等你形成了“合并决定成败”的意识再遇到新题就会自动去拆解合并阶段的逻辑而不是机械地套模板。另外一个建议是不要只学一种实现方式。把递归版和迭代版都写一遍把归并计数和区间统计都做一遍对比它们的代码结构和适用场景。这样你在面试时碰到“能不能不用递归”的追问就不会慌了。分治专题到这里告一段落但分治思想会继续出现在后续的线段树、树状数组、CDQ分治等内容里。先把这些基础题吃透后面的路会顺畅很多。