如果说《算法导论》的前三章是算法的“入门心法”,那么第4章与第5章就是一套“进阶招式”——不仅将分治策略玩出新花样,还为算法加上了概率的buff。本文旨在用清晰的逻辑梳理这两章的核心思想与经典案例,帮助你深入理解分治算法的奥妙与概率分析的威力。
一、第4章:分治策略——分解、解决与合并的艺术
分治策略的核心思想可以概括为“大事化小,小事化了”。面对一个复杂的大问题,我们将其分解成若干个规模更小、结构相似的子问题;递归地解决这些子问题;最后,将子问题的解合并起来,从而得到原问题的解。经典的归并排序早已运用了这一思想,而第四章则将其拓展到更多实用场景。
1. 最大子数组问题:寻找最佳投资时机
问题本质
假设你有一支股票连续n天的价格变动数据(每日价格变化,上涨为正,下跌为负),目标是找到一个连续的时间段进行买入和卖出,使得总利润最大化。这等价于在一个整数数组中,寻找一个连续子数组,其元素之和最大。
例如,给定数组 [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12],其最大子数组是 [18, 20, -7, 12],总和为43,对应“第8天买入,第11天卖出”的策略。
分治解法:三分法寻优
暴力枚举所有连续子数组需要 O(n²) 的时间。利用分治法,我们可以将时间复杂度降至 O(n log n)。
- 分解:将数组从中间位置分成左、右两个子数组。
- 解决:递归地求解左子数组和右子数组内的最大子数组。此外,还必须求解一个“跨越中点”的最大子数组(即子数组必须包含中点元素,并向左右两侧延伸)。
- 合并:比较左子数组解、右子数组解和跨越中点的解,取三者中最大的作为整个数组的解。
生活化理解:这好比在一个班级里找“身高最高的连续3人组合”。你可以先把班级分成左右两组,分别在两组内找最高的3人组合;同时,你还要考虑一种特殊情况:这个组合可能由左组末尾的几人和右组开头的几人共同组成。最后,从这三个候选组合中选出最高的。
2. Strassen矩阵乘法:以加法换乘法的智慧
为何需要优化?
两个 n×n 的矩阵相乘,标准的暴力算法需要执行 n³ 次标量乘法和若干次加法,其时间复杂度为 O(n³)。当 n=1000 时,需要进行10亿次乘法运算,计算量巨大。
Strassen算法的关键洞察
Volker Strassen发现,对于 2×2 的矩阵相乘,可以通过精心设计的线性组合,将所需的8次标量乘法减少到7次。其核心思想是“用更多的加法运算来换取一次宝贵的乘法运算”。
- 普通算法:8次乘法 + 4次加法。
- Strassen算法:7次乘法 + 18次加法。
对于更大的矩阵,可以通过分治策略递归地应用这个 2×2 的优化方法。
性能提升
Strassen算法将矩阵乘法的时间复杂度从 O(n³) 降低到了约 O(n^2.81)。不要小看这 0.19 的指数差距,当 n 很大时(如 n=1000),计算量可能减少一个数量级。
生活化理解:计算3个小组完成3项任务的总工时。笨办法是每个小组分别计算每项任务的工时再汇总(9次计算)。Strassen的思路类似先计算“小组1+小组2”完成“任务2+任务3”的联合工时,再通过几次加减运算拆分出最终结果,从而减少总的核心计算次数。
3. 递归式求解:主方法与辅助工具
分治算法的时间效率通常用一个递归式来描述,例如 T(n) = aT(n/b) + f(n)。第四章提供了三种求解递归式的通用方法:
- 代入法:先猜测解的形式(例如
O(n log n)),然后使用数学归纳法证明其正确性。
- 递归树法:将递归过程展开成一棵树,计算每一层的时间代价,然后对所有层求和。例如,归并排序的递归树每层代价总和都是
n,树高为 log n,故总代价为 O(n log n)。
- 主方法:这是最直接的“公式法”,适用于形如
T(n) = aT(n/b) + f(n) 的递归式。它根据 f(n) 与 n^(log_b a) 的增长速度关系,直接给出解:
- 情况1:若
f(n) 渐进小于 n^(log_b a),则 T(n) = Θ(n^(log_b a))。
- 情况2:若
f(n) 与 n^(log_b a) 渐进相等,则 T(n) = Θ(n^(log_b a) * log n)。
- 情况3:若
f(n) 渐进大于 n^(log_b a),且满足正则条件,则 T(n) = Θ(f(n))。
生活化理解:主方法好比一个“快递收费公式”。a是你寄的包裹数,b是每个包裹被压缩的倍数,f(n)是额外的包装费。根据包装费相对于包裹本身价值(n^(log_b a))的大小,采用不同的计费标准。
二、第5章:概率分析与随机算法
前四章的算法是“确定性的”——给定固定的输入,输出总是相同。第五章引入了不确定性:算法内部可以包含随机操作,因此对于相同的输入,多次运行的输出可能不同。然而,其平均性能往往非常优秀,甚至能避免最坏情况。
1. 雇用问题:如何最小化招聘成本?
问题场景
你需要雇一名助理。每天面试一位候选人,如果该候选人比当前在职的助理更优秀,你就会解雇当前助理并雇佣新人。每次面试花费 100 元,每次解雇并重新雇佣的总费用为 1000 元。目标是最小化总成本。
最坏情况分析
如果候选人恰好按能力从低到高的顺序到来,那么你每天都会遇到“更优者”,从而需要雇佣 n 次。总成本 = 100n + 1000n = 1100n,这是最糟糕的局面。
概率分析
假设候选人的顺序是完全随机的。可以证明,平均情况下,你只会雇佣大约 log n 次(自然对数)。例如,当 n=100 时,平均雇佣次数约为5次。总成本 ≈ 100*100 + 1000*5 = 15000 元,远低于最坏情况的 110000 元。
随机算法
如果候选人的到来顺序不是随机的(例如,有人故意将优秀的候选人排在后面),上述概率分析的结论就失效了。此时,我们可以采用随机算法:在面试开始前,主动将候选人顺序随机打乱。这样,无论原始输入顺序如何,我们都“创造”了一个随机输入,从而保证了算法的期望性能。
2. 指示器随机变量:概率分析的利器
为了计算平均雇佣次数,直接推导比较繁琐。使用指示器随机变量可以简化分析:
- 定义
X_i:事件“第 i 位候选人被雇佣”发生则 X_i=1,否则 X_i=0。
- 总雇佣次数
X = X_1 + X_2 + ... + X_n。
- 根据期望的线性性质(即使变量不独立也成立),
E[X] = E[X_1] + E[X_2] + ... + E[X_n]。
- 而
E[X_i] = P(第 i 位候选人被雇佣) = 1/i。
- 因此,
E[X] = 1 + 1/2 + 1/3 + ... + 1/n ≈ ln n。
生活化理解(生日悖论):计算一个23人的班级中至少有两人生日相同的概率。定义指示器变量 X_ij=1(表示第 i 人和第 j 人生日相同),则总的对数 X 的期望 E[X] = C(23,2) * (1/365) ≈ 0.69。虽然这个期望值小于1,但通过更精确的概率计算可知,此时概率已超过50%。指示器变量为理解此类问题提供了简洁的入口。
3. 其他概率算法实例
- 球与箱子(礼券收集者问题):有
n 个不同的箱子,每次随机向一个箱子投球。平均需要投 n * (ln n + O(1)) 次球,才能保证每个箱子都至少有一个球。这解释了为什么收集一套 n 张不同的卡片,平均需要购买远多于 n 包商品。
- 特征序列:抛一枚均匀硬币
n 次,最长连续正面朝上的序列的期望长度是 Θ(log n)。这意味着,在大量试验中,出现极长连续序列的概率非常低。
- 在线雇用问题(“拒掉前k个”策略):必须立即决定是否雇佣当前候选人,且不能回头。最优策略是:拒绝前
k 个候选人作为样本,之后雇佣第一个比前 k 个都优秀的候选人。理论证明,当 k ≈ n/e(e为自然常数)时,选到绝对最佳候选人的概率最高,约为 1/e ≈ 37%。这是一个在信息不完全下做最优决策的经典动态规划思想体现。
三、核心总结:分治与概率,提升算法效率的两大支柱
- 分治策略:其威力在于“分”而治之。关键在于设计高效的分解与合并步骤。它适用于那些能够被自然划分为相似子问题的问题,如排序、矩阵乘法、最近点对等。
- 递归式求解:主方法是快速分析分治算法效率的利器,而代入法和递归树法则提供了更通用的证明与推导工具。
- 随机算法:通过主动引入随机性,使得算法不依赖于输入的特定分布,从而获得良好的期望性能,并能有效避免最坏情况输入。
- 概率分析:指示器随机变量是将复杂概率期望问题拆解为简单0-1变量求和的强大工具,极大简化了分析过程。
总而言之,第4、5章揭示了算法设计不仅依赖于严谨的“硬算”逻辑,也离不开“巧算”的智慧——分治是结构化的巧算,概率是借助随机性的巧算。掌握这两种思想,能让你在解决复杂问题时,思路更加开阔,效率显著提升。如果你想深入探讨更多算法与数据结构的话题,欢迎来到云栈社区与广大开发者交流。