全栈开发知识库

90.环形子数组的最大和

题目

给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 。

环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。

子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。

示例 1:

  • 输入:nums = [1,-2,3,-2]
  • 输出:3
  • 解释:从子数组 [3] 得到最大和 3

示例 2:

  • 输入:nums = [5,-3,5]
  • 输出:10
  • 解释:从子数组 [5,5] 得到最大和 5 + 5 = 10

示例 3:

  • 输入:nums = [3,-2,2,-3]
  • 输出:3
  • 解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3

提示:

  • n == nums.length
  • 1 <= n <= 3 * 104
  • -3 * 104 <= nums[i] <= 3 * 104

思路

"环形"的意思是:下标 n-1 的下一个是下标 0,所以子数组可以跨过末尾绕回开头。

nums = [5, -3, 5]

不绕圈:5 + (-3) + 5 = 7 绕圈: 末尾的 5 → 绕回 → 开头的 5 = 10 ✅ 答案

但有一条硬约束:每个元素最多只能用一次,也就是子数组长度不能超过 n。不能绕两圈把 5 数两遍。

面对环形,硬去枚举"从哪开始、绕多远"会很乱。正确的第一步是分类:

最优子数组要么绕圈,要么不绕圈,没有第三种可能。

情况 A:不跨越边界 [ . . ■ ■ ■ . . ] 就是普通数组的最大子数组和 → 53 题的 Kadane └─────┘

情况 B:跨越边界 [ ■ ■ . . . ■ ■ ] 由一段前缀 + 一段后缀组成 └─┘ └─┘

情况 A 直接调用上一题的 Kadane 就完事了。问题全在情况 B。

情况 B 的关键洞察:看它的"补集"

前缀 + 后缀这个形状不好直接算。但请注意它剩下的那部分:

下标: 0 1 2 3 4 5 [ ■ ■ □ □ ■ ■ ] └─前缀─┘ └─后缀─┘ └─中间─┘

被挖掉的"中间那段",恰好是一个普通的、连续的、不绕圈的子数组。

于是:

绕圈子数组的和 = 数组总和 − 中间那段的和

想让左边最大,就要让"中间那段的和"最小。而"求最小连续子数组和"——就是把 Kadane 里的 max 全换成 min,一模一样的算法。

于是整道题的解法浮现了:

▎ 一次遍历,同时算出三样东西:总和 total、最大子数组和 bestMax、最小子数组和 bestMin。 ▎ ▎ 答案 = max(bestMax, total - bestMin)

一个 O(n) 的循环解决,不需要真的去处理环形。

手动走一遍 [5, -3, 5]

┌─────┬─────┬────────────────┬─────────┬─────────────────┬─────────┬───────┐ │ i │ x │ curMax │ bestMax │ curMin │ bestMin │ total │ ├─────┼─────┼────────────────┼─────────┼─────────────────┼─────────┼───────┤ │ 0 │ 5 │ 5 │ 5 │ 5 │ 5 │ 5 │ ├─────┼─────┼────────────────┼─────────┼─────────────────┼─────────┼───────┤ │ 1 │ -3 │ max(-3, 2) = 2 │ 5 │ min(-3, 2) = -3 │ -3 │ 2 │ ├─────┼─────┼────────────────┼─────────┼─────────────────┼─────────┼───────┤ │ 2 │ 5 │ max(5, 7) = 7 │ 7 │ min(5, 2) = 2 │ -3 │ 7 │ └─────┴─────┴────────────────┴─────────┴─────────────────┴─────────┴───────┘

  • 情况 A:bestMax = 7
  • 情况 B:total - bestMin = 7 - (-3) = 10

答案 max(7, 10) = 10 ✅

bestMin = -3 对应的是中间那个 -3,把它挖掉剩下的正是首尾两个 5 —— 和我们预期的绕圈答案完全对上。

四、必须特判的坑:全是负数

这个公式有一个漏洞。看 [-3, -2, -3]:

  • total = -8
  • bestMax = -2(只选那个最大的负数)
  • bestMin = -8(整个数组就是最小子数组)
  • total - bestMin = -8 - (-8) = 0

于是公式给出 max(-2, 0) = 0。但 0 是不合法的——它对应的是"把整个数组都挖掉",剩下一个空子数组。而题目要求非空。正确答案是 -2。

问题的本质: 当 bestMin 恰好等于整个数组的和时,情况 B 的"中间段"吞掉了一切,前缀和后缀都空了。

修法(一行):

if (bestMax < 0) return bestMax

为什么这一行就够? 因为 bestMax < 0 等价于"数组里全是负数"(只要有一个元素 ≥ 0,bestMax 就至少是它,不可能为负)。而全负数时,最优答案就是那个最大的负数,也就是 bestMax。

反过来,只要有一个元素 ≥ 0,那么 bestMax ≥ 0,此时哪怕公式算出了那个虚假的 0,max(bestMax, 0) = bestMax 也不会被它污染。所以这个特判是严密的,不是打补丁。

  class Solution {

      /**
       * 环形子数组的最大和
       *
       * 核心思想:答案只有两种形态 ——
       *   A) 不跨越边界 → 就是普通的最大子数组和(Kadane)
       *   B) 跨越边界(前缀 + 后缀)→ 它的补集"中间那段"是一个普通连续子数组,
       *      所以此时和 = total - 最小子数组和
       *   答案 = max(A, B)
       *
       * 时间复杂度 O(n):一次遍历同时算出三个量
       * 空间复杂度 O(1):只用几个变量
       */
      fun maxSubarraySumCircular(nums: IntArray): Int {
          var total = 0                    // 数组总和

          // 一组跑最大(Kadane 原版)
          var curMax = 0                   // 以当前元素结尾的最大子数组和
          var bestMax = Int.MIN_VALUE      // 全局最大子数组和

          // 一组跑最小(把 max 换成 min,完全对称)
          var curMin = 0                   // 以当前元素结尾的最小子数组和
          var bestMin = Int.MAX_VALUE      // 全局最小子数组和

          for (x in nums) {
              total += x

              // 决策同 53 题:接着前面那段继续,还是从我这里重开一段
              curMax = maxOf(x, curMax + x)
              bestMax = maxOf(bestMax, curMax)

              // 镜像逻辑:前面累积和是正数就是拖累,甩掉重开
              curMin = minOf(x, curMin + x)
              bestMin = minOf(bestMin, curMin)
          }

          // ⚠️ 必须特判:bestMax < 0 等价于"全是负数"。
          //    此时 bestMin 就是 total,total - bestMin = 0 对应空子数组,非法。
          //    全负数时答案就是那个最大的负数,即 bestMax。
          if (bestMax < 0) return bestMax

          // 情况 A 与情况 B 取较优
          return maxOf(bestMax, total - bestMin)
      }
  }

复杂度

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记