题目

给定一个二进制数组 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

思路

相比于这个系列第一题1004. 最大连续1的个数 III - 图1,这个题允许1004. 最大连续1的个数 III - 图2中间最多有1004. 最大连续1的个数 III - 图31004. 最大连续1的个数 III - 图4,那么我们让滑窗右边界尽量向右移动,同时记录滑窗内1004. 最大连续1的个数 III - 图5或者1004. 最大连续1的个数 III - 图6的个数,当滑窗内1004. 最大连续1的个数 III - 图7的个数超过1004. 最大连续1的个数 III - 图8就收缩左边界,然后更新最长连续1004. 最大连续1的个数 III - 图9的个数。

去年有段时间连着做了好多滑窗题目,以至于再看到这类题特别敏感地第一反应就是滑窗,其实思路有一个优化过程,滑窗是1004. 最大连续1的个数 III - 图10#card=math&code=O%28n%29&id=eNhsu)时间复杂度,其实还有另一种“不好想”的思路,前缀和二分,思路如下:

因为我们希望序列尽可能长,所以对于1004. 最大连续1的个数 III - 图11内的一个下标1004. 最大连续1的个数 III - 图12,我们找尽可能小的1004. 最大连续1的个数 III - 图13,使1004. 最大连续1的个数 III - 图14区间内1004. 最大连续1的个数 III - 图15的个数不超过1004. 最大连续1的个数 III - 图16。对于区间内1004. 最大连续1的个数 III - 图17的个数,我们将原始输入的1004. 最大连续1的个数 III - 图18变成1004. 最大连续1的个数 III - 图191004. 最大连续1的个数 III - 图20变成1004. 最大连续1的个数 III - 图21,然后就可以使用前缀和1004. 最大连续1的个数 III - 图22#card=math&code=O%281%29&id=YMZgu)时间计算,区间内1004. 最大连续1的个数 III - 图23的个数不超过1004. 最大连续1的个数 III - 图24就可以表示为1004. 最大连续1的个数 III - 图25,即1004. 最大连续1的个数 III - 图26,其中1004. 最大连续1的个数 III - 图27为前缀和数组,1004. 最大连续1的个数 III - 图281004. 最大连续1的个数 III - 图29为数组下标,介于1004. 最大连续1的个数 III - 图30。巧妙的是,前缀和数组是有序的,所以对于每个1004. 最大连续1的个数 III - 图31下标,我们可以使用二分查找满足条件最小的1004. 最大连续1的个数 III - 图32,因此总时间为1004. 最大连续1的个数 III - 图33#card=math&code=O%28nlogn%29&id=wTJXM),相比滑窗虽然时间变长了,但是这种解法值得学习。

代码

滑窗O(n)

  1. class Solution {
  2. public int longestOnes(int[] nums, int k) {
  3. int n = nums.length;
  4. int left = 0;
  5. int right = 0;
  6. int cnt = 0;
  7. int ans = 0;
  8. while (right < n) {
  9. cnt += nums[right];
  10. if (right - left + 1 - cnt > k) {
  11. cnt -= nums[left];
  12. left++;
  13. }
  14. right++;
  15. ans = Math.max(ans, right - left);
  16. }
  17. return ans;
  18. }
  19. }

二分+前缀和O(nlogn)

  1. class Solution {
  2. public int longestOnes(int[] nums, int k) {
  3. int n = nums.length;
  4. int ans = 0;
  5. int[] preSum = new int[n + 1];
  6. for (int i = 1; i <= n; i++) {
  7. preSum[i] = preSum[i - 1] + 1 - nums[i - 1];
  8. }
  9. for (int right = 0; right < n; right++) {
  10. // right和left为滑窗的右边界和左边界
  11. // 求最小的left满足:pre[left]>=pre[right+1]-k
  12. // 前缀和数组求和right要偏移一个下标,因此是right+1
  13. int left = bsearch(preSum, preSum[right + 1] - k);
  14. ans = Math.max(ans, right - left + 1);
  15. }
  16. return ans;
  17. }
  18. // 二分查找nums数组中第一个不小于target的数,返回其下标
  19. int bsearch(int[] nums, int target) {
  20. int len = nums.length;
  21. int low = 0;
  22. int high = len - 1;
  23. while (low < high) {
  24. int mid = low + (high - low) / 2;
  25. if (nums[mid] < target) {
  26. low = mid + 1;
  27. } else {
  28. high = mid;
  29. }
  30. }
  31. return low;
  32. }
  33. }