题目0:滑窗模板
public int SlidingWindow(String s) {
len = s.length(); // 串的长度
int[] count = new int[N]; // 用于统计区间内的信息
int L = 0, R = 0; // 窗口边界,这是一个闭区间[L, R]
int res = 0; // 窗口最大宽度(最终结果)
while (R < len) {
count[s.charAt(R)]++; // 修改统计值,因为R的移动改变了窗口
while (区间不符合题意) {
count[s.charAt(L)]--; // 修改统计值,因为L的移动改变了窗口
L++; // 左指针被动移动(虫子的尾部)
}
res = Math.max(res, R - L + 1); // 尝试更新res
R++; // 右指针主动移动(虫子的头部)
}
return res;
}
注:模板附有详细注释。若对此模板感到陌生,请移步这篇文章(戳这里)
题目1:替换后最长重复字符串的长度
给你一个仅由大写英文字母组成的字符串,你可以将任意的字符替换成另一个字符,但最多可替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。
输入:s = “AABABA”, k = 1
输出:4
解释:将中间的一个’B’替换为’A’,字符串变为 “AAAABA”
>>> 1.count数组统计的是区间内每个字母出现的次数,并维护了出现次数最多的字母的出现次数maxCnt
>>> 2.关于区间的合法性,用了一个巧妙的判断:maxCnt + k < R - L + 1()
>>> 即(出现次数最多的字母的出现次数+容错次数即可替换次数k)都无法填满这个窗口(R-L+1)的话,则非法了
class Solution {
public int characterReplacement(String s, int k) {
int len = s.length();
char[] charArr = s.toCharArray();
int[] count = new int[26];
int maxCnt = 0;
int L = 0, R = 0;
int res = 0;
while (R < len) {
count[charArr[R] - 'A']++;
maxCnt = Math.max(maxCnt, count[charArr[R] - 'A']);
while (maxCnt + k < R - L + 1) {
count[charArr[L] - 'A']--;
L++;
}
res = Math.max(res, R - L + 1);
R++;
}
return res;
}
}
题目2:最大连续1的个数
给定一个由若干 0 和 1 组成的数组 A,我们最多可以将 K 个值从 0 变成 1 。返回仅包含 1 的最长(连续)子数组的长度。
输入:A = “1101010”, k = 1
输出:4
>>> 因为只需要考虑1的长度,因此不需要使用数组,而是直接用一个int类型的count统计1即可
>>> 判断区间是否合法的写法和第一题一模一样
class Solution {
public int longestOnes(int[] A, int K) {
int len = A.length;
int count = 0;
int L = 0, R = 0;
int res = 0;
while (R < len) {
count += A[R];
while (count + K < R - L + 1) {
count -= A[L];
L++;
}
res = Math.max(res, R - L + 1);
R++;
}
return res;
}
}
题目3:尽可能使字符串相等
给你两个长度相同的字符串,s 和 t。将 s 中的字符转换为 t 中对应的字符需要的开销是两个字符的 ASCII 码值的差的绝对值。给出可用的最大预算(maxCost),请你尽可能转换,返回可以转化的最大长度。
输入:s = “abcd”, t = “bcdf”, maxCost = 3
输出:3
解释:s 中的 “abc” 可以变为 “bcd”。开销为 3,所以最大长度为 3
>>> count统计的是已经用的开销,非常典型的模板题
class Solution {
public int equalSubstring(String s, String t, int maxCost) {
int len = s.length();
char[] arr1 = s.toCharArray();
char[] arr2 = t.toCharArray();
int cost = 0;
int L = 0, R = 0;
int res = 0;
while (R < len) {
cost += Math.abs(arr1[R] - arr2[R]);
while (cost > maxCost) {
cost -= Math.abs(arr1[L] - arr2[L]);
L++;
}
res = Math.max(res, R - L + 1);
R++;
}
return res;
}
}
优化进阶(重点>_<)
上面的三个经典模板题(替换后最长重复字符串的长度、最大连续1的个数、尽可能使字符串相等)都有一个共同的特点,你发现了吗?那就是,这三道题的本质都是找到一个最大合法窗口,或者说找到毛毛虫的最大长度。
请进一步思考,如果此刻的合法窗口的大小为4,在它接下来的滑动过程中,我们还需要找到长度为3的合法窗口吗?不需要。因为我们在寻找一个最大值,接下来的滑动过程中窗口的大小只需要增大或者不变,但完全没必要减小———即窗口大小只需要增大或不变就可以了。换句话说,这是一只长度非严格递增的毛毛虫———一条贪吃蛇。
那么具体如何实现这只贪吃蛇呢?
经典的滑窗模板中,其遵循着右指针主动移动一步,左指针被动跟进0步或多步的原则,显然在这个过程中,窗口大小会出现收缩。再具体一点,右指针固定移动一步后,在内层while循环中,如果左指针被动跟进0步,则窗口扩展;如果左指针被动跟进1步,则窗口大小不变;如果左指针被动跟进2步及以上,则窗口收缩———那么改动方式呼之欲出,将执行0/1/2/3/4…次的while改为执行0/1次的if。
这样做的另一个好消息是,res的维护无比简单。之前之所以使用Math.max
,是因为res有可能收缩减小。而此时仅仅由两种情况———毛毛虫前脚主动移动后,新区间不合法,则后脚跟进从而长度不变;新区间合法,则后脚不动从而长度加1。这是一个if-else逻辑。
优化后的模板如下(与原模板对比,注意改动的地方即可):
public int SlidingWindow(String s) {
len = s.length();
int[] count = new int[N];
int L = 0, R = 0;
int res = 0;
while (R < len) {
count[s.charAt(R)]++;
if (区间不符合题意) { // 改动一:while改为if
count[s.charAt(L)]--;
L++;
} else {
res++; // 改动二:在else逻辑中实现res的递增
}
R++;
}
return res;
}
一只“身体可伸缩的毛毛虫”,被优化为了一条“一步一步移动的贪吃蛇”。值得注意的是,经过优化后的过程,已经不能保证每个时刻区间都合法了;而之所以我们敢这么做,是因为我们只关心窗口最大值,不关心这个窗口在何处。
还没有结束。
我们不妨再大胆一些,既然贪吃蛇的长度保证非严格递增,那么最终游戏结束时(R==len也就是越界时),这条贪吃蛇最终的长度,不就是窗口在整个过程中的最大值吗?!res值根本没有必要维护?!
下面是进一步优化的模板(再次对比一下)
public int SlidingWindow(String s) {
len = s.length();
int[] count = new int[N];
int L = 0, R = 0; // 改动一:彻底抛弃res
while (R < len) {
count[s.charAt(R)]++;
if (区间不符合题意) {
count[s.charAt(L)]--;
L++;
}
R++;
}
return R - L; // 改动二:返回最终的窗口大小即可(注意因为最终R多进行了一次自增,窗口大小不需+1)
}
题目千变万化,但从模板抽象出的核心思想固定不变:
两个while嵌套 + R主动前移,L被迫跟进 + 贪吃蛇的优化思路
题目4:无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入:s = “suukiiii”
输出:3
解释:最长子串为"uki"
理解: 简单题。进阶之处在于HashSet和HashMap的使用,这经常用于判断区间合法性。
class Solution {
public int lengthOfLongestSubstring(String s) {
int len = s.length();
char[] arr = s.toCharArray();
Set<Character> set = new HashSet<>();
int L = 0, R = 0;
int res = 0;
while (R < len) {
while (set.contains(arr[R])) {
set.remove(arr[L]);
L++;
}
res = Math.max(res, R - L + 1);
set.add(arr[R]);
R++;
}
return res;
}
}
class Solution {
public int lengthOfLongestSubstring(String s) {
int len = s.length();
char[] arr = s.toCharArray();
Map<Character, Integer> map = new HashMap<>();
int L = 0, R = 0;
int res = 0;
for(int i = 0; i < len; i++) { // R和i其实是同一个变量
if(map.containsKey(arr[R])) {
L = Math.max(L, map.get(arr[R]) + 1); // 使用Math.max是因为L的移动可能已经使map中出现无效值
}
map.put(arr[R], i);
res = Math.max(res, R - L + 1);
R++;
}
return res;
}
}
题目5:长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组
理解(重点>_<):
学习了Part1的内容后,我们能立马意识到这是一个滑动窗口。
但是…这道题与上面的滑动窗口有什么不同呢?没错,之前的题目是在找一个“最大值”,而本题是在找一个“最小值”!!!下面请尝试理解这两句话:
求符合条件的窗口的最大值的代码逻辑是:R主动移动一步后;while窗口不符合题意,L就被迫跟进一步,从而收缩到符合题意的最长状态;res值的维护在所有while执行结束之后
求符合条件的窗口的最小值的代码逻辑是:R主动移动一步后;while窗口符合题意,L就被迫跟进一步,从而收缩到符合题意的最短状态;res值的维护在每次while执行过程之中
因而模板也就转化为了如下(理解辅助记忆,甚至不需记忆):
public int SlidingWindow(String s) {
len = s.length();
int[] count = new int[N];
int L = 0, R = 0;
int res = Integer.MAX_VALUE; // 改动一:res初始化为最大值,每次尝试取min
while (R < len) {
count[s.charAt(R)]++;
while (区间符合题意) { // 改动二:while(区间非法)改为while(区间合法)
res = Math.min(res, R - L + 1); // 改动三:res的维护放到while之中
count[s.charAt(L)]--;
L++;
}
R++;
}
return res;
}
本题答案如下:
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int len = nums.length;
int count = 0;
int L = 0, R = 0;
int res = Integer.MAX_VALUE;
while (R < len) {
count += nums[R];
while (count >= target && L <= R) { // 细节一:防止L超过R
res = Math.min(res, R - L + 1);
count -= nums[L];
L++;
}
R++;
}
return res == Integer.MAX_VALUE ? 0 : res; // 细节二:防止res未被更新而返回MAX_VALUE
}
}
题目6:恰好包含K个的不同整数的子数组
给定一个正整数数组 A,如果 A 的某个子数组中不同整数的个数恰好为 K,则称 A 的这个连续、不一定不同的子数组为好子数组。返回 A 中好子数组的数目。
输入:A = [1,2,1,2,3], K = 2
输出:7
解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2]
理解:
这道题对比上面所有题的创新点在于,窗口的限制条件不再是最大或最小,而是恰好;求的也不是窗口尺寸,而是合法窗口的个数。
因此,这里引入了一个巧妙的思路——恰好K个 = 最多K个 - 最多K-1个
class Solution {
public int subarraysWithKDistinct(int[] A, int K) {
// 恰好K个 = 最多K个 - 最多K-1个
// 而“最多”,使用的就是最经典的滑窗模板
return countWithKDistinct(A, K) - countWithKDistinct(A, K - 1);
}
private int countWithKDistinct(int[] A, int K) {
int len = A.length;
int[] countA = new int[len + 1]; // 统计具体数字
int countK = 0; // 统计不同数字的个数
int L = 0, R = 0;
int res = 0;
while (R < len) {
if(countA[A[R]] == 0) {
countK++;
}
countA[A[R]]++;
while (countK > K) {
countA[A[L]]--;
if(countA[A[L]] == 0) {
countK--;
}
L++;
}
res += (R - L + 1); // 每次R主动移动后,合法窗口的大小即为对结果的贡献
R++;
}
return res;
}
}
E N D END END
B Y A L O L I C O N BY A LOLICON BYALOLICON