一、题目描述
给定一个二进制数组
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"。一旦想清楚这一点,问题就变成了求满足条件的最长子数组长度,而这类问题有两个经典解法:
-
滑动窗口(双指针) ------ 最优解,O(n)
-
动态规划 ------ 辅助理解,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)。二维数组