Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
题目描述
这是 LeetCode 上的 1004. 最大连续1的个数 III ,难度为 中等。
Tag : 「双指针」、「滑动窗口」、「二分」、「前缀和」
给定一个由若干 00 和 11 组成的数组A
,我们最多电脑恢复出厂设置可以将KK个值从 00 变成 11 。
返回仅包含 11 的最长(线程安全连续)子数组的长度。
示例 1:
输入:A = [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:
输入:A = [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<=A.length<=200001 <= A.length <滑动窗口= 20000
- 0<=算法分析的目的是K<=A.length0 <= K <电脑;= A.length
- A[i]A[i] 为00或11
动态规划(TLE)
看到本题,其实首先想到的是算法的时间复杂度取决于 DP,但是 DP 是 O(nk)O(nk) 算法。
看到了数据范围是 10410^4,那么时空复杂度电脑怎么截图应该都是 10810^8。
空间可以通过「滚动数组」优化到 10410^4,但时间无法优化,会超时。
PS. 什么时候我们会用 DP 来解本题?通过如果 K 的数量级不超过电脑恢复出厂设置 1000 的话,DP 应该是最常规的做法。
定义 f[i,j]f[i,j] 代表考虑前 ii 个数(并算法设计与分析以 A[i]A[i] 为结尾的),最大翻转次数为 jj 时,连续 11 的最大长度。leetcode是什么意思
- 如果 A[i]A[i] 本身就为 1 的话,无须消耗翻转次数,f[i][j]=f[i−1][j]+1f[i][j] = f[i – 1][j] + 1。线程数越多越好吗
- 如果 A[i]A[i] 本身不为 1 的话,由于定义是必须以 A[i]A[i] 为结尾,电脑黑屏却开着机因此必须要选择翻转该位置,f[i][j]=f[i−1][j−1]+1f[i][j] =滑动窗口机制 f[i – 1][j – 1] + 1。
代码:
class Solution {
public int longestOnes(int[] nums, int k) {
int n = nums.length;
int[][] f = new int[2][k + 1];
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= k; j++) {
if (nums[i - 1] == 1) {
f[i & 1][j] = f[(i - 1) & 1][j] + 1;
} else {
f[i & 1][j] = j == 0 ? 0 : f[(i - 1) & 1][j - 1] + 1;
}
ans = Math.max(ans, f[i & 1][j]);
}
}
return ans;
}
}
- 时间复杂度:O(nk)O(nk)
- 空间复杂度:O(k)O(k)
前缀和 + 二分
从数据范围上分析,平方级别的算法过不了,往下优化就应该是对数级别的算法。
因此,很容易我们就会想到「二分」。
当然还需要我们对问题做一下等价变形。
最大替换次数不超过 k
次,可以将问电脑锁屏快捷键题转换为找出连电脑键盘续一段区间 [l,r]
,使得区间中出现 0 的次数不超过 k
次。
我们可以枚举区间 左端点/右端点 ,然后找到其满足「出现 0 的次数不超过 k
次」的最远右端点/最远左端点。
为了快速判断 [l,r]
之间出现 0 的个数算法分析的目的是,我们需要用到前缀和。
假设 [l,r]
的区间长度为 len
,区间和为 tot
,那么出现 0 的格式为 len - tol
,再与 k
进行比较。
由于数组中不滑动窗口的python代码会出现负权值,因此前缀和算法的有穷性是指数组具有「单调性」,那么必然满足「其中一段满足 len−tol<=klen – tol <= k,另外一段不满足 len−tol<=klen – tol <= k」。
因此,对于某个确定的「左端点/右端点」而言,以「其最远右端点/最远左端点」为分割点的前缀和数轴,具有「二段性」。可以通过二分来找分割点。
代码:
class Solution {
public int longestOnes(int[] nums, int k) {
int n = nums.length;
int ans = 0;
int[] sum = new int[n + 1];
for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
for (int i = 0; i < n; i++) {
int l = 0, r = i;
while (l < r) {
int mid = l + r >> 1;
if (check(sum, mid, i, k)) {
r = mid;
} else {
l = mid + 1;
}
}
if (check(sum, r, i, k)) ans = Math.max(ans, i - r + 1);
}
return ans;
}
boolean check(int[] sum, int l, int r, int k) {
int tol = sum[r + 1] - sum[l], len = r - l + 1;
return len - tol <= k;
}
}
- 时间复杂度:O(nlogn)O(nlog{n})
- 空间复杂度:O(n)O(n)
关于二分结束后再次 check
的说明:由于「二分」本质是找满足某个性质的分割点,通常我们的某个性质会是「非滑动窗口等值条件」,不一定会取得 =
。
例如我们很熟悉的:从某个非递减数组算法的特性中找目标值,找到返回下标,否则返回 -1。
当目标值不存在,「二分」找到的应该是数组内比目标值leetcode刷题指南小或比目标值大的最接近的数。因此二分结束后先进行 check
再使用是一个好习惯。
双指针
由于我们总是比较 len
、tot
和 k
三者的关系。
因此我们可以使用「滑动窗口」的思路,动态维护一个左右区间 [j, i]
和维护窗口内和 tot
。
右端点一直右移,左端点在窗口不满足「len - tol <= k
」的时候进行右移,即可做到线程扫描的复杂度。
代码:
class Solution {
public int longestOnes(int[] nums, int k) {
int n = nums.length;
int ans = 0;
for (int i = 0, j = 0, tot = 0; i < n; i++) {
tot += nums[i];
while ((i - j + 1) - tot > k) tot -= nums[j++];
ans = Math.max(ans, i - j + 1);
}
return ans;
}
}
- 时间复杂度:O(n)O(n)
- 空间复杂leetcode是什么意思度:O(1)O(1)
总结
除了掌握本题解法以外,我还希望你能理解这几种解法是如何被想到的(特别是如何从「动态规划」想到「二分」)。
根据数据范围(复杂度)调整自己所使用的算法的分析能力,比解决该题本身更加重要。
最后
这是我们「刷穿 LeetCode」系列文章的第 No.1004
篇,系列开始于 2021/01/01,截止于起始算法是什么日 LeetCo线程的几种状态de 上线程池共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码电脑锁屏快捷键。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/Sharileetcode官网ngSour…滑动窗口的python代码 。
在仓库地址里,你滑动窗口可以看到系列文章的题解链接、系列文章的相应代码、LeetCod滑动窗口协议e 原题链接和其他优选题解。
发表回复
要发表评论,您必须先登录。