从暴力解法到滑动窗口。
从一道基础算法题快速了解滑动窗口,再一道竞赛题快速加深对滑动窗口的理解。
长度最小的子数组
链接:https://leetcode.cn/problems/minimum-size-subarray-sum/description/
暴力解法:直接两层循环遍历出所有区间,取最小长度。结果会是超时的。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int res = nums.length + 1; // 当然Integer.MAX_VALUE也可以,return判断改一下即可
for (int i = 0,sum = 0; i < nums.length; i++) {
for (int j = i; j < nums.length; j++) {
sum += nums[j];
if (sum >= target) {
res = Math.min(res, j - i + 1);
break;
}
}
sum = 0;
}
return (res == nums.length + 1) ? 0 : res;
}
}
// 暴力-逻辑-易
// 时间复杂度:$O(n^2)$ 空间复杂度:$O(1)$
滑动窗口解法:
滑动窗口,两层循环变一层循环,那么外循环一般都会是对应终止位置。
先遍历外层终止位置:
当子数组中存在比target大或等的数,那么就开始移动起始位置,同时更新最小长度,直 到不存在这样的target(可以做个优化,如果最小长度为1,break)
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int res = nums.length + 1; // 当然Integer.MAX_VALUE也可以
for (int i = 0, sum = 0, index = 0; i < nums.length; i++) {
sum += nums[i];
while (sum >= target) {
res = Math.min(res, i - index + 1); // 可以优化:当res==1时,直接return
sum -= nums[index++]; // 用哪个index,就减去哪一个
}
}
return (res == nums.length + 1) ? 0 : res;
}
}
// 双指针——优
// 时间复杂度:$O(n)$ 空间复杂度:$O(1)$
最大子矩阵
时间限制: 1.0s 内存限制: 512.0MB 本题总分:15 分
【问题描述】
小明有一个大小为 N × M 的矩阵,可以理解为一个 N 行 M 列的二维数组。
我们定义一个矩阵 m 的稳定度 f(m) 为 f(m) = max(m) − min(m),其中 max(m)
表示矩阵 m 中的最大值,min(m) 表示矩阵 m 中的最小值。现在小明想要从这
个矩阵中找到一个稳定度不大于 limit 的子矩阵,同时他还希望这个子矩阵的面
积越大越好(面积可以理解为矩阵中元素个数)。
子矩阵定义如下:从原矩阵中选择一组连续的行和一组连续的列,这些行
列交点上的元素组成的矩阵即为一个子矩阵。
【输入格式】
第一行输入两个整数 N,M,表示矩阵的大小。
接下来 N 行,每行输入 M 个整数,表示这个矩阵。
最后一行输入一个整数 limit,表示限制。
【输出格式】
输出一个整数,分别表示小明选择的子矩阵的最大面积。
【样例输入】
3 4
2 0 7 9
0 6 9 7
8 4 6 4
8
【样例输出】
6
【样例说明】
满足稳定度不大于 8 的且面积最大的子矩阵总共有三个,面积都是6(粗体表示子矩阵元素):
2 0 7 9
0 6 9 7
8 4 6 4
2 0 7 9
0 6 9 7
8 4 6 4
2 0 7 9
0 6 9 7
8 4 6 4
【滑动窗口解法】:
本题还自行使用单独队列自行维护,用单调队列来实现滑动窗口算法。
在入值时,把矩阵每列的行位置(1-n)对应最大值、最小值维护好,然后给予固定范围比较时,就相当于不同列之间的一维数组做比较,根据窗口的移动,维护单独队列,以查看是否达到限制,若没有则TRUE,有则到最后一列时返回FALSE。
import java.util.*;
public class Main {
static int[][][] max;
static int[][][] min;
static int n, m, limit, res;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
max = new int[m+1][n+1][n+1];
min = new int[m+1][n+1][n+1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
max[j][i][i] = min[j][i][i] = sc.nextInt();
}
}
/**
max[1][1][1] = min[1][1][1] = 2 max[2][1][1] = min[2][1][1] = 0 max[3][1][1] = min[3][1][1] = 7 ...
max[1][2][2] = min[1][2][2] = 0 ...
*/
limit = sc.nextInt();
// max[k][i][j]表示第k列中[i,j]之间的最大值
for (int k = 1; k <= m; k++) { // 遍历列
for (int i = 1; i < n; i++) { // 遍历行
for (int j = i + 1; j <= n; j++) { // 遍历行后
max[k][i][j] = Math.max(max[k][i][j - 1], max[k][j][j]); // f(j) = f(j-1) / f(j)
min[k][i][j] = Math.min(min[k][i][j - 1], min[k][j][j]);
}
}
}
for (int x1 = 1; x1 <= n; x1++) { // 遍历行
for (int x2 = x1; x2 <= n; x2++) { // 遍历行和行后(因为有为1)
int l = 1, r = m; // 为什么这里不直接遍历m到1或1到m,因为用二分能加快查速
while (l < r) {
int mid = l + r + 1 >> 1; // 偏右的位置(这样不会出现mid重复的情况,因为mid是给left的)
if (check(x1, x2, mid)) {
l = mid;
} else {
r = mid - 1;
}
}
if (check(x1,x2,r)) {
res = Math.max(res, (x2 - x1 + 1) * r);
}
}
}
System.out.println(res);
}
/*
单调队列 + 滑动窗口
检查x1 x2范围内是否有连续k列满足条件
x1 x2为行范围,k为列的最大长度
*/
private static boolean check(int x1, int x2, int k) {
Deque<Integer> MAX = new ArrayDeque<Integer>();
Deque<Integer> MIN = new ArrayDeque<Integer>(); // 塞入队列的是列元素
for (int i = 1; i <= m; i++) { // 遍历所有列
if (!MAX.isEmpty() && i - MAX.peekFirst() + 1 > k) { // 如果超过列最大长度,去除最前面的(发生前k列不满足的情况)
MAX.pollFirst();
}
while (!MAX.isEmpty() && max[MAX.peekLast()][x1][x2] < max[i][x1][x2]) {
MAX.pollLast(); // MAX只会依次放入大值,维持一个单调递增,队列尾最大的情况
}
MAX.offerLast(i); // 入队尾(会发现,第i列在队列中的位置,不会超过i-1,所以保证了前k列不满足,抛出队列头后还能继续判断)
if (!MIN.isEmpty() && i - MIN.peekFirst() + 1 > k) { // 如果超过列最大长度,去除最前面的
MIN.pollFirst();
}
while (!MIN.isEmpty() && min[MIN.peekLast()][x1][x2] > min[i][x1][x2]) {
MIN.pollLast(); // MIN只会依次放入小值,维持一个单调递减,队列尾最小的情况
}
MIN.offerLast(i); // 入队尾
// 当i大于最大长度时,若最大值与最小值之差在limit内,则返回true,否则直到i到m返回false
if (i >= k && max[MAX.peekFirst()][x1][x2] - min[MIN.peekFirst()][x1][x2] <= limit) {
return true;
}
}
return false;
}
}