Sliding Window Algorithm Questions
LeetCode 424. Longest Repeating Character Replacement
Given a string that consists of only uppercase English letters, you can replace any letter in the string with another letter at most k times. Find the length of a longest substring containing all repeating letters you can get after performing the above operations.
Algorithm: For a given window, always keep the letter that appears the most and replace other letters. This minimizes replacement times. For a window of [i, j] with length L = j - i + 1, as long as L - maxFrequency <= k, we can keep extending this window's right bound by 1. As soon as L - maxFrequency > k, we know that we've just reached the limit of all windows that start at index i and the max value of all these windows is L - 1(including the current letter requires k + 1 replacements). At this point, update the current max value then move forward the left bound of possible windows to i + 1 and decrement the frequency of letter at i by 1.
When calculating the max frequency in a given window, we don't need to go through the entire frequency array. Since any possible new max frequency can only come from either the new frequency of the newly added letter or the previous max frequency, we just need to compare these two.
class Solution {
public int characterReplacement(String s, int k) {
int[] freq = new int[26];
int start = 0, end = 0, maxFreq = 0, maxLen = 0;
while(end < s.length()) {
freq[s.charAt(end) - 'A']++;
maxFreq = Math.max(maxFreq, freq[s.charAt(end) - 'A']);
int len = end - start + 1;
if(len - maxFreq > k) {
maxLen = Math.max(maxLen, len - 1);
freq[s.charAt(start) - 'A']--;
start++;
}
end++;
}
return Math.max(maxLen, end - start);
}
}
LeetCode 438. Find All Anagrams in a String
Given a string s and a non-empty string p, find all the start indices of p's anagrams in s.
Strings consists of lowercase English letters only and the length of both strings s and p will not be larger than 20,100.
The order of output does not matter.
Algorithm: Preprocess string p to get a character frequency difference map. Take a window of size p.length(), and slide it from left to right through s. Each slide has 1 add and 1 deletion operation to the diff map. When a character's diff becomes 0, remove this key from diff map. When the entire diff map is empty, the current window is one anagram of p. Since it is a diff map, each time we add a character, we decrement its count by 1, representing the difference is getting smaller; each time we remove a character, we increment its count by 1, representing the difference is getting bigger.
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
if(p.length() > s.length()) {
return res;
}
Map<Character, Integer> diff = new HashMap<>();
for(int i = 0; i < p.length(); i++) {
diff.put(p.charAt(i), diff.getOrDefault(p.charAt(i), 0) + 1);
}
for(int i = 0; i < p.length(); i++) {
diff.put(s.charAt(i), diff.getOrDefault(s.charAt(i), 0) - 1);
if(diff.get(s.charAt(i)) == 0) {
diff.remove(s.charAt(i));
}
}
if(diff.size() == 0) {
res.add(0);
}
for(int i = p.length(); i < s.length(); i++) {
char start = s.charAt(i - p.length()), end = s.charAt(i);
diff.put(start, diff.getOrDefault(start, 0) + 1);
if(diff.get(start) == 0) {
diff.remove(start);
}
diff.put(end, diff.getOrDefault(end, 0) - 1);
if(diff.get(end) == 0) {
diff.remove(end);
}
if(diff.size() == 0) {
res.add(i - p.length() + 1);
}
}
return res;
}
}
LeetCode 76. Minimum Window Substring
Given a string S and a string T, find the minimum window in S which will contain all the characters in T in complexity O(n).
Example:
Input: S = "ADOBECODEBANC", T = "ABC"
Output: "BANC"
Note:
- If there is no such window in S that covers all characters in T, return the empty string
""
. - If there is such window, you are guaranteed that there will always be only one unique minimum window in S.
Similarly with Find All Anagrams in a String, we construct a difference map from T. Finding a minimum window that contains all the characters of T is quite different with finding an anagram of T. The latter has a fixed window size and does not allow characters that do not exist in T. It does not allow extra characters than needed. A minimum window allow both. For example, if S = "ABBDF" and T = "ABF", then the entire S contains T, but there is no substrings that is an anagram of T.
Algorithm
1. construct a difference map from T; Use a counter variable to track if a given window contains all characters from T. counter is initialized to be the size of the difference map. counter-- means for a particular character from T, we've matched all its occurences and counters++ means we can't match all the occurences of a particular character from T.
2. Keep a sliding window. As long as counter != 0, keep extending the right bound and update the diff map: (a) if the current character does not exist in T, ignore; (b) otherwise update the diff map. If the new count of this character is 0, counter--.
3. When counter == 0, the current sliding window contains all characters from T; update min length and keep contracting the left bound if possible: (a) if the current character does not exist in T, ignore; (b) otherwise update the diff map. If the new count of this character is > 0, counter++.
class Solution {
public String minWindow(String s, String t) {
if(s.length() < t.length() || t.length() == 0) return "";
Map<Character, Integer> diff = new HashMap<>();
for(int i = 0; i < t.length(); i++) {
diff.put(t.charAt(i), diff.getOrDefault(t.charAt(i), 0) + 1);
}
int left = 0, right = 0, minLen = Integer.MAX_VALUE, minLeft = 0, minRight = 0, counter = diff.size();
while(right < s.length()) {
char addChar = s.charAt(right);
if(diff.containsKey(addChar)) {
int currCount = diff.get(addChar);
diff.put(addChar, currCount - 1);
if(currCount == 1) {
counter--;
}
}
while(counter == 0) {
if(right - left + 1 < minLen) {
minLen = right - left + 1;
minLeft = left;
minRight = right;
}
char delChar = s.charAt(left);
if(diff.containsKey(delChar)) {
int currCount = diff.get(delChar);
diff.put(delChar, currCount + 1);
if(currCount == 0) {
counter++;
}
}
left++;
}
right++;
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(minLeft, minRight + 1);
}
}
LeetCode 727. Minimum Window Subsequence
Given strings S
and T
, find the minimum (contiguous) substring W
of S
, so that T
is a subsequence of W
.
If there is no such window in S
that covers all characters in T
, return the empty string ""
. If there are multiple such minimum-length windows, return the one with the left-most starting index.
Example 1:
Input:
S = "abcdebdde", T = "bde"
Output: "bcde"
Explanation:
"bcde" is the answer because it occurs before "bdde" which has the same length.
"deb" is not a smaller window because the elements of T in the window must occur in order.
Note:
- All the strings in the input will only contain lowercase letters.
- The length of
S
will be in the range[1, 20000]
. - The length of
T
will be in the range[1, 100]
.
A naive solution is to check all S substrings with length >= T.length. There will be O(N^2) such substrings, N is S.length(). Checking if T is a subsequence of such substring takes O(substring length) time. Obviously this solution is not feasible for an input string of up to 2 * 10^4 characters.
Dynamic Programming Intuition: Let's try to build the final result by first building substring S[0, i] and substring T[0, j]. Say we enforce that S[i] must be the last character of such an window that contains T[0, j] as a subsequence, no matter if S[i] contributes to subsequence T[0, j] or not. To solve this subproblem, we can apply recursion here. If S[i] == T[j], reduce subproblem to S[0, i - 1] and T[0, j - 1]; otherwise reduce to S[0, i - 1] and T[0, j]. We recursively solve smaller subproblems until we hit the base case. There will be overlapping subproblems and this is a hint for dynamic programming.
State:
dp[i][j]: the largest index LI such that S[LI, i] contains T[0, j] as subsequence.
Initialization:
Init all dp[i][j] to -1, representing such window does not exist initially.
dp[0][0] = 0 if S[0] == T[0]; dp[i][0] = i if S[i] == T[0]; otherwise dp[i][0] = dp[i - 1][0].
State Transition:
dp[i][j] = dp[i - 1][j - 1] if S[i] == T[j]; otherwise dp[i][j] = dp[i - 1][j]
Answer:
Min length of i - dp[i][T.length() - 1] + 1, for all dp[i][T.length() - 1] >= 0; If dp[i][T.length() - 1] < 0, it means there is no such window in S[0, i] to contains T as subsequence.
If there are ties, take the smallest i.
The running time is O(S.length() * T.length())
class Solution { public String minWindow(String S, String T) { int ls = S.length(), ts = T.length(); int[][] dp = new int[ls][ts]; for(int i = 0; i < ls; i++) { Arrays.fill(dp[i], -1); } if(S.charAt(0) == T.charAt(0)) { dp[0][0] = 0; } for(int i = 1; i < ls; i++) { if(S.charAt(i) == T.charAt(0)) { dp[i][0] = i; } else { dp[i][0] = dp[i - 1][0]; } } for(int i = 1; i < ls; i++) { for(int j = 1; j < ts && j <= i; j++) { if(S.charAt(i) == T.charAt(j)) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = dp[i - 1][j]; } } } int left = -1, right = ls, minLen = S.length() + 1; for(int i = ts - 1; i < ls; i++) { if(dp[i][ts - 1] >= 0) { int len = i - dp[i][ts - 1] + 1; if(len < minLen) { left = dp[i][ts - 1]; right = i; minLen = len; } } } if(minLen > S.length()) { return ""; } return S.substring(left, right + 1); } }