题目描述

✅ 689. 三个无重叠子数组的最大和

image-20260929104540252

题意分析

选择三个长度都为 k、互不重叠的子数组,使总和最大,返回从左到右排列的三个起始下标。总和相同时,需要返回字典序最小的下标组合。

直接枚举三个窗口太慢。固定中间窗口以后,左窗口只能出现在它左侧,右窗口只能出现在它右侧,两边可以独立选取最大窗口和,因此只需预处理每个前缀、后缀里的最优窗口。

解法:窗口和 + 两侧最优预处理

核心思路

[!blue]

先用滑动窗口得到 sums[i],表示从 i 开始、长度为 k 的子数组和。共有 n-k+1 个窗口;移动起点时,减去离开的左端元素,加上新进入的右端元素即可。

left[i] 保存起点在 0..i 中的最大和窗口起点,平局选更小下标;right[i] 保存起点在 i..n-k 中的最大和窗口起点,同样平局选更小下标。正向构造 left 时,旧候选下标更小,所以只在新窗口和严格更大时更新;反向构造 right 时,当前下标更小,所以相等时也要更新。

设中间窗口起点为 j。为了不重叠,左窗口起点最多是 j-k,右窗口起点至少是 j+k,于是取 l = left[j-k]、r = right[j+k]。两侧选择都已与中间隔开,也就不会彼此重叠;各取最大和能得到固定 j 下的最大总和。枚举 k <= j <= n-2k 就能覆盖所有合法中间位置。

最终答案也要正确处理平局。中间位置 j 按升序枚举,而 left[j-k] 只会保持不变或更新为更靠右的起点,绝不会变小。因此两个总和相同的候选中,先遇到的左起点不大于后者;左起点相同时,先遇到的中间起点更小,所以先遇到的组合字典序一定更小。只在总和严格增大时更新答案即可。

对同一个中间位置,两侧表又已经分别保留最小的最优起点,尤其在左、中起点固定后,右表保证第三个下标最小。三处平局规则合在一起,才保证完整答案的字典序。

解题步骤

  1. 计算全部长度为 k 的窗口和。
  2. 从左到右建立前缀最优起点表,只在窗口和严格更大时替换候选。
  3. 从右到左建立后缀最优起点表,窗口和大于或等于旧候选时都替换。
  4. 升序枚举中间起点 j,用 left[j-k] 和 right[j+k] 取得互不重叠的两侧窗口。
  5. 三窗口总和严格超过当前最大值时保存三个零基起点;相等则保留已有答案。题目保证 3k <= n,至少存在一个合法组合。

代码实现

class Solution {
    public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int n = nums.length;
        int[] sums = new int[n - k + 1];

        int windowSum = 0;

        for (int i = 0; i < k; i++) {
            windowSum += nums[i];
        }

        sums[0] = windowSum;

        for (int i = 1; i <= n - k; i++) {
            windowSum += nums[i + k - 1] - nums[i - 1];
            sums[i] = windowSum;
        }

        int[] left = new int[sums.length];
        int bestLeft = 0;

        for (int i = 0; i < sums.length; i++) {
            // 正向扫描遇到同和保留旧起点,保证左侧起点更小。
            if (sums[i] > sums[bestLeft]) {
                bestLeft = i;
            }

            left[i] = bestLeft;
        }

        int[] right = new int[sums.length];
        int bestRight = sums.length - 1;

        for (int i = sums.length - 1; i >= 0; i--) {
            // 反向扫描遇到同和更新为当前起点,保证右侧起点更小。
            if (sums[i] >= sums[bestRight]) {
                bestRight = i;
            }

            right[i] = bestRight;
        }

        int[] answer = new int[] {
            -1,
            -1,
            -1
        };
        int maxSum = -1;

        for (int j = k; j + k < sums.length; j++) {
            // 左右候选分别与中间窗口至少间隔一个窗口长度。
            int l = left[j - k];
            int r = right[j + k];
            int total = sums[l] + sums[j] + sums[r];

            if (total > maxSum) {
                maxSum = total;
                answer[0] = l;
                answer[1] = j;
                answer[2] = r;
            }
        }

        return answer;
    }
}
func maxSumOfThreeSubarrays(nums []int, k int) []int {
    n := len(nums)
    sums := make([]int, n-k+1)

    windowSum := 0
    for i := 0; i < k; i++ {
        windowSum += nums[i]
    }
    sums[0] = windowSum
    for i := 1; i <= n-k; i++ {
        windowSum += nums[i+k-1] - nums[i-1]
        sums[i] = windowSum
    }

    left := make([]int, len(sums))
    bestLeft := 0
    for i := 0; i < len(sums); i++ {
        // 正向扫描遇到同和保留旧起点,保证左侧起点更小。
        if sums[i] > sums[bestLeft] {
            bestLeft = i
        }
        left[i] = bestLeft
    }

    right := make([]int, len(sums))
    bestRight := len(sums) - 1
    for i := len(sums) - 1; i >= 0; i-- {
        // 反向扫描遇到同和更新为当前起点,保证右侧起点更小。
        if sums[i] >= sums[bestRight] {
            bestRight = i
        }
        right[i] = bestRight
    }

    result := []int{
        -1,
        -1,
        -1,
    }
    maxSum := -1
    for j := k; j+k < len(sums); j++ {
        // 左右候选分别与中间窗口至少间隔一个窗口长度。
        l := left[j-k]
        r := right[j+k]
        total := sums[l] + sums[j] + sums[r]

        if total > maxSum {
            maxSum = total
            result[0] = l
            result[1] = j
            result[2] = r
        }
    }

    return result
}

复杂度分析

  • 时间复杂度:$O(n)$。窗口和、左右最优表及中间位置枚举都只进行线性扫描。
  • 空间复杂度:$O(n)$。保存窗口和以及左右最优起点数组,答案固定为三个下标。

关键点总结

[!green]

  • 固定中间窗口后,两侧互不影响,分别取最大窗口和就是这一中间位置的最优解。
  • 非重叠条件限制的是窗口起点:左端不超过 j-k,右端不小于 j+k。
  • 左表使用严格大于、右表使用大于等于,都是为了在扫描方向不同的情况下保留更小起点。
  • 中间起点升序、最优左起点不后退,使总和平局时保留旧答案可以保证整体字典序。

易错点总结

[!yellow]

  • 直接取三个最大窗口,它们可能互相重叠,无法组成合法答案。
  • 左表查到 j,或右表从 j+1 开始查,没有为中间窗口留出完整的 k 个位置。
  • 右表反向扫描只在严格更大时更新,会让平局候选停留在更靠右的位置。
  • 左表同和时也更新,会把更小起点换成更大起点。
  • 最终总和相等时覆盖答案,会丢掉先前字典序更小的组合。
  • maxSum = -1 依赖题目中元素为正的范围;它能保证第一个合法组合一定写入答案。

相似题目

题目 难度 关联与区别
1031. 两个无重叠子数组的最大和 中等 同样选择互不重叠的固定长度窗口,原题选两段,本题选三段并处理下标字典序。
1235. 规划兼职工作 困难 同样最大化不重叠区间收益,本题窗口等长且只选三段,可用左右最优数组简化通用区间DP。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/11776921
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!