贪心的小包 | 豆包MarsCode AI刷题

112 阅读5分钟

问题背景:

给定一个整数数组 data,长度为 N,该数组可能被重复 M 次。我们需要计算从这个重复的数组中找到一个子数组,使其和最大。这个问题的关键挑战是如何高效处理数组被重复多次的情况,以及如何在跨多个数组时找到最大子数组和。

解决思路:

1. 最大子数组和问题(Kadane 算法)

这是一个经典的问题,目标是找到一个数组中的子数组,使其元素和最大。这个问题可以通过 Kadane 算法 来解决,Kadane 算法的核心思想是动态地更新当前子数组的和,在遍历过程中维护一个最大和。

Kadane 算法的步骤

  • 我们从数组的第一个元素开始,初始化当前子数组的和 currentSum 为第一个元素,最大和 maxSum 也为第一个元素。
  • 遍历数组的每个元素,对于每个元素,我们决定是将当前元素与已有的子数组和继续累加,还是从当前元素重新开始一个新的子数组。如果累加的和小于当前元素,则说明从当前元素重新开始一个子数组可能更优。
  • 每一步,我们更新最大子数组和 maxSum,确保它保存到目前为止的最大子数组和。

Kadane 算法的时间复杂度O(N),因为我们只需要遍历一次数组。

2. 前缀和和后缀和

在考虑数组重复的情况下,除了单个数组的最大子数组和之外,我们还需要考虑跨越多个数组的情形。为此,我们需要计算数组的前缀和(从左到右的最大和)和后缀和(从右到左的最大和)。

  • 前缀和:我们从数组的最左端开始,逐步累加并更新当前的最大和,这个最大和代表了数组的前缀部分的最大子数组和。
  • 后缀和:类似地,我们从数组的最右端开始,逐步累加并更新当前的最大和,代表了数组的后缀部分的最大子数组和。

这两个值对跨越多个数组时非常重要。比如,当数组被重复两次时,我们可以利用第一个数组的后缀和和第二个数组的前缀和来构造一个新的子数组和。

3. 跨越多个数组的情形

M > 1 时,数组重复了多次,我们不仅仅考虑单个数组的最大子数组和,还需要考虑多个数组的组合。具体来说:

  • 对于跨越两个数组的情形,我们可以考虑一个数组的前缀部分和另一个数组的后缀部分结合起来,这样形成一个跨越两个数组的子数组。
  • 对于 M > 2 的情况,我们可以加上 M-2 个完整的数组的总和来进一步优化我们的解法。

具体的计算方式如下:

  • 单个数组最大子数组和:我们已经通过 Kadane 算法计算出来了。
  • 前缀和:从左到右累加并更新最大值。
  • 后缀和:从右到左累加并更新最大值。
  • 多个数组的总和:如果 M > 2,则可以将跨越多个数组的最大和表示为前缀和、后缀和,以及中间多个数组的总和。

4. 特殊情况

  • 最大子数组和为负数的情况: 如果数组中的所有元素都是负数或者数组包含负数且其他元素的和不能弥补负数的影响,Kadane 算法计算出的最大子数组和可能为负数。例如:  如果数组是 [-5, -3, -2, -1],Kadane 算法会计算出最大的子数组和为 -1(即选取最小的负数)。这种情况下,最大的子数组和是负数。直接返回-1即可。
  • 如果 M = 1,我们只考虑单个数组的最大子数组和。
  • 如果 M > 1,则需要考虑跨越两个数组甚至更多数组的情况。

实现过程:

下面是对以上思路的详细实现:


public class Main {
    public static int solution(int N, int M, int[] data) {
        // 1. 计算数组的最大子数组和 (Kadane's algorithm)
        int maxSingleArraySum = kadane(data);
        if (maxSingleArraySum<=0){
            return maxSingleArraySum;
    }
        // 2. 计算S数组的前缀和与后缀和
        int prefixSum = 0, suffixSum = 0;
        int totalSum = 0; // S数组的总和
        for (int i = 0; i < N; i++) {
            totalSum += data[i];
            if (prefixSum < totalSum)
                prefixSum = totalSum;
        }

        totalSum = 0;
        for (int i = N - 1; i >= 0; i--) {
            totalSum += data[i];
            if (suffixSum < totalSum)
                suffixSum = totalSum;
        }

        // 3. 对于M > 1的情况,考虑跨越两段的情况
        int result = maxSingleArraySum;

        if (M > 1) {
            result = Math.max(result, prefixSum + suffixSum); // 两次S的前后缝合
            if (M > 2) {
                result = Math.max(result, prefixSum + suffixSum + (M - 2) * totalSum);
            }
        }
        return result;
    }

    public static int kadane(int[] arr) {
        int maxSum = arr[0];
        int currentSum = arr[0];

        for (int i = 1; i < arr.length; i++) {
            currentSum = Math.max(arr[i], currentSum + arr[i]);
            maxSum = Math.max(maxSum, currentSum);
        }

        return maxSum;
    }

    public static void main(String[] args) {
        System.out.println(solution(5, 1, new int[] { 1, 3, -9, 2, 4 }) == 6);
        System.out.println(solution(5, 3, new int[] { 1, 3, -9, 2, 4 }) == 11);
    }
}

代码解释:

  1. Kadane 算法kadane(data) 用来计算单个数组的最大子数组和。它遍历数组并通过动态规划的方式找到最大子数组和。
  2. 前缀和与后缀和:通过两次遍历数组,分别计算前缀和(prefixSum)和后缀和(suffixSum)。这些值是用来处理跨越多个数组时的情况。
  3. 跨越多个数组的情况:我们利用 prefixSumsuffixSum 来处理跨越多个数组的情况。如果 M > 1,我们将前缀和与后缀和相加,得到跨越两个数组的最大和。如果 M > 2,我们还可以考虑其他数组的总和。

时间复杂度:

  • Kadane 算法O(N),因为我们只需要遍历一次数组。
  • 计算前缀和与后缀和O(N),各自遍历一次数组。
  • 因此,总的时间复杂度为 O(N),其中 N 是数组 data 的长度。

结论:

这个解法通过利用 Kadane 算法和前缀、后缀和的技巧,能够在 O(N) 时间复杂度内解决跨越多个数组的最大子数组和问题。