算法导论读书笔记(4)
最大子数组问题
假设你要投资挥发性化学品公司。就像这家公司生产的化学品那样,该公司的股价也相当的不稳定,而且你一次只能买入一股并在之后的某个时间点卖出。为了弥补这种限制,你可以知道未来几天的股价。你的目标就是最大化你的收益。下图显示的是公司17天之内的股价。
当然,你会想要在最低点买入,在最高点卖出。但不幸的是,上图中的最低点发生在最高点之后。又或者换一种策略:找出最高点和最低点,从最高点向左找之前的最低点,从最低点向右找之后的最高点,分别找出这两种情况的最大收益,然后取值大的那个序对。但下图给出了一个简单的反例:
如图所示,股价最高点和最低点分别出现在第1天和第4天,但最大收益却是从第2天到第3天。
现在我们以一种不同的方式看这些股价。我们想找到一个日期的序列,使得从第一天到最后一天的净变(交易所当日与前一日收盘价之差)最大。这里我们考虑的不再是每天的股价,而是每天股价的变化,即第 i 天的价格变化是第 i - 1天的股价与第 i 天的股价之差。我们将这些股价的变化视为一个数组 A :
现在我们要找的就是数组 A 的一个子数组,该子数组要非空,连续,并且其值的和最大。我们把这种连续的子数组叫做 最大子数组 。
分治法解决最大子数组问题
假设我们要找出数组 A [ low .. high ]的最大子数组。分治法建议我们将数组分成两个规模尽可能相同的子数组。首先要找出数组中点 mid ,然后考虑子数组 A [ low .. mid ]和 A [ mid + 1 .. high ]。任何数组 A [ low .. high ]的连续子数组 A [ i .. j ]一定位于下列位置之一:
- 完全在子数组 A [ low .. mid ]中,有 low <= i <= j <= mid ,
- 完全在子数组 A [ mid + 1 .. high ]中,有 mid < i <= j <= high ,或
- 穿过中点,有 low <= i <= mid < j <= high 。
因此,数组 A [ low .. high ]的最大子数组也必然会满足上面情况中的一种。我们可以递归地求 A [ low .. mid ]和 A [ mid + 1 .. high ]的最大子数组,剩下的工作就是找出一个穿过中点的最大子数组,然后取三个子数组中有最大和的那个。
我们可以很容易的用线性时间找出穿过中点的最大子数组。基本策略是:穿过中点的子数组本身又是由两个子数组 A [ i .. mid ]和 A [ mid + 1 .. j ],其中 low <= i <= mid 和 mid < j <= high 。因此,我们只需要找出形如 A [ i .. mid ]和 A [ mid + 1 .. j ]的最大子数组,然后将它们合并起来就可以了。
FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high) 1 left-sum = -∞ 2 sum = 0 3 for i = mid downto low 4 sum = sum + A[i] 5 if sum > left-sum 6 left-sum = sum 7 max-left = i 8 right-sum = -∞ 9 sum = 0 10 for j = mid + 1 to high 11 sum = sum + A[j] 12 if sum > right-sum 13 right-sum = sum 14 max-right = j 15 return (max-left, max-right, left-sum + right-sum)
有了 FIND-MAX-CROSSING-SUBARRAY
过程在手,我们就可以编写分治法解决最大子数组问题的伪码了:
FIND-MAXIMUM-SUBARRAY(A, low, high) 1 if high == low 2 return (low, high, A[low]) // base case: only one element 3 else 4 mid = FLOOR((low + high) / 2) 5 (left-low, left-high, left-sum) = FIND-MAXIMUM-SUBARRAY(A, low, mid) 6 (right-low, right-high, right-sum) = FIND-MAXIMUM-SUBARRAY(A, mid + 1, high) 7 (cross-low, cross-high, cross-sum) = FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high) 8 if left-sum >= right-sum and left-sum >= cross-sum 9 return (left-low, left-high, left-sum) 10 elseif right-sum >= left-sum and right-sum >= cross-sum 11 return (right-low, right-high, right-sum) 12 else 13 return (cross-low, cross-high, cross-sum)
最大子数组问题的简单Java实现
/** * 伪码中作为结果返回的三元组 */ public class Triple { public int low; public int high; public int sum; public Triple(int low, int high, int sum) { this.low = low; this.high = high; this.sum = sum; } public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Triple)) return false; Triple t = (Triple) o; return this.sum == t.sum && this.low == t.low && this.high == t.high; } }
private static Triple findMaxCrossingSubArray(int[] arr, int low, int mid, int high) { int leftSum = Integer.MIN_VALUE; int rightSum = Integer.MIN_VALUE; int sum = 0; int maxLeft = mid; int maxRight = mid + 1; for (int i = mid; i >= low; i--) { sum += arr[i]; if (sum > leftSum) { leftSum = sum; maxLeft = i; } } sum = 0; for (int j = mid + 1; j <= high; j++) { sum += arr[j]; if (sum > rightSum) { rightSum = sum; maxRight = j; } } return new Triple(maxLeft, maxRight, (leftSum + rightSum)); }
public static Triple findMaxSubArray(int[] arr) { return findMaxSubArray(arr, 0, arr.length - 1); } private static Triple findMaxSubArray(int[] arr, int low, int high) { if (high == low) return new Triple(low, high, arr[low]); else { int mid = (low + high) >> 1; Triple leftSubArray = findMaxSubArray(arr, low, mid); Triple rightSubArray = findMaxSubArray(arr, mid + 1, high); Triple crossSubArray = findMaxCrossingSubArray(arr, low, mid, high); if (leftSubArray.sum >= rightSubArray.sum && leftSubArray.sum >= crossSubArray.sum) return leftSubArray; else if (rightSubArray.sum >= leftSubArray.sum && rightSubArray.sum >= crossSubArray.sum) return rightSubArray; else return crossSubArray; } }
最大子数组问题分析
首先假设问题的规模是2的幂,这样所有子问题的规模都是整数。设 T [ n ]为过程 FIND-MAXIMUM-SUBARRAY
在 n 个元素数组上的运行时间。当 n = 1时,过程的第1行,第2行都使用常量时间,所以 T (1) = Θ (1)。当 n > 2时递归开始,在第5行,第6行解决的子问题的规模是 n / 2(即原问题规模的一半),它们的运行时间都是 T ( n / 2 ),这样总的加起来就是2 T ( n / 2 )。第7行调用的过程 FIND-MAX-CROSSING-SUBARRAY
的运行时间为 Θ ( n )。最后得出 FIND-MAXIMUM-SUBARRAY
的运行时间为:
显而易见,该过程的运行时间为 T ( n ) = Θ ( n lg n )。