找到最大非递减数组的长度

标签: 队列 数组 二分查找 动态规划 单调队列 单调栈

难度: Hard

给你一个下标从 0 开始的整数数组 nums 。

你可以执行任意次操作。每次操作中,你需要选择一个 子数组 ,并将这个子数组用它所包含元素的  替换。比方说,给定数组是 [1,3,5,6] ,你可以选择子数组 [3,5] ,用子数组的和 8 替换掉子数组,然后数组会变为 [1,8,6] 。

请你返回执行任意次操作以后,可以得到的 最长非递减 数组的长度。

子数组 指的是一个数组中一段连续 非空 的元素序列。

示例 1:

输入:nums = [5,2,2]
输出:1
解释:这个长度为 3 的数组不是非递减的。
我们有 2 种方案使数组长度为 2 。
第一种,选择子数组 [2,2] ,对数组执行操作后得到 [5,4] 。
第二种,选择子数组 [5,2] ,对数组执行操作后得到 [7,2] 。
这两种方案中,数组最后都不是 非递减 的,所以不是可行的答案。
如果我们选择子数组 [5,2,2] ,并将它替换为 [9] ,数组变成非递减的。
所以答案为 1 。

示例 2:

输入:nums = [1,2,3,4]
输出:4
解释:数组已经是非递减的。所以答案为 4 。

示例 3:

输入:nums = [4,3,2,6]
输出:3
解释:将 [3,2] 替换为 [5] ,得到数组 [4,5,6] ,它是非递减的。
最大可能的答案为 3 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

Submission

运行时间: 376 ms

内存: 38.8 MB

class Solution:
    def findMaximumLength(self, nums: List[int]) -> int:
        n = len(nums)

        # [结合值, 总值, 前缀和]
        preQueue = curQeueue = deque()
        curQeueue.append([0,0,0])

        lastDps = []
        segment = 0
        preSum = 0

        for i, num in enumerate(nums):
            preSum += num
            if curQeueue[0][0] <= preSum:
                preQueue = curQeueue
                curQeueue = deque()
                segment += 1
            
            while preQueue and preQueue[0][0] <= preSum:
                lastDps = preQueue.popleft()

            total = preSum - lastDps[2]
            mix = total + preSum
            
            while curQeueue and curQeueue[-1][0] >= mix: curQeueue.pop()
            curQeueue.append([mix, total, preSum])
        
        return segment

Explain

这个题解的核心思路是使用动态规划和两个队列(deque)来跟踪可能的非递减子数组的状态。每次迭代中,通过计算前缀和来识别是否可以增加一个新的非递减子数组,或者更新现有的非递减子数组。具体来说,使用两个队列preQueue和curQueue来维护前一段和当前段的非递减子数组状态。每个队列的元素包含三个值:结合值、总值和前缀和。当当前前缀和大于等于最小结合值时,意味着可以形成一个新的子数组段。否则,更新当前队列的状态以尝试将当前值整合到现有的非递减子数组中。

时间复杂度: O(n)

空间复杂度: O(n)

# Python 3 code with comments

class Solution:
    def findMaximumLength(self, nums: List[int]) -> int:
        n = len(nums)

        # Initialize the deques for maintaining the states
        preQueue = curQueue = deque()
        curQueue.append([0,0,0])  # [combine value, total value, prefix sum]

        lastDps = []
        segment = 0
        preSum = 0

        for i, num in enumerate(nums):
            preSum += num  # Update prefix sum
            if curQueue[0][0] <= preSum:
                preQueue = curQueue  # Start a new segment
                curQueue = deque()
                segment += 1  # Increment segment count

            while preQueue and preQueue[0][0] <= preSum:
                lastDps = preQueue.popleft()  # Pop all elements with combine value <= current prefix sum

            total = preSum - lastDps[2]  # Calculate total for the current subarray
            mix = total + preSum  # Calculate new combine value

            while curQueue and curQueue[-1][0] >= mix:
                curQueue.pop()  # Maintain deque monotonicity
            curQueue.append([mix, total, preSum])  # Append new state

        return segment  # Return the number of segments

Explore

在此题解中,队列的每个元素代表一个潜在的非递减子数组的状态。'结合值'是指当前子数组与前一个子数组的连接点的值,它应该保证非递减性;'总值'是指从当前子数组开始到当前元素的累积总和;'前缀和'则是从数组开始到当前元素的累积和。这些值帮助算法判断何时应继续扩展当前子数组,何时应开始新的子数组,以及如何更新子数组的状态。

'最小结合值'是通过比较所有在preQueue中的'结合值'来计算的,选择最小的一个。逻辑依据是,为了维持子数组的非递减性,新的子数组的开始值(即前缀和的当前值)需要大于或等于前一个非递减子数组的结合值。这确保了新的子数组段的开始不会使整个数组失去非递减的特性。

从preQueue中删除前缀和小于等于当前和的元素是为了确保所有保留在队列中的状态都是有可能用于未来数组段的。删除这些元素可以避免无用的计算和空间浪费,提高算法的效率。算法的正确性保持不变,因为被删除的状态已经被当前的前缀和超越,不会再对形成新的非递减子数组有贡献。

每个元素在被添加到队列时考虑的是当前元素能否形成或扩展非递减子数组,而在被移除时考虑的是它是否还能对形成新的非递减子数组有用。这种处理确保了每个元素的每个可能状态都被充分利用,同时维持了效率。因为每个状态的有效性都在它失效前被充分利用,所以不会漏掉任何可能的非递减子数组配置。