[LeetCode] 1031: Maximum sum of two non-overlapping subarrays

原题目

Given an array A of non-negative integers, return the maximum sum of elements in two non-overlapping (contiguous) subarrays, which have lengths L and M. (For clarification, the L-length subarray could occur before or after the M-length subarray.)

Formally, return the largest V for which V = (A[i] + A[i+1] + ... + A[i+L-1]) + (A[j] + A[j+1] + ... + A[j+M-1]) and either:

  • 0 <= i < i + L - 1 < j < j + M - 1 < A.length, or
  • 0 <= j < j + M - 1 < i < i + L - 1 < A.length.

Example 1:

Input: A = [0,6,5,2,2,5,1,9,4], L = 1, M = 2
Output: 20
Explanation: One choice of subarrays is [9] with length 1, and [6,5] with length 2.
复制代码

Example 2:

Input: A = [3,8,1,3,2,1,8,9,0], L = 3, M = 2
Output: 29
Explanation: One choice of subarrays is [3,8,1] with length 3, and [8,9] with length 2.
复制代码

Example 3:

Input: A = [2,1,5,6,0,9,5,0,3,8], L = 4, M = 3
Output: 31
Explanation: One choice of subarrays is [5,6,0,9] with length 4, and [3,8] with length 3.
复制代码

题目翻译

给出一个正整数数组 A,并给出两个数值 L、M; 求这个数组中长度为 L 和 M 的,且无重复元素的子数组的元素之和的最大值。

也就是求 V = (A[i] + A[i+1] + ... + A[i+L-1]) + (A[j] + A[j+1] + ... + A[j+M-1]) 的最大值,其中

  • (A[i] + A[i+1] + ... + A[i+L-1]) 是长度为 L 的子数组
  • (A[j] + A[j+1] + ... + A[j+M-1]) 是长度为 M 的子数组

并且 i、j、L、M 满足:

  • 0 <= i < i + L - 1 < j < j + M - 1 < A.length, or
  • 0 <= j < j + M - 1 < i < i + L - 1 < A.length.

答案 (JavaScript)

/**
 * @param {number[]} A
 * @param {number} L
 * @param {number} M
 * @return {number}
 */
var maxSumTwoNoOverlap = function(A, L, M) {
  const prefixSum = [], length = A.length
  prefixSum[0] = A[0]
  for (let i=1; i<length; i++) {
    prefixSum[i] = prefixSum[i-1] + A[i]
  }
  if (L + M === length){
    return prefixSum[length - 1];
  }
  return Math.max(split_and_find_max_sum(A, prefixSum, L, M), 
                  split_and_find_max_sum(A, prefixSum, M, L));
};


var split_and_find_max_sum = (A, prefixSum, L, M) => {
  const length = A.length
  const leftMax = new Array(length), rightMax = new Array(length);
  for (let i = L-1; i<length; i++) {
    const tmpSum = prefixSum[i] - prefixSum[i - L + 1] + A[i - L + 1];
    if (i === L - 1) {
      leftMax[i] = tmpSum;
    } else {
      leftMax[i] = Math.max(leftMax[i - 1], tmpSum);
    }
  }
  
  for (let i = length - M; i >= 0; i--) {
    const tmpSum = prefixSum[i + M - 1] - prefixSum[i] + A[i];
    if (i === length - M) {
      rightMax[i] = tmpSum;
    } else {
      rightMax[i] = Math.max(rightMax[i + 1], tmpSum);
    }
  }
  
  let sum = -Infinity
  for (let i = L - 1; i < length - M; i++) {
    sum = Math.max(sum, leftMax[i] + rightMax[i + 1]);
  }
  return sum
}
复制代码

代码解析

如何高效的求子数组的和

这道题需要找到子数组元素和的最大值,那么肯定就需要求很多很多很多不同子数组的元素和,然后进行比对。

每次都将选出来的子数组元素逐一相加肯定是不合算的。

这里的方法是,建立一个 prefixSum 数组:

const prefixSum = [];
prefixSum[0] = A[0];
for (let i=1; i<length; i++) {
  prefixSum[i] = prefixSum[i-1] + A[i]
}
复制代码

这样,对于数组 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],如果我们想求 2-7 这个子数组的和,只需要用 prefixSum[6] - prefixSum[0] 就行了。

如何避免子数组重叠

这道题最容易想到的方法其实就是 —— 暴力搜索:

即,将所有长度为 L 和 M 的子数组都找出来,两两配对,每一对都判断一下它们是不是重合,如果重合就舍弃;如果没有重合部分,就计算这两个数组的和,然后找出最大值即可。

但是,效率真的低啊。。。虽然打着开心的 Accepted,但是。。。

Runtime: 
100 ms, faster than 16.16% of JavaScript online submissions 
for Maximum Sum of Two Non-Overlapping Subarrays.
复制代码

不爽 =。=

所以该怎么办呢?

不用暴力搜索,那么就要聪明的搜索。 这道题有个很关键的可以「聪明搜索」的点,就是 non-overlapping:既然必须是无重合的数组对,我们一开始只需要寻找无重合的配对来求和就可以了。

如何寻找?

...怎么才能让同桌的领地和自己的领地不要重合呢?...画个三八线呗 =。=

同理,在数组中找一条**「分界线」**,在这条线两边分别寻找子数组,并让每个子数组的求和尽可能大。 最后找到所有分界情况下的子数组求和的最大值,就是答案了。

例如:有一个数组,[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],选取数字 4,5 之间作为分界线。那么两个子数组 C、D 分别在 [1, 2, 3, 4][5, 6, 7, 8, 9, 10] 中寻找,就一定能保证数组 C、D 不重合。同理,我们还可以把分界线选在 6,7 之间...

这样,比暴力搜索就节省了很多次无用的比较。

函数 split_and_find_max_sum 其实就是在做画分割线 & 寻找最大可能的和这件事情。

不过在函数中,它先分别从前往后(从后往前)计算了所有可能分界线的左边(右边)可能子元素的最大值。

这个是从前往后找:

  for (let i = L-1; i<length; i++) {
      const tmpSum = prefixSum[i] - prefixSum[i - L + 1] + A[i - L + 1];
      if (i === L - 1) {
          leftMax[i] = tmpSum;
      } else {
          leftMax[i] = Math.max(leftMax[i - 1], tmpSum);
      }
  }
复制代码

这个 leftMax 就可以认为是,当分界线选了 i,i+1 之间的时候,分界线左边所有长度为 L 的子数组元素求和的最大值

这个是从后往前找:

  for (let i = length - M; i >= 0; i--) {
      const tmpSum = prefixSum[i + M - 1] - prefixSum[i] + A[i];
      if (i === length - M) {
        rightMax[i] = tmpSum;
      } else {
        rightMax[i] = Math.max(rightMax[i + 1], tmpSum);
      }
  }
复制代码

rightMax 和 leftMax 基本同理。只不过 rightMax 选子数组的范围是线右边,同时数组长度是 M。

最后一轮 for 循环才是在比对所有分界线情况,并找出最大值。

  let sum = -Infinity
  for (let i = L - 1; i < length - M; i++) {
    sum = Math.max(sum, leftMax[i] + rightMax[i + 1]);
  }
  return sum
复制代码

函数 split_and_find_max_sum 需要调用两次,原因是两个子数组长度可能不同,谁在线左边,谁在线右边,需要分情况讨论。

转载于:https://juejin.im/post/5d0b66b75188253c7a4b1e80

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值