49、滑动窗口技巧与相关题目

思想

滑动窗口:维持左、右边界都不回退的一段范围,来求解很多子数组(串)的相关问题

滑动窗口的关键:找到 范围 和 答案指标 之间的 单调性关系(类似贪心)

滑动过程:滑动窗口可以用 简单变量 或者 结构 来 维护信息

求解大流程:求子数组在 每个位置 开头 或 结尾 情况下的答案(开头还是结尾在于个人习惯)

注意:
滑动窗口维持最大值 或者 最小值的更新结构,在【必备】课程【单调队列】视频里讲述

题目一:长度最小的子数组

问题描述

累加和大于等于target的最短子数组长度
给定一个含有 n 个正整数的数组和一个正整数 target
找到累加和 >= target 的长度最小的子数组并返回其长度
如果不存在符合条件的子数组返回0

测试链接 : https://leetcode.cn/problems/minimum-size-subarray-sum/

算法思想

使用滑动窗口,寻找以每个位置数字做结尾的长度,如果大于target,看能不能缩头(丢弃l位置元素),寻找长度最短的解

代码如下

	public static int minSubArrayLen(int target, int[] nums) {
		int ans = Integer.MAX_VALUE;
		for (int l = 0, r = 0, sum = 0; r < nums.length; r++) {
			sum += nums[r];
			while (sum - nums[l] >= target) {
				// sum : nums[l....r]
				// 如果l位置的数从窗口出去,还能继续达标,那就出去
				sum -= nums[l++];
			}
			if (sum >= target) {
				ans = Math.min(ans, r - l + 1);
			}
		}
		return ans == Integer.MAX_VALUE ? 0 : ans;
	}

问题二:无重复字符的最长子串

问题描述

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

测试链接 : https://leetcode.cn/problems/longest-substring-without-repeating-characters/

算法思想

使用滑动窗口,寻找以每个位置字母结尾的无重复字符的子串。每次来到一个位置时,要记录这个位置字母最后出现的位置。加入下一个位置来更新右边界;当计算左边界时,左边界=max{当前左边界位置,当前这个字母最后一次出现位置+1}

代码如下

	public static int lengthOfLongestSubstring(String str) {
		char[] s = str.toCharArray();
		int n = s.length;
		// char -> int -> 0 ~ 255
		// 每一种字符上次出现的位置
		int[] last = new int[256];
		// 所有字符都没有上次出现的位置
		Arrays.fill(last, -1);
		// 不含有重复字符的 最长子串 的长度
		int ans = 0;
		for (int l = 0, r = 0; r < n; r++) {
			l = Math.max(l, last[s[r]] + 1);
			ans = Math.max(ans, r - l + 1);
			// 更新当前字符上一次出现的位置
			last[s[r]] = r;
		}
		return ans;
	}

题目三:最小覆盖子串

问题描述

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串
如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

测试链接 : https://leetcode.cn/problems/minimum-window-substring/

算法思想

使用滑动窗口,首先创造一张欠债表,表里刚开始记录字符串t中的元素各出现的次数(出现一次就是-1,两次就是-2,没出现的就是0)。还是来计算每个位置结尾的窗口是否能还完债,如果不能就向右扩充窗口,如果能,看看能不能从左边缩小窗口(如果不要当前字符,欠债表是否会欠债)。最后取所有位置最短的那个。

代码如下

		public static String minWindow(String str, String tar) {
		if (str.length() < tar.length()) {
			return "";
		}
		char[] s = str.toCharArray();
		char[] t = tar.toCharArray();
		int[] cnts = new int[256];
		for (char cha : t) {
			cnts[cha]--;
		}
		// 最小覆盖子串的长度
		int len = Integer.MAX_VALUE;
		// 从哪个位置开头,发现的这个最小覆盖子串
		int start = 0;
		for (int l = 0, r = 0, debt = t.length; r < s.length; r++) {
			// s[r] 当前字符 -> int
			// cnts[s[r]] : 当前字符欠债情况,负数就是欠债,正数就是多给的
			if (cnts[s[r]]++ < 0) {
				debt--;
			}
			if (debt == 0) {
				// r位置结尾,真的有覆盖子串!
				// 看看这个覆盖子串能不能尽量短
				while (cnts[s[l]] > 0) {
					// l位置的字符能拿回
					cnts[s[l++]]--;
				}
				// 从while里面出来,
				// l....r就是r位置结尾的最小覆盖子串
				if (r - l + 1 < len) {
					len = r - l + 1;
					start = l;
				}
			}
		}
		return len == Integer.MAX_VALUE ? "" : str.substring(start, start + len);
	}

题目四:加油站

问题描述

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,
从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升
你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周
则返回出发时加油站的编号,否则返回 -1
如果存在解,则 保证 它是 唯一 的。

测试链接 : https://leetcode.cn/problems/gas-station/

算法思想

使用滑动窗口,首先用结余数组=gas[r] - cost[r];;车辆尝试从0~n-1出发,看能不能走一圈,如果当前窗口大小等于n了,代表这个开头是满足的。要不然看累加和是否>=0,>=0证明还能阔,否则就换下一个开头。所有开头都没就代表不存在。

代码如下

	public static int canCompleteCircuit(int[] gas, int[] cost) {
		int n = gas.length;
		// 车辆尝试从0~n-1出发,看能不能走一圈,l
		// r : 窗口即将进来数字的位置
		// len : 窗口大小
		// sum : 窗口累加和
		for (int l = 0, r = 0, len = 0, sum = 0; l < n; l++) {
			while (sum >= 0) {
				// 当前窗口累加和>=0,尝试扩
				if (len == n) {
					return l;
				}
				// r : 窗口即将进来数字的位置
				r = (l + (len++)) % n;
				sum += gas[r] - cost[r];
			}
			// sum < 0,此时l位置无法转一圈
			len--;
			sum -= gas[l] - cost[l];
		}
		return -1;
	}

