class Solution:
    def longestSubstring(self, s: str, k: int) -> int:
        """ tc O(26N) sc O(26)
        main idea: sliding window.  inorder to prevent right pointer keep moving to the end, set up a stop condition ---  when number of unique chars (max_unique)== uniques, stop moving right pointer and start moving left pointer instead.
        1.  set up a unique number 1-26. create a hash map counting occurance of each character; 
            a variable(number_of_unique) to track for current substring, number of unique character 
            a variable (number_of_least_k) to tract for current substring, number of unique character that has repeating chars >= k
        2.  keep moving right pointer while  number_of_unique <=  max_unique
        3.  start moving left pointer when number_of_unique > max_unique 
        4.  when number_of_unique == max_unique and number_of_least_k == max_unique, update result 
        """
        res  =  0
        for max_unique in range(1,27):
            char_cnt = collections.defaultdict(int)
            l = r =  number_of_unique = number_of_least_k =  0
            
            while r < len(s):
                cur = s[r]
                if number_of_unique <= max_unique:
                    char_cnt[cur] += 1 
                    if char_cnt[cur] == 1:
                        number_of_unique += 1
                    if char_cnt[cur] == k:
                        number_of_least_k += 1
                    r += 1 
                else: 
                    cur = s[l]
                    if char_cnt[cur] == 1:
                        number_of_unique -= 1
                    if char_cnt[cur] == k:
                        number_of_least_k -= 1  
                    char_cnt[cur] -= 1
                    l += 1
                if number_of_unique == max_unique and number_of_least_k == max_unique:
                    res = max(res,r-l) # (r +1) - l or  r - (l-1)
        return res 

Divide and Conquer solution

class Solution:
    def longestSubstring(self, s: str, k: int) -> int:
        """ tc O(N^2) sc O(N) for max recursion depth 
        main idea: divide and conquer
        XXXYXYXX  regard Y as char that has accumutice count < k,  Y here is a cut point to break s into substring 
        1. count any string s 
        2. edge case : not s -> return 0
        3. check if  cut_point exists
                                        if no: return whole string 
                                        if yes: go to step 4 (find the cutting point) 
    
        4. loop through current string until encounter cut point, use substring so far as target string for next recursion,  and compare with result to get max result 
        5. update current start point with next position of cut point.  repeat step4-5 until loop ends, return result 
        """
        res  =  0
        if not s: return res 
        cnt = Counter(s)
        no_cut = True 
        for key, val in cnt.items():
            if val < k: 
                no_cut = False
                break
        if no_cut: return len(s)
        start = end = 0
        while end < len(s):
            if cnt[s[end]] < k :
                res = max(res, self.longestSubstring(s[start:end],k))
                start = end +1 
            end += 1 
        res = max(res,self.longestSubstring(s[start:],k)) # when there is no cut point after substring[start:] 
        return res