删掉一个元素以后全为 1 的最长子数组

标签: 数组 动态规划 滑动窗口

难度: Medium

给你一个二进制数组 nums ,你需要从中删掉一个元素。

请你在删掉元素的结果数组中,返回最长的且只包含 1 的非空子数组的长度。

如果不存在这样的子数组,请返回 0 。

提示 1:

输入:nums = [1,1,0,1]
输出:3
解释:删掉位置 2 的数后,[1,1,1] 包含 3 个 1 。

示例 2:

输入:nums = [0,1,1,1,0,1,1,0,1]
输出:5
解释:删掉位置 4 的数字后,[0,1,1,1,1,1,0,1] 的最长全 1 子数组为 [1,1,1,1,1] 。

示例 3:

输入:nums = [1,1,1]
输出:2
解释:你必须要删除一个元素。

提示:

  • 1 <= nums.length <= 105
  • nums[i] 要么是 0 要么是 1

Submission

运行时间: 44 ms

内存: 20.0 MB

class Solution:
    def longestSubarray(self, nums: List[int]) -> int:
        ans,l,r = 0,0,0
        cnt = 0
        n = len(nums)
        if all(nums):
            return n-1
        if not any(nums):
            return 0
        while r < n:
            if nums[r] == 0:
                if cnt == 0:
                    cnt += 1
                else:
                    ans = max(ans,r-l-1)
                    while nums[l] != 0:
                        l += 1
                    l += 1
            r += 1
        return max(ans,r-l-cnt)

Explain

这道题解采用了滑动窗口的策略来找出最长的全1子数组,即使其中包含一个0。算法维护了三个指针l(左边界)和r(右边界),以及cnt,用于记录窗口内0的个数。遍历数组时,当遇到0且cnt为0,cnt加1,表示窗口内可以包含一个0;如果再次遇到0,则需要调整左边界l,直到窗口内的0的数量减少到1。这通过将左边界移动到下一个0之后来实现。在每次右边界r移动时,都会尝试更新最大长度ans。特殊情况处理:如果数组全为1,则删除一个元素后的长度为n-1;如果数组全为0,则返回0。

时间复杂度: O(n)

空间复杂度: O(1)

class Solution:
    def longestSubarray(self, nums: List[int]) -> int:
        ans, l, r = 0, 0, 0 # 初始化答案,左右指针
        cnt = 0 # 用于记录窗口内0的数量
        n = len(nums) # 数组长度
        if all(nums): # 如果数组全为1
            return n-1 # 返回n-1
        if not any(nums): # 如果数组全为0
            return 0 # 返回0
        while r < n: # 遍历数组
            if nums[r] == 0: # 当遇到0
                if cnt == 0: # 如果窗口内没有0
                    cnt += 1 # 增加0的数量
                else: # 窗口内已有一个0
                    ans = max(ans, r - l - 1) # 更新最大长度
                    while nums[l] != 0: # 移动左指针直到遇到0
                        l += 1
                    l += 1 # 移过这个0
            r += 1 # 移动右指针
        return max(ans, r - l - cnt) # 最后一次更新最大长度

Explore

当数组中只有一个0时,滑动窗口的策略会在遇到这个0时将cnt设置为1,然后继续扩展右边界r直到数组末尾。因为只有一个0,左边界l不会移动直到遍历结束。算法最后在循环外通过表达式`max(ans, r - l - cnt)`来计算最长的全1子数组长度,其中`cnt`=1表示存在一个0,从而确保计算出正确的最长全1子数组长度。

在遍历结束后,需要再次更新最大长度来确保最后一个潜在的最长子数组被考虑。特别是如果最长的全1子数组在数组的末尾或包含数组末尾的部分,这种情况下,窗口的右边界r已经到达数组的末尾而左边界l可能还未更新,因此需要计算最后一次的子数组长度。公式中的`-cnt`是为了从长度中减去窗口内0的数量,因为我们需要计算的是全1的子数组长度。

提供特殊规则对于全为1或全为0的数组能够直接返回结果,大大提高了算法的效率,因为这避免了不必要的遍历和计算。这种预处理步骤可以快速处理边缘情况,使算法更加高效和直观。

滑动窗口的目的是维持窗口内最多一个0的条件。当窗口内已有一个0,再遇到新的0时,需要将左指针移动到第一个0之后,以确保窗口内只保留一个0。这样做可以简化逻辑,并确保每次只处理一个0的情况,避免复杂的多0处理。直接跳过多个0可能会错过有效的全1子数组,因为这些子数组可能包括部分被跳过的0之间的区间。