【算法】最大子数组

问题描述:

给定一只股票在某段时间内的历史价格变化曲线,找出一个能够实现收益最大化的时间段。

理解:

为找出最大化的收益,需要考虑的是在买进和卖出时的价格变化幅度,因此从该股票的每日变化幅度来考虑问题比较合适。由此,可以将上述问题稍作变形:给定一只股票在某段时间内的每日变化幅度,找出一个合适的买进和卖出时间,以实现收益最大化。因此,将输入数据转换如下,并试图在整个时间段中找到一个累加和最大的子区间,亦即最大子数组。

暴力求解:

首先能够想到的是在一个给定数组(区间)中,其子数组(子区间)的个数是C(2,n),很容易就能遍历完所有子数组从而找出最大的那个,其最坏情况渐进时间复杂度是Θ(n2)。假设每日变化幅度保存在数组A中(A的下标从1到n),A.length表示A的元素个数,最终结果以元组形式返回;给出伪码如下:
BRUTE_FORCE(A)
            i = 1
            sum = -infinity
            for i <= A.length, inc by 1
                j = i
                last_sum = 0
                for j <= A.length, inc by 1
                    last_sum += A[j]
                    if last_sum > sum
                        sum = last_sum
                        start = i
                        end = j
            return (start, end, sum)

java代码实现:

private static void bruteForce(int[] arr) {
  int start = -1, end = -1, max = 0;
  for (int i = 0; i < arr.length; i++) {
    // 定义lastSum的目的,每一轮新的循环都需要重新累计
  int lastSum = 0;
  for (int j = i; j < arr.length; j++) {
    lastSum += arr[j];
    if (lastSum > max) {
      max = lastSum;
      start = i;
      end = j;
        }
    }
  }
  System.out.println("maxSum = " + max + ", start : " + start + ", end = " + end);
 }

分治求解:

分治策略通常包含:分解子问题,解决子问题,合并子问题。由此可以推出大致的解决思路:首先依然假设数据输入如上一个方法那样,然后考虑将A[1...n]拆分为规模大致相同的两个子数left[1...mid]和right[mid+1...n],其中mid=(1+n)/2向下取整,那么可以肯定,最大子数组要么在这两个子数组中,要么横跨这两个子数组,因此可以分别求解这三种情况,取其中最大的子数组并返回即可。
对于left/right子数组可递归求解,而对于横跨两个子数组的情况,如果能够使得该情况下的求解时间复杂度为O(n),那么应该能让整体的最坏时间复杂度低于Θ(n2)。如果仅仅是通过遍历所有包含A[mid]和A[mid+1]的子数组来找最大子数组,那么很显然仅求解该情况就需要Θ(n2)的时间。可以推断横跨两个子数组的最大子数组,必须由两个分别在left/right中的子数组组成,这两个子数组在分别包含了A[mid]和A[mid+1]的所有子数组中是最大的;因为如果存在一个不满足上述条件的最大子数组,那么总可以用上述方法找到一个更大的子数组。
根据上述思路,很容易推知求解横跨两个子数组的情况只需要O(n)的时间。由此给出伪码如下:
(1)子过程:找出横跨两个子数组的最大子数组
 FIND_CROSSING_MAX_SUBARRAY(A, low, mid, high)
                left_sum = -infinity
                sum = 0
                i = mid
                for i >= low, dec by 1
                    sum += A[i]
                    if sum > left_sum
                        left_sum = sum
                        left_index = i
                
                right_sum = -infinity
                sum = 0
                i = mid + 1
                for i <= high, inc by 1
                    sum += A[i]
                    if sum > right_sum
                        right_sum = sum
                        right_index = i
                return (left_index, right_index, left_sum+right_sum)
(2)主过程:分治法找出最大子数组
FIND_MAX_SUBARRAY(A, low, high)
                if low == high
                    return (low, high, A[low])
                else
                    mid = down_trunc((low + high) / 2)
                    (left_start, left_end, left_sum) =
                        FIND_MAX_SUBARRAY(A, low, mid)
                    (right_start, right_end, right_sum) =
                        FIND_MAX_SUBARRAY(A, mid+1, high)
                    (cross_start, cross_end, cross_sum) =
                        FIND_CROSSING_MAX_SUBARRAY(A, low, mid, high)
                    
                    if left_sum > right_sum and left_sum > cross_sum
                        return (left_start, left_end, left_sum)
                    else if right_sum > left_sum and right_sum > cross_sum
                        return (right_start, right_end, right_sum)
                    else
                        return (cross_start, cross_end, cross_sum)

可以看出上述算法渐进时间复杂度为Θ(nlg(n))。
java代码实现:

