1. 程式人生 > >[LeetCode] 340. Longest Substring with At Most K Distinct Characters

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