题目
给定一个二进制数组 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
思路
相比于这个系列第一题,这个题允许
中间最多有
个
,那么我们让滑窗右边界尽量向右移动,同时记录滑窗内
或者
的个数,当滑窗内
的个数超过
就收缩左边界,然后更新最长连续
的个数。
去年有段时间连着做了好多滑窗题目,以至于再看到这类题特别敏感地第一反应就是滑窗,其实思路有一个优化过程,滑窗是#card=math&code=O%28n%29&id=eNhsu)时间复杂度,其实还有另一种“不好想”的思路,前缀和二分,思路如下:
因为我们希望序列尽可能长,所以对于内的一个下标
,我们找尽可能小的
,使
区间内
的个数不超过
。对于区间内
的个数,我们将原始输入的
变成
,
变成
,然后就可以使用前缀和
#card=math&code=O%281%29&id=YMZgu)时间计算,区间内
的个数不超过
就可以表示为
,即
,其中
为前缀和数组,
和
为数组下标,介于
。巧妙的是,前缀和数组是有序的,所以对于每个
下标,我们可以使用二分查找满足条件最小的
,因此总时间为
#card=math&code=O%28nlogn%29&id=wTJXM),相比滑窗虽然时间变长了,但是这种解法值得学习。
代码
滑窗O(n)
class Solution {
public int longestOnes(int[] nums, int k) {
int n = nums.length;
int left = 0;
int right = 0;
int cnt = 0;
int ans = 0;
while (right < n) {
cnt += nums[right];
if (right - left + 1 - cnt > k) {
cnt -= nums[left];
left++;
}
right++;
ans = Math.max(ans, right - left);
}
return ans;
}
}
二分+前缀和O(nlogn)
class Solution {
public int longestOnes(int[] nums, int k) {
int n = nums.length;
int ans = 0;
int[] preSum = new int[n + 1];
for (int i = 1; i <= n; i++) {
preSum[i] = preSum[i - 1] + 1 - nums[i - 1];
}
for (int right = 0; right < n; right++) {
// right和left为滑窗的右边界和左边界
// 求最小的left满足:pre[left]>=pre[right+1]-k
// 前缀和数组求和right要偏移一个下标,因此是right+1
int left = bsearch(preSum, preSum[right + 1] - k);
ans = Math.max(ans, right - left + 1);
}
return ans;
}
// 二分查找nums数组中第一个不小于target的数,返回其下标
int bsearch(int[] nums, int target) {
int len = nums.length;
int low = 0;
int high = len - 1;
while (low < high) {
int mid = low + (high - low) / 2;
if (nums[mid] < target) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
}