[LeetCode] 340. Longest Substring with At Most K Distinct Characters
Given a string, find the length of the longest substring T that contains at most k distinct characters.
Example 1:
Input: s = "eceba", k = 2
Output: 3
Explanation: T is "ece" which its length is 3.
Example 2:
Input: s = "aa", k = 1 Output: 2 Explanation: T is "aa" which its length is 2.
Solution 1. Brute force, O(N^3) runtime
For each possible substring, check if it contains at most k distinct characters, then get the longest.
1 class Solution { 2 public int lengthOfLongestSubstringKDistinct(String s, int k) { 3 int max = 0; 4 for(int i = 0; i < s.length(); i++) { 5 for(int j = i + 1; j <= s.length(); j++) { 6 String sub = s.substring(i, j); 7 Set<Character> chars = new HashSet<>(); 8 for(int t = i; t < j; t++) { 9 chars.add(s.charAt(t)); 10 } 11 if(chars.size() <= k) { 12 max = Math.max(max, sub.length()); 13 } 14 } 15 } 16 return max; 17 }
Solution 2. O(N^2) runtime
One thing that can be optimized in solution 1 is to use checked substring's characters counts to determine if the next substring of the same length satisfies the condition, from O(N) to O(1). This is done by maintaining a hashmap of each distinct character's frequency.
1. Initialize a hash map of each distinct character's frequency.
2. Starting from the possible maximum length n = s.length(), do the following.
a. from left to right, slide a window of length n to check if a substring meets the required condition. If it does, return; otherwise keep sliding one character at a time until reaching the right end.
b. reduce sliding window length by 1 and from right to left, slide a window of length n to check if a substring meets the required condition. If it does, return; otherwise keep sliding one character at a time until reaching the left end.
c. repeat a and b until n = 0.
1 class Solution { 2 public int lengthOfLongestSubstringKDistinct(String s, int k) { 3 int[] counts = new int[256]; 4 for(int i = 0; i < s.length(); i++) { 5 counts[s.charAt(i) - '\0']++; 6 } 7 int uniqueChars = 0; 8 for(int i = 0; i < 256; i++) { 9 if(counts[i] > 0) { 10 uniqueChars++; 11 } 12 } 13 int maxLen = s.length(); 14 boolean leftToRight = false; 15 16 for(; maxLen > 0; maxLen--) { 17 leftToRight = !leftToRight; 18 if(leftToRight) { 19 int leftIdx = 0, rightIdx = maxLen - 1; 20 if(rightIdx + 1 < s.length()) { 21 counts[s.charAt(rightIdx + 1) - '\0']--; 22 if(counts[s.charAt(rightIdx + 1) - '\0'] == 0) { 23 uniqueChars--; 24 } 25 } 26 if(uniqueChars <= k) { 27 return maxLen; 28 } 29 rightIdx ++; 30 while(rightIdx < s.length()) { 31 counts[s.charAt(leftIdx) - '\0']--; 32 if(counts[s.charAt(leftIdx) - '\0'] == 0) { 33 uniqueChars--; 34 } 35 leftIdx++; 36 37 if(counts[s.charAt(rightIdx) - '\0'] == 0) { 38 uniqueChars++; 39 } 40 41 counts[s.charAt(rightIdx) - '\0']++; 42 if(uniqueChars <= k) { 43 return maxLen; 44 } 45 rightIdx++; 46 } 47 } 48 else { 49 int rightIdx = s.length() - 1, leftIdx = s.length() - maxLen; 50 if(leftIdx >= 1) { 51 counts[s.charAt(leftIdx - 1) - '\0']--; 52 if(counts[s.charAt(leftIdx - 1) - '\0'] == 0) { 53 uniqueChars--; 54 } 55 } 56 if(uniqueChars <= k) { 57 return maxLen; 58 } 59 leftIdx--; 60 while(leftIdx >= 0) { 61 counts[s.charAt(rightIdx) - '\0']--; 62 if(counts[s.charAt(rightIdx) - '\0'] == 0) { 63 uniqueChars--; 64 } 65 rightIdx--; 66 67 if(counts[s.charAt(leftIdx) - '\0'] == 0) { 68 uniqueChars++; 69 } 70 counts[s.charAt(leftIdx) - '\0']++; 71 if(uniqueChars <= k) { 72 return maxLen; 73 } 74 leftIdx--; 75 } 76 } 77 } 78 return maxLen; 79 }
Solution 3. O(N) runtime
To further optimize solution 2, we have the this observation: for a substring that starts at index i, s[i, j - 1], if it meets the condition while s[i, j] does not, we do not need to backtrack j to i + 1.This is true because all the substring from index i + 1 to j - 1 are a smaller set of s[i, j - 1]. If s[i, j - 1] is a possible solution, it eliminates the need of checking a smaller solution. We just need to increment i by 1 and pick up where j stopped.
For substrings that start at index i, there are two cases when we will stop incrementing j.
1. s[i, j] has more than k distinct characters; In this case, we need to update the frequency of s.charAt(i), increment i by 1 then repeat the same process.
2. j is out of bound, j == s.length(); In this case, we've found the answer as no other qualified substrings will have a longer length(i can only be incremented).
The runtime is O(N) as both the start index i and end index j only move forward, which means at any given iteration, either i or j is incremented. This takes linear time to finish.
1 class Solution { 2 public int lengthOfLongestSubstringKDistinct(String s, int k) { 3 if(s == null || s.length() == 0 || k <= 0){ 4 return 0; 5 } 6 int[] counts = new int[256]; 7 int distinctChars = 0; 8 int endIdx = 0, maxLen = 0; 9 for(int startIdx = 0; startIdx < s.length(); startIdx++) { 10 while(endIdx < s.length()) { 11 if(counts[s.charAt(endIdx) - '\0'] > 0) { 12 counts[s.charAt(endIdx) - '\0']++; 13 } 14 else if(distinctChars == k){ 15 break; 16 } 17 else { 18 counts[s.charAt(endIdx) - '\0']++; 19 distinctChars++; 20 } 21 endIdx++; 22 } 23 maxLen = Math.max(maxLen, endIdx - startIdx); 24 if(endIdx == s.length()) { 25 break; 26 } 27 counts[s.charAt(startIdx) - '\0']--; 28 if(counts[s.charAt(startIdx) - '\0'] == 0) { 29 distinctChars--; 30 } 31 } 32 return maxLen; 33 } 34 }