目录

题目描述

918. 环形子数组的最大和

题意分析

数组首尾相接成环,要在环上取一段连续且非空的元素使和最大。与线性版本的唯一区别是:这段元素允许从数组尾部绕回头部,例如在 [5,-3,5] 上可以取到末尾的 5 和开头的 5,答案是 10。同时题目明确规定每个下标最多被取一次,也就是绕回来之后不能继续往前吃掉已经取过的元素。

元素可以是负数(范围 -3 * 10^43 * 10^4),这意味着"和越加越大"的直觉不成立,任何依赖单调性的窗口做法都不适用;而 $n$ 最大到 $3 \times 10^4$,把数组复制一份变成长度 $2n$ 再暴力枚举所有起点终点是 $O(n^2)$,在这个规模下已经很危险,约束在提示我们要找一个一次扫描就能出结果的做法。

所有元素都可能是负数,这时最优解只能是"损失最小的那一个元素",而不是空集——非空的要求让这种全负输入成为必须单独讨论的边界。除此之外还要注意长度为 1 的数组,此时唯一的答案就是那个元素本身。

解法:最大子数组和 + 最小子数组和

核心思路

问题关键: 环形连续子数组只有两种形态:不跨首尾、跨首尾。前者就是普通最大子数组和;后者的补集是原数组中间的一段连续子数组。

为什么用双 Kadane: 设数组总和为 total。不跨首尾的候选是最大子数组和 maxSum;跨首尾等价于挖掉一段和最小的中间区间,候选为 total - minSum。两个极值都能一趟计算。

状态与不变量: curMaxcurMin 分别表示“必须以当前元素结尾”的最大和、最小和;每步只需在“接上前一段”和“从当前元素重开”之间选择。maxSumminSum 记录截至当前的全局极值。

正确性: 两类子数组覆盖了全部可能,分别取到最优值后再取较大者就是全局最优。唯一例外是全负数组:此时 minSum == totaltotal - minSum 等于 0,实际表示删掉整个数组。题目要求子数组非空,因此必须返回 maxSum

解题步骤

  1. 初始化总和、两组当前状态以及两组全局极值;全局极值用 nums[0] 初始化,保证答案非空。
  2. 扫描每个 num,累加 total,同时更新以 num 结尾的最大和、最小和及其全局极值。
  3. maxSum < 0,说明所有元素都为负,直接返回 maxSum
  4. 否则返回 max(maxSum, total - minSum)

口述示例: [5,-3,5]total = 7maxSum = 7minSum = -3,跨首尾候选为 7 - (-3) = 10,所以答案是 10。

边界反例: [-3,-2,-3]total - minSum = 0,它表示把整个数组删光,并非合法子数组;正确答案是 maxSum = -2

代码实现

class Solution {
    public int maxSubarraySumCircular(int[] nums) {
        int total = 0;
        int curMax = 0;
        int curMin = 0;
        int maxSum = nums[0];
        int minSum = nums[0];

        for (int num : nums) {
            total += num;
            curMax = Math.max(curMax + num, num);
            maxSum = Math.max(maxSum, curMax);
            curMin = Math.min(curMin + num, num);
            minSum = Math.min(minSum, curMin);
        }

        if (maxSum < 0) {
            return maxSum;
        }
        return Math.max(maxSum, total - minSum);
    }
}
func maxSubarraySumCircular(nums []int) int {
    total, curMax, curMin := 0, 0, 0
    maxSum, minSum := nums[0], nums[0]

    for _, num := range nums {
        total += num
        curMax = maxCircular(curMax+num, num)
        maxSum = maxCircular(maxSum, curMax)
        curMin = minCircular(curMin+num, num)
        minSum = minCircular(minSum, curMin)
    }

    if maxSum < 0 {
        return maxSum
    }
    return maxCircular(maxSum, total-minSum)
}

func maxCircular(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func minCircular(a, b int) int {
    if a < b {
        return a
    }
    return b
}

复杂度分析

  • 时间复杂度:$O(n)$,数组只扫描一次。
  • 空间复杂度:$O(1)$,只维护若干标量。

关键点总结

  • 环形区间按“是否跨首尾”分类,跨首尾解用补集转化。
  • Kadane 的局部状态必须限定“以当前元素结尾”,不能与全局极值混淆。
  • 最大和、最小和可在同一趟扫描中对称维护。
  • 面试时要主动说明全负特判:它排除了“挖掉整个数组”的非法空集。

易错点总结

  • 漏掉全负特判:[-3,-2,-3] 会错返空集的和 0,而非 -2。
  • 将全局极值初始化为 0:[-1] 会被错判为 0。
  • 把跨首尾候选写成 total - maxSum;应当减去最小连续子数组。
  • 复制两份数组后直接跑 Kadane:若不限制区间长度,同一下标可能被取两次。

相似题目

题目 难度 考察点
53. 最大子数组和 中等 线性版本,只需要一套 Kadane 状态,没有接缝和补集讨论
剑指 Offer 42. 连续子数组的最大和 简单 与 53 同题,适合用来把"必须以当前元素结尾"的状态定义练熟
面试题 16.17. 连续数列 简单 同样是线性 Kadane,常被用来考察全负数组的边界处理
1186. 删除一次得到子数组最大和 中等 状态多出"是否已删除一个元素"这一维,转移从一条变成两条
1191. K 次串联后最大子数组之和 中等 数组重复 K 次而非成环,要按总和正负分类并对中间段做整段累加