你持有一支股票的历史价格,允许在某个交易日买入并在之后的某个交易日卖出,目标是最大化收益。这个问题表面上在说股票,实际上等价于一个更简洁的数学表述:给定一个整数数组,找出和最大的连续子数组,返回其和。
这就是最大子数组问题。它看似简单,却恰好成为检验多种算法设计范式的绝佳试金石。
一、分治解法:横跨中点的最大子数组
我们首先尝试用分治思想求解。将数组Alow..high从中间mid处一分为二,最大子数组必然落于三种位置之一:完全位于左半区Alow..mid、完全位于右半区Amid+1..high、或者横跨中点跨越左右两个半区。
前两种情况直接递归求解。第三种情况需要特殊处理:任何横跨中点的最大子数组,一定由左半区以mid为右端点的最大后缀和,加上右半区以mid+1为左端点的最大前缀和拼接而成。找出这两个值都只需线性扫描,耗时O(n)。取三者中的最大值即为答案。
因此递归方程为T(n)=2T(n/2)+O(n),求解得T(n)=Θ(n log n)。这个复杂度对于此问题而言并非最优,但分治解法展现了一套清晰的模块化思维------将跨越边界的计算作为合并步骤的核心,这种"横跨结构"在图论与几何问题的分治算法中反复出现。
二、Kadane算法:动态规划的极简形态
Kadane算法将时间复杂度压到了O(n)。它基于一个看似朴素却足够有力的观察:考虑以位置i结尾的最大子数组和dpi。这个dpi只有两种可能:要么接在前面的最优解上,形成dpi-1+Ai;要么从当前位置重新开始,就是Ai本身。即:
dpi=max(dpi−1+Ai, Ai)dpi=max(dpi−1+Ai,Ai)
最终答案是所有dpi中的最大值。由于dpi仅依赖于dpi-1,我们甚至不需要维护整个dp数组,只需一个变量currentSum记录"以当前位置结尾的最大和",另一个变量maxSum追踪全局最大值,空间代价降到O(1)。
该算法仅用一次遍历,两个变量,便干净利落地终结了问题。它的设计思想本质上属于动态规划------将原问题的最优解归结为子问题最优解的递推。但与分治的自顶向下分解不同,动态规划自底向上逐步构造,避免了子问题的重复计算。
三、两种范式的对比审视
分治给出O(n log n),Kadane给出O(n)。纯粹从时间效率看,Kadane胜出。但若就此判定分治方案毫无价值,则失之草率。
分治方案的优势在于结构上的通用性。它所构造的"横跨中点最大子数组"技术,可以直接迁移到二维最大子矩阵问题中。给定一个n×n矩阵,枚举上下边界后压缩为一维数组,调用一维最大子数组算法即可------此时Kadane作为子过程承担最内层的计算。而二维问题的外层框架,恰恰是分治或枚举所搭建的。
此外,分治在并行计算场景下具有天然优势。子问题完全独立,可被分配到不同处理核心同时执行,而Kadane算法的串行依赖(每个位置的计算必须等前一个位置完成)使其难以直接并行化。
这引出了一个重要的算法设计哲学:最优的渐进复杂度不意味着最优的工程适用性。特定场景下,一个稍慢但结构灵活、易并行、可扩展的算法,可能比那个理论上最快的串行算法更具实用价值。
四、问题变形与延伸
最大子数组问题有若干重要变形值得关注。若数组中元素全为负数,算法应返回最大单元素(而非0),上述两种解法均需做相应调整。若要求返回子数组的具体起止位置,Kadane需要在更新maxSum时同时记录左右边界。若问题推广到允许至多k个不相交子数组的最大和,则回到经典动态规划的范畴,状态维度的增加使问题复杂度陡然上升。
从股票买卖到信号处理中的峰值检测,最大子数组问题以极简的输入输出映射了一个复杂而通用的优化核心。下一篇,我们将把视角从分治转向另一种强大的设计范式------动态规划,从矩阵链乘法入手,系统拆解最优子结构的构建方法。