在问 · 算法题解

918. 环形子数组的最大和

中等动态规划Kadane 最大和与最小和力扣原题 ↗

题目描述

输入一个长度为 n 的整数数组 nums,把它看成首尾相接的环:nums[n-1] 之后紧跟 nums[0]。

在环上取一段连续、非空的元素,称为子数组。子数组可以跨过末尾绕回开头,但同一个位置的元素最多只能出现一次,因此长度不超过 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 * 10^4
  • -3 * 10^4 <= nums[i] <= 3 * 10^4

解题思路:Kadane 最大和与最小和

最大子数组只有两种形状:不跨边界时就是普通的最大子数组和(Kadane:cur = max(cur + x, x));跨边界时是“头一段 + 尾一段”,剩下的中间部分是一个普通子数组,要让头尾最大就要让中间最小,即 total - 最小子数组和。一遍扫描同时求最大和最小子数组和,取两者较大值。

为什么正确

处理完 x 后,cur_max / cur_min 是以 x 结尾的子数组最大 / 最小和,best_max / best_min 是目前已见的最大 / 最小子数组和。

复杂度分析

时间 O(n),空间 O(1)。

易错点

全是负数时最小子数组就是整个数组,total - best_min = 0 对应空子数组,不合法。所以 best_max < 0 时直接返回 best_max。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def maxSubarraySumCircular(self, nums: List[int]) -> int:
        total = 0
        cur_max, best_max = 0, nums[0]
        cur_min, best_min = 0, nums[0]
        for x in nums:
            total += x
            cur_max = max(cur_max + x, x)
            best_max = max(best_max, cur_max)
            cur_min = min(cur_min + x, x)
            best_min = min(best_min, cur_min)
        if best_max < 0:
            return best_max
        return max(best_max, total - best_min)

载入编辑器,自己跑一遍 →

其他解法

主解法用“总和减最小子数组和”的转化,O(1) 空间最省;前缀最大和枚举直接拼接后缀与前缀,思路更直观;单调队列解法最通用,能处理带长度限制的变形。

解法 2:前缀最大和枚举 时间 O(n),空间 O(n)

跨越首尾的子数组由一段后缀和一段前缀拼成。先求 left[i],表示以 nums[0] 开头、结尾不超过 i 的前缀的最大和,同时用 Kadane 求不跨越首尾的最大子数组和。再从右往左累加后缀和 suf,用 suf + left[i - 1] 更新答案。

直接枚举“后缀 + 前缀”的组合,不需要“总和减最小和”的转化,也不用单独处理全负数组。代价是多一个长度为 n 的数组。

class Solution:
    def maxSubarraySumCircular(self, nums: List[int]) -> int:
        n = len(nums)
        left = [0] * n
        left[0] = pre = nums[0]
        cur = best = nums[0]
        for i in range(1, n):
            pre += nums[i]
            left[i] = max(left[i - 1], pre)
            cur = max(cur + nums[i], nums[i])
            best = max(best, cur)
        suf = 0
        for i in range(n - 1, 0, -1):
            suf += nums[i]
            best = max(best, suf + left[i - 1])
        return best

解法 3:前缀和 + 单调队列 时间 O(n),空间 O(n)

把数组复制一份接在后面,求长度 2n 的前缀和 pre。子数组 (i, j] 的和是 pre[j] - pre[i],要求 j - n <= i < j。用单调递增的双端队列维护窗口内最小的 pre[i],每个 j 用队首更新答案。

这是“长度不超过 n 的最大子数组和”的通用解法,可以推广到任意长度上限的问题。常数和空间都比主解法大。

from collections import deque

class Solution:
    def maxSubarraySumCircular(self, nums: List[int]) -> int:
        n = len(nums)
        pre = [0] * (2 * n + 1)
        for i in range(2 * n):
            pre[i + 1] = pre[i] + nums[i % n]
        dq = deque([0])
        best = nums[0]
        for j in range(1, 2 * n + 1):
            while dq[0] < j - n:
                dq.popleft()
            best = max(best, pre[j] - pre[dq[0]])
            while dq and pre[dq[-1]] >= pre[j]:
                dq.pop()
            dq.append(j)
        return best

动态规划 专题的其他题目

查看 动态规划 全部题目 →