LeetCode 1004.最大连续1的个数III 滑动窗口及动态规划题解

一、题目描述

给定一个二进制数组 nums 和一个整数 k,假设最多可以翻转 k 个 0,则返回执行操作后数组中连续 1 的最大个数。

示例 1:

复制代码
输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。

示例 2:

复制代码
输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。

提示:

  • 1 <= nums.length <= 10^5

  • nums[i] 不是 0 就是 1

  • 0 <= k <= nums.length

二、题目本质分析

这道题表面上是在问"翻转 k 个 0 后能得到多长的连续 1",但换个角度理解:

等价于:在一个二进制数组中,找到一个最长的子数组,使得子数组中 0 的个数不超过 k。

这个转化非常关键,因为"翻转 k 个 0"实际上就是"允许窗口内出现最多 k 个 0"。一旦想清楚这一点,问题就变成了求满足条件的最长子数组长度,而这类问题有两个经典解法:

  1. 滑动窗口(双指针) ------ 最优解,O(n)

  2. 动态规划 ------ 辅助理解,O(n·k)

三、法一:滑动窗口

3.1 核心思路

维护一个窗口 [left, right],保证窗口内 0 的个数不超过 k。

  • 右指针 right 扩张 :每次把 nums[right] 加入窗口,如果是 0,则 zeroCount++。

  • 当窗口内 0 的个数超过 k 时 :说明窗口不合法,需要左指针 left 收缩,直到 zeroCount <= k。收缩时如果移出的是 0,则 zeroCount--。

  • 每次窗口合法时 ,用 right - left + 1 更新答案。

3.2 图示理解(以示例 1 为例)

nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2

复制代码
初始:    left=0, right=0, zeroCount=0
右扩:    [1]                          zeroCount=0, 合法, ans=1
右扩:    [1,1]                        zeroCount=0, 合法, ans=2
右扩:    [1,1,1]                      zeroCount=0, 合法, ans=3
右扩:    [1,1,1,0]                    zeroCount=1, 合法, ans=4
右扩:    [1,1,1,0,0]                  zeroCount=2, 合法, ans=5
右扩:    [1,1,1,0,0,0]                zeroCount=3, 不合法!
收缩:    [1,1,0,0,0]  移出1          zeroCount=3, 仍不合法
收缩:    [1,0,0,0]    移出1          zeroCount=3, 仍不合法
收缩:    [0,0,0]      移出1          zeroCount=3, 仍不合法
收缩:    [0,0,0]      移出0          zeroCount=2, 合法!
右扩:    [0,0,0,1]                    zeroCount=2, 合法, ans=4
右扩:    [0,0,0,1,1]                  zeroCount=2, 合法, ans=5
...以此类推
最终最长窗口: [0,0,1,1,1,1,1,1] (长度 6)

3.3 代码实现(C++)

cpp 复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int left = 0, right = 0;
        int zeroCount = 0; // 当前窗口内 0 的个数
        int maxLen = 0;
        
        while (right < nums.size()) {
            // 右指针元素进入窗口
            if (nums[right] == 0) {
                zeroCount++;
            }
            
            // 如果窗口内 0 的个数超过 k,左指针收缩
            while (zeroCount > k) {
                if (nums[left] == 0) {
                    zeroCount--;
                }
                left++;
            }
            
            // 更新最大长度
            maxLen = max(maxLen, right - left + 1);
            right++;
        }
        
        return maxLen;
    }
};

3.4 复杂度分析

  • 时间复杂度 :O(n)。left 和 right 各自最多向右移动 n 次,总移动次数不超过 2n。

  • 空间复杂度:O(1)。只使用常数个变量。

四、法二:动态规划(会超出内存限制)

4.1 为什么可以用 DP?

滑动窗口是空间上的贪心,而 DP 是另一种思路:从每个位置出发,思考以该位置结尾的最长合法子数组能有多长。

4.2 状态定义

定义 dp[i][j] 表示:

