【图解算法】模板的优化与进阶——滑动窗口专题

Part1. 模板题

 
题目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逻辑。

【图解算法】模板的优化与进阶——滑动窗口专题_第1张图片

优化后的模板如下(与原模板对比,注意改动的地方即可):

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)
}

 
 
 
 
 
 
 
 

Part2. 进阶题

 
题目千变万化,但从模板抽象出的核心思想固定不变:

两个while嵌套 + R主动前移,L被迫跟进 + 贪吃蛇的优化思路

 

题目4:无重复字符的最长子串

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

输入:s = “suukiiii”
输出:3
解释:最长子串为"uki"
 
理解: 简单题。进阶之处在于HashSetHashMap的使用,这经常用于判断区间合法性。

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个

【图解算法】模板的优化与进阶——滑动窗口专题_第2张图片

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

你可能感兴趣的:(#,图解算法,算法,滑动窗口)