题目五:替换子串得到平衡字符串

问题描述

有一个只含有 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符,且长度为 n 的字符串。
假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。
给你一个这样的字符串 s,请通过「替换一个子串」的方式,
使原字符串 s 变成一个「平衡字符串」。
你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。
请返回待替换子串的最小可能长度。
如果原字符串自身就是一个平衡字符串,则返回 0。

测试链接 : https://leetcode.cn/problems/replace-the-substring-for-balanced-string/

算法思想

较难理解,建议看视频

代码如下

	public static int balancedString(String str) {
		int n = str.length();
		int[] arr = new int[n];
		int[] cnts = new int[4];
		for (int i = 0; i < n; i++) {
			char c = str.charAt(i);
			arr[i] = c == 'W' ? 1 : (c == 'E' ? 2 : (c == 'R' ? 3 : 0));
			cnts[arr[i]]++;
		}
		// str : 长度是4的整数倍,n
		// 每种字符出现的个数 : n/4
		int require = n / 4;
		// 至少要修改多长的子串,才能做到四种字符一样多
		int ans = n;
		// 自由变化的窗口l....r
		for (int l = 0, r = 0; l < n; l++) {
			// l = 0, r= 0, 窗口0长度
			// l...r-1 : [l,r)
			while (!ok(cnts, r - l, require) && r < n) {
				// cnts : 窗口之外的统计
				cnts[arr[r++]]--;
			}
			// 1) l...r-1 [l,r) ,做到了!
			// 2) r == n,也没做到
			if (ok(cnts, r - l, require)) {
				ans = Math.min(ans, r - l);
			}
			// [l,r),不被cnts统计到的
			//   l+1
			cnts[arr[l]]++;
		}
		return ans;
	}

	// cnts : l...r范围上的字符不算!在自由变化的窗口之外,每一种字符的词频统计
	// len : 自由变化窗口的长度
	// require : 每一种字符都要达到的数量
	// 返回值 : 请问能不能做到
	public static boolean ok(int[] cnts, int len, int require) {
		for (int i = 0; i < 4; i++) {
			// 0 1 2 3
			if (cnts[i] > require) {
				return false;
			}
			// require - cnts[i] : 20 - 16 = 4
			len -= require - cnts[i];
		}
		return len == 0;
	}

题目六:K个不同整数的子数组

问题描述

给定一个正整数数组 nums和一个整数 k,返回 nums 中 「好子数组」 的数目。
如果 nums 的某个子数组中不同整数的个数恰好为 k
则称 nums 的这个连续、不一定不同的子数组为 「好子数组 」。
例如,[1,2,3,1,2] 中有 3 个不同的整数:1,2,以及 3。
子数组 是数组的 连续 部分

测试链接 : https://leetcode.cn/problems/subarrays-with-k-different-integers/

算法思想

问题转换:恰好由 k个不同整数组成的子数组=子数组数字种类不超过k的个数-子数组数字种类不超过k-1的个数.子数组种类不超过k的个数可以用滑动窗口来做。

代码如下

	public static int subarraysWithKDistinct(int[] arr, int k) {
		return numsOfMostKinds(arr, k) - numsOfMostKinds(arr, k - 1);
	}

	public static int MAXN = 20001;

	public static int[] cnts = new int[MAXN];

	// arr中有多少子数组,数字种类不超过k
	// arr的长度是n,arr里的数值1~n之间
	public static int numsOfMostKinds(int[] arr, int k) {
		Arrays.fill(cnts, 1, arr.length + 1, 0);
		int ans = 0;
		for (int l = 0, r = 0, collect = 0; r < arr.length; r++) {
			// r(刚进)
			if (++cnts[arr[r]] == 1) {
				collect++;
			}
			// l.....r    要求不超过3种,已经4种,l往右(吐数字)
			while (collect > k) {
				if (--cnts[arr[l++]] == 0) {
					collect--;
				}
			}
			//以r结尾的子数组
			// l.....r不超过了
			// 0...3
			// 0~3
			// 1~3
			// 2~3
			// 3~3
			ans += r - l + 1;
		}
		return ans;
	}

题目七:至少有K个重复字符的最长子串

问题描述

给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串
要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度
如果不存在这样的子字符串,则返回 0。

测试链接 : https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/

算法思想

建议看视频,可以用滑动窗口,也可以用动态规划

代码如下

	public static int longestSubstring(String str, int k) {
		char[] s = str.toCharArray();
		int n = s.length;
		int[] cnts = new int[256];
		int ans = 0;
		// 每次要求子串必须含有require种字符,每种字符都必须>=k次,这样的最长子串是多长
		for (int require = 1; require <= 26; require++) {
			Arrays.fill(cnts, 0);
			// collect : 窗口中一共收集到的种类数
			// satisfy : 窗口中达标的种类数(次数>=k)
			for (int l = 0, r = 0, collect = 0, satisfy = 0; r < n; r++) {
				cnts[s[r]]++;
				if (cnts[s[r]] == 1) {
					collect++;
				}
				if (cnts[s[r]] == k) {
					satisfy++;
				}
				// l....r 种类超了!
				// l位置的字符,窗口中吐出来!
				while (collect > require) {
					if (cnts[s[l]] == 1) {
						collect--;
					}
					if (cnts[s[l]] == k) {
						satisfy--;
					}
					cnts[s[l++]]--;
				}
				// l.....r : 子串以r位置的字符结尾,且种类数不超的,最大长度!
				if (satisfy == require) {
					ans = Math.max(ans, r - l + 1);
				}
			}
		}
		return ans;
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值