【Leetcode】918. Maximum Sum Circular Subarray

题目地址:

https://leetcode.com/problems/maximum-sum-circular-subarray/

给定一个长 n n n的数组 A A A,要求找到和最大的非空循环子数组。返回其和。

法1:单调队列。首先容易想到构造一个新数组 B B B,其恰好是 A A A后面再append一个 A A A,并且删掉最后一个数,这样 B B B的长度是 A A A的两倍减 1 1 1,并且任意一个 A A A的循环子数组都能在 B B B中找到一个长度不超过 n n n的子数组与之对应(这种对应不是一一对应,但是能够做到不遗漏)。这样问题就转化为,求 B B B中长度不超过 n n n的最大和子数组的和是多少,可以用单调队列来做,参考https://blog.csdn.net/qq_46105170/article/details/109590586。具体做法是开一个严格单调增的双端队列,存下标,并且构造 B B B的前缀和 p p p。维护队列里存的是下一轮要遍历到的数 p [ i ] p[i] p[i]为右端点的、最大和的子段对应的左端点的备选,那么当遍历到 p [ i ] p[i] p[i]的时候, i − n − 1 i-n-1 in1这个下标如果在队列里的话,应该出队(超出长度了),接着队列如果非空,则队首 j j j所对应的子段 p [ i ] − p [ j ] p[i]-p[j] p[i]p[j]就是以 B [ i + 1 ] B[i+1] B[i+1]为右端点的最大和子段的和(这其实是因为队列是严格单调增的,下面会看到原因),下一步很关键,要将队尾大于等于 p [ i ] p[i] p[i]的下标都出队(这里的意思不是拿下标与 p [ i ] p[i] p[i]比,而是拿下标对应的 p p p值去比),因为这些数在接下来的遍历中,不会成为左端点的候选(因为它们如果是候选的话, p [ i ] p[i] p[i]必然更是候选,因为 p [ i ] p[i] p[i]不但值更小,而且位置更右),接着将 i i i入队。从这些步骤可以看出,队列里的下标对应的 p p p值是严格单调增的。代码如下:

import java.util.ArrayDeque;
import java.util.Deque;

public class Solution {
    public int maxSubarraySumCircular(int[] A) {
        int[] preSum = new int[A.length * 2];
        for (int i = 0; i < A.length * 2 - 1; i++) {
            preSum[i + 1] = preSum[i] + A[i % A.length];
        }
        
        int res = Integer.MIN_VALUE;
        Deque<Integer> deque = new ArrayDeque<>();
        for (int i = 0; i < preSum.length; i++) {
        	// 去掉出界的队首
            if (!deque.isEmpty() && deque.peekFirst() < i - A.length) {
                deque.pollFirst();
            }
            
            // 此时p[i]为右端点对应的最大子段和就是p[i]减去p在队首的值
            if (!deque.isEmpty()) {
                res = Math.max(res, preSum[i] - preSum[deque.peekFirst()]);
            }
            
            // 维护单调性
            while (!deque.isEmpty() && preSum[deque.peekLast()] >= preSum[i]) {
                deque.pollLast();
            }
            
            deque.offerLast(i);
        }
        
        return res;
    }
}

时空复杂度 O ( n ) O(n) O(n)

法2:求最小子段和。容易看出,最大循环子段和要么是最大子段和,要么是总和减去最小子段和(这里需要注意一个特殊情况,如果最大子段和是负的,那么说明 A A A里的所有数都是负的,此时最大循环子段和就是 A A A里的所有负数里的最大的那个,这个数恰好等于最大子段和。但是,此时最小子段和是 A A A的总和,总和减去最小子段和结果是 0 0 0,结论并不对。根本原因是题目要求找的循环子段和是非空的循环子段)。而求最大和最小子段和可以用动态规划来做。代码如下:

public class Solution {
    public int maxSubarraySumCircular(int[] A) {
        int curMaxSum = 0, curMinSum = 0, total = 0, minSum = Integer.MAX_VALUE, maxSum = Integer.MIN_VALUE;
        for (int x : A) {
            curMaxSum = Math.max(x, x + curMaxSum);
            curMinSum = Math.min(x, x + curMinSum);
            minSum = Math.min(minSum, curMinSum);
            maxSum = Math.max(maxSum, curMaxSum);
            total += x;
        }
        
        return maxSum >= 0 ? Math.max(maxSum, total - minSum) : maxSum;
    }
}

时间复杂度 O ( n ) O(n) O(n),空间 O ( 1 ) O(1) O(1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值