以 nums[i] 为结尾 ,且恰好使用了 j 次翻转机会 时,能得到的最长连续 1 的长度。

这里的"恰好使用 j 次"是指最多可以使用 j 次 ,因为翻不翻是灵活的,但为了状态统一,我们约定 j 表示"预算",遇到 0 就消耗一次。

4.3 状态转移方程

分两种情况讨论:

情况 1:nums[i] == 1

不需要消耗翻转次数,直接延长前一个状态即可:

cpp 复制代码
dp[i][j] = dp[i-1][j] + 1

情况 2:nums[i] == 0

必须消耗一次翻转机会:

  • 如果 j > 0(还有翻转次数),则:
cpp 复制代码
dp[i][j] = dp[i-1][j-1] + 1
  • 如果 j == 0(没有翻转次数了),遇到 0 只能断掉:
cpp 复制代码
dp[i][j] = 0

4.4 为什么"以 i 结尾"很重要?

因为题目要求的是连续 子数组。定义状态时明确"以 nums[i] 结尾",转移时才能强制连续。

如果定义成 dp[i][j] 为"前 i 个元素中的最长连续 1",就无法正确转移,因为子数组可能从前面的某个位置开始,不一定包含 nums[i]。

4.5 初始化

dp 数组大小为 (n+1) × (k+1),全部初始化为 0。

dp[0][*] = 0,表示还没有元素,长度为 0。

遍历时 i 从 1 到 n,j 从 0 到 k。

4.6 代码实现(C++)

cpp 复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int n = nums.size();
        // dp[i][j] 表示以 i 结尾,用了 j 次翻转的最大长度
        vector<vector<int>> dp(n + 1, vector<int>(k + 1, 0));
        int ans = 0;
        
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= k; j++) {
                if (nums[i - 1] == 1) {
                    dp[i][j] = dp[i - 1][j] + 1;
                } else { // nums[i-1] == 0
                    if (j > 0) {
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    } else {
                        dp[i][j] = 0; // 没有翻转次数了,遇到0只能断
                    }
                }
                ans = max(ans, dp[i][j]);
            }
        }
        return ans;
    }
};

4.7 举例演示

以 nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2 为例,展示部分 DP 过程:

i numsi-1 j=0 j=1 j=2
1 1 1 1 1
2 1 2 2 2
3 1 3 3 3
4 0 0 4 4
5 0 0 1 5
6 0 0 0 2
7 1 1 1 3
8 1 2 2 4
9 1 3 3 5
10 1 4 4 6
11 0 0 5 5

最后答案取所有 dp[i][j] 的最大值,即 6。

4.8 复杂度分析

  • 时间复杂度:O(n·k)。双重循环,外层 n,内层 k。

  • 空间复杂度:O(n·k)。二维数组

相关推荐
朝朝辞暮i1 小时前
C++ 第 22 课:const —— 不允许随便修改的数据
开发语言·c++·算法
蒸蒸yyyyzwd2 小时前
力扣二分总结-lowerbound
c++
Chen—LSN2 小时前
数据结构——拿捏复杂度
java·c语言·开发语言·数据结构·经验分享·笔记·算法
Chen_harmony2 小时前
六、排序详解
c语言·数据结构·算法·排序算法
ShineWinsu2 小时前
对于Redis:List类型的解析
数据库·c++·redis·链表·缓存·面试·list
-dzk-2 小时前
【贪心算法】LC 55.跳跃游戏
算法·游戏·贪心算法
ofoxcoding2 小时前
Jev 模型评测深度解析:核心得分、能力局限与最佳适用任务场景
人工智能·算法·机器学习·ai
Evand J2 小时前
【MATLAB例程】三维A*路径规划与TOA-AOA-TDOA融合定位算法。附下载链接
数据库·算法·matlab
Doubbbbbbble云2 小时前
图算法中的剪枝技术与启发式优化分析4
算法·机器学习·剪枝