[算法]最大子序列和问题

问题:给定(可能有负数)整数A1,A2,A3,…,An,求∑j,k=i Ak的最大值。(为了降低难度,如果所有整数均为负数,则最大子序列和为0)

通俗的讲,这个问题其实就是要找到这么多数中,哪几个相邻的数加起来的值是最大的。

算法一:

  分析:如果把所有数二分一下变成前半部分和后半部分,则最大子序列可能出现三中情况。第一种,整个最大子序列都在前半部分;第二种,整个子序列都在后半部分;第三种,整个子序列跨越了前半部分的尾和后半部分的头。

  第一和第二种情况又可以分别二分一下,所以是可以用递归来求解的。第三种情况,既然是跨越了前半部分和后半部分的,所以肯定是包含了前半部分的最后一个数字和后半部分的第一个数字。我们先从前半部分的最后一个数字开始,往前倒序计算,找出前半部分包含最后一个数字时的最大和,记作M;然后我们再从后半部分的第一个数字开始,往后正序计算,找出后半部分包含第一个数字时的最大和,记作N;进而M+N即是第三种情况的解。

/**
   * 算法一
   * 寻找下标left~right之间的最优解
   * @param array
   * @param left
   * @param right
   * @return
   */
  private static int maxSubPart(int[] array, int left, int right) {
    
    // 二分到最后只剩一个数字时,此处是递归的基准,没有基准递归将无法终止。
    if (left == right) {
      if (array[left] > 0) {
        return array[left];
      } else {
        return 0; // 问题说明中降低了难度,如果所有数字都是负数时,解就为0
      }
    }
    
    int center = (left + right) / 2;  // 二分
    // 假设的第一种情况,最大和在前半部分,递归求解
    int maxLeftPartSum = maxSubPart(array, left, center);
    // 假设的第二种情况,最大和在后半部分,递归求解
    int maxRightPartSum = maxSubPart(array, center + 1, right);
    
    // 假设的第三种情况,最大和跨越前半部分和后半部分
    // 求解M
    int maxLeftBorderSum = 0;
    int leftBorderSum = 0;
    for (int i = center; i >= left; i--) {
      leftBorderSum += array[i];
      if (leftBorderSum > maxLeftBorderSum) {
        maxLeftBorderSum = leftBorderSum;
      }
    }
    
    // 求解N
    int maxRightBorderSum = 0;
    int rightBorderSum = 0;
    for (int i = center + 1; i < right; i++) {
      rightBorderSum += array[i];
      if (rightBorderSum > maxRightBorderSum) {
        maxRightBorderSum = rightBorderSum;
      }
    }
    
    // 比较三种情况的解,其中最大的和即为问题的最终解
    return maxOf3(maxLeftPartSum, maxRightPartSum, maxLeftBorderSum + maxRightBorderSum);
  }

求解三个数中的最大数

/**
 * 求三个数中的最大数
 * @param param1
 * @param param2
 * @param param3
 * @return
 */
private static int maxOf3(int param1, int param2, int param3) {
  int max = param1 > param2 ? param1 : param2;
  max = max > param3 ? max : param3;
  return max;
}

算法二:

  分析:假设最大序列和是所有数中第i~j个数字,那么a[i]必然不是负数,因为如果a[i]是负数的话,那a[i]~a[j]的和肯定小于a[i+1]~a[j]的和,所以最优解的第一个数字只能是正数,同理,任何负的子序列都不可能是最大子序列和的前缀。

  通俗的讲,我们可以从第一个正数字开始往后累加,假设是从第i个开始累加,如果累加到第j个数字时子序列的和变成负数了,此时该子序列已不可能是最优解的前缀了,那么我们就可以从第j+1个数字开始重新累加了,为什么是从第j+1个开始呢?因为从i累加到j-1时和还是正数,是在加了j之后才变成的负数,所以我们从i到j-1之间的任何一个数开始重新累加到j的话和依然会是负数,依然不会是最优解,所以就能大胆的从第j+1个开始重新累加了。