private static int[] findMaxSubArray(int[] arr, int left, int right) {
  // 分解到只有一个元素
 if (left == right) {
    return new int[]{left, right, arr[left]};
  }
  int mid = (left + right) / 2;
  int[] leftResult = findMaxSubArray(arr, left, mid);
  int[] rightResult = findMaxSubArray(arr, mid + 1, right);
  int[] crossingResult = findCrossingMaxSubArray(arr, mid, left, right);
  if (leftResult[2] > rightResult[2] && leftResult[2] > crossingResult[2]) {
    return leftResult;
  } else if (rightResult[2] > leftResult[2] && rightResult[2] > crossingResult[2]) {
    return rightResult;
  } else {
    return crossingResult;
  }
}

private static int[] findCrossingMaxSubArray(int[] arr, int mid, int left, int right) {
  int leftIndex = -1, rightIndex = -1, leftMax = Integer.MIN_VALUE, rightMax = Integer.MIN_VALUE, sum = 0;
  for (int i = mid; i >= left; i--) {
    sum += arr[i];
    if (sum > leftMax) {
      leftIndex = i;
      leftMax = sum;
    }
  }
  sum = 0;
  for (int i = mid + 1; i <= right; i++) {
    sum += arr[i];
    if (sum > rightMax) {
      rightIndex = i;
      rightMax = sum;
    }
  }
  return new int[]{leftIndex, rightIndex, leftMax + rightMax};
}

缩减问题规模:

缩减问题规模的方法:

在查找过程中,是否可以根据现有的信息,来缩减需要排查的子数组个数,进而获得更好的时间复杂度呢?一个思路是不再重复检查以前累加过的元素,即从左至右累加元素,保存其中的最大子数组,如果在加入一个元素后累加和为负数,则从该元素的后一个元素重新累加,直至整个数组遍历完毕。该思路有效的前提是证明以下几个假设:

1.  可以将最大子数组来源分为三种:已经遍历完的数组部分、未遍历的数组部分以及跨越这两部分的子数组
2.  可以假设当从左至右累加直至累加和为负,所得的最大子数组是当前已遍历完的数组部分中最大的
3.  可以假设当累加和为负时,潜在的最大子数组不可能从该元素或该元素左边的元素开始

假设1不证自明。
假设从A[1]累加到A[i]时第一次遇到其累加和为负(1<=i<=n),那么A[i]一定为负,且A[1]+...+A[i-1]>=0。当i<=2时,显然此时假设2成立。
当i>2时,可以认为在A[1]...A[i]中,所有子数组可分为三种:从A[1]开始向右拓展、从A[i]开始向左拓展以及不包含A[1]和A[i]的中间子数组;
显然从A[i]向左拓展的不可能是最大子数组,而如果不包含A[1]和A[i]的中间子数组是最大子数组,那么可以使该中间子数组加上其左边的部分构成一个新的子数组,而且该子数组总是大于等于这个中间子数组,因为其左边部分总是大于等于0,所以该情况下假设2也得证。综合来看假设2是成立的。
对于假设3,显然潜在的最大子数组不可能从A[i]开始,因为A[i]<0。当潜在的最大子数组从A[i]的左边开始时,假设其从A[j]开始(1<=j1时,A[j]+...+A[i]一定是负数,因为A[1]+...+A[j-1]一定大于等于0而A[1]+...+A[i]一定为负。所以综合来看,从A[i]或者A[i]的左边寻找潜在的子数组是没有意义的。
伪码如下,时间复杂度为Θ(n)。对于全部是负数的情况,特殊处理即可,不影响时间复杂度。
 LINEAR_SEARCH_MAX_SUBARRAY(A)
            sum = -infinity
            start = 0
            end = 0

            cur_sum = 0
            cur_start_index = 1

            i = 1
            for i <= A.length, inc by 1
                cur_sum += A[i]
                if cur_sum < 0
                    cur_sum = 0
                    cur_start_index = i + 1
                else
                    if sum < cur_sum
                        sum = cur_sum
                        start = cur_start_index
                        end = i

            return (start, end, sum)

java代码如下:

private static int[] linerSearchMaxSubArray(int[] nums) {
  int start = 0;
  int end = 0;
  int max = 0;
  int temp = 0;
  int ts = 0;
  for (int i = 0; i < nums.length; i++) {
    temp += nums[i];
    if (temp < 0) {
      ts = i + 1;
      temp = 0;
    } else {
      if (temp > max) {
        start = ts;
        end = i;
        max = temp;
      }
    }
  }
  return new int[]{start, end, max};
}

参考文章:https://www.cnblogs.com/SyBlog/p/11371922.html#commentform
参考书籍:《算法导论》

你可能感兴趣的