Fork me on GitHub

算法导论读书笔记(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 <= midmid < 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-SUBARRAYn 个元素数组上的运行时间。当 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 )。

posted on 2014-04-12 18:53  sungoshawk  阅读(1087)  评论(0编辑  收藏  举报