private static int maxSubSum(int[] array) {
  int maxSum = 0;
  int currentSum = 0;
  
  for (int i = 0; i < array.length; i++) {
    currentSum += array[i];
    if (currentSum > maxSum) {
      maxSum = currentSum;  // 把累加到的最大和记下来
    } else if (currentSum < 0) {  // 当前子序列的和变成负数了
      currentSum = 0; // 重下一个数字开始重新累加
    }
  }
  
  return maxSum;
}

大数据计算量来看的话,算法二的效率是高于算法一的。

全部代码:

public class MaxSubSum {

  private static final int[] ARRAY_PARAM_A = new int[]{-2, 11, -4, 13, -5, -2};
  private static final int[] ARRAY_PARAM_B = new int[]{4, -3, 5, -2, -1, 2, 6, -2};
  
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    solutionOne();
    solutionTwo();
  }

  /**
   * 算法一
   * 寻找下标left~right之间的最优解
   * @param array
   * @param left
   * @param right
   * @return
   */
  private static int maxSubPart(int[] array, int left, int right) {
    
    // 二分到最后只剩一个数字时,此处是递归的基准,没有基准递归将无法终止。
    if (left == right) {
      if (array[left] > 0) {
        return array[left];
      } else {
        return 0; // 问题说明中降低了难度,如果所有数字都是负数时,解就为0
      }
    }
    
    int center = (left + right) / 2;  // 二分
    // 假设的第一种情况,最大和在前半部分,递归求解
    int maxLeftPartSum = maxSubPart(array, left, center);
    // 假设的第二种情况,最大和在后半部分,递归求解
    int maxRightPartSum = maxSubPart(array, center + 1, right);
    
    // 假设的第三种情况,最大和跨越前半部分和后半部分
    // 求解M
    int maxLeftBorderSum = 0;
    int leftBorderSum = 0;
    for (int i = center; i >= left; i--) {
      leftBorderSum += array[i];
      if (leftBorderSum > maxLeftBorderSum) {
        maxLeftBorderSum = leftBorderSum;
      }
    }
    
    // 求解N
    int maxRightBorderSum = 0;
    int rightBorderSum = 0;
    for (int i = center + 1; i < right; i++) {
      rightBorderSum += array[i];
      if (rightBorderSum > maxRightBorderSum) {
        maxRightBorderSum = rightBorderSum;
      }
    }
    
    // 比较三种情况的解,其中最大的和即为问题的最终解
    return maxOf3(maxLeftPartSum, maxRightPartSum, maxLeftBorderSum + maxRightBorderSum);
  }
  
  /**
   * 求三个数中的最大数
   * @param param1
   * @param param2
   * @param param3
   * @return
   */
  private static int maxOf3(int param1, int param2, int param3) {
    int max = param1 > param2 ? param1 : param2;
    max = max > param3 ? max : param3;
    return max;
  }
  
  private static void solutionOne() {
    int solutionOneForA = maxSubPart(ARRAY_PARAM_A, 0, ARRAY_PARAM_A.length -1);
    System.out.println("[算法一]求解A: " + solutionOneForA);
    int solutionOneForB = maxSubPart(ARRAY_PARAM_B, 0, ARRAY_PARAM_B.length -1);
    System.out.println("[算法一]求解B: " + solutionOneForB);
  }
  
  private static int maxSubSum(int[] array) {
    int maxSum = 0;
    int currentSum = 0;
    
    for (int i = 0; i < array.length; i++) {
      currentSum += array[i];
      if (currentSum > maxSum) {
        maxSum = currentSum;  // 把累加到的最大和记下来
      } else if (currentSum < 0) {  // 当前子序列的和变成负数了
        currentSum = 0; // 重下一个数字开始重新累加
      }
    }
    
    return maxSum;
  }
  
  private static void solutionTwo() {
    int solutionTwoForA = maxSubSum(ARRAY_PARAM_A);
    System.out.println("[算法二]求解A: " + solutionTwoForA);
    int solutionTwoForB = maxSubSum(ARRAY_PARAM_B);
    System.out.println("[算法二]求解B: " + solutionTwoForB);
  }
}

输出结果:

[算法一]求解A: 20
[算法一]求解B: 11
[算法二]求解A: 20
[算法二]求解B: 11

点赞