[LeetCode]395. Longest Substring with At Least K Repeating Characters滑动窗口+分治(JavaScript)
题目描述
LeetCode原题链接:395. Longest Substring with At Least K Repeating Characters
Given a string s
and an integer k
, return the length of the longest substring of s
such that the frequency of each character in this substring is greater than or equal to k
.
Example 1:
Input: s = "aaabb", k = 3 Output: 3 Explanation: The longest substring is "aaa", as 'a' is repeated 3 times.
Example 2:
Input: s = "ababbc", k = 2 Output: 5 Explanation: The longest substring is "ababb", as 'a' is repeated 2 times and 'b' is repeated 3 times.
Constraints:
1 <= s.length <= 104
s
consists of only lowercase English letters.1 <= k <= 105
解法一:分治法(divide and conquer)
根据题目要求可以知道,符合条件的字串的长度至少是k(对应字串中只有一个字母且该字母出现k次的情况);同时,符合条件的字串中所有字母出现的次数都要>=k,换句话说,原字符串中出现次数<k的字母一定不可能出现在满足条件的字串中。那么这些不合法的字母就是分治法中“分”的点。所以,对于原字符串,假设存在这样的分割点pivot,那么我们通过递归不断切分,那么,longestSubstring(s, k) = max(longestSubstring(s.slice(0, pivot), k), longestSubstring(s.slice(pivot + 1), k))。在“治”的过程中,如果当前入参的字符串的长度小于k,直接返回0;如果遍历完整个字符串都找不到上述可以分割的点,那么说明原字符串本身就是符合条件的最长子串,返回s.length。
1 var longestSubstring = function(s, k) { 2 if(s.length < k) return 0; 3 let frequency = new Map(); 4 for(let char of s) { 5 frequency.set(char, frequency.has(char) ? frequency.get(char) + 1 : 1); 6 } 7 for(let pivot = 0; pivot < s.length; pivot++) { 8 if(frequency.get(s[pivot]) < k) { 9 let nextPivot = pivot + 1; 10 while(nextPivot < s.length && frequency.get(s[nextPivot]) < k) nextPivot++; 11 return Math.max(longestSubstring(s.slice(0, pivot), k), longestSubstring(s.slice(nextPivot), k)); 12 } 13 } 14 return s.length; 15 };
也可以单独用一个util函数来处理迭代:
1 var longestSubstring = function(s, k) { 2 return recursion(s, k, 0, s.length); 3 }; 4 5 var recursion = function(s, k, start, end) { 6 if(end - start < k) return 0; 7 let frequency = new Map(); 8 for(let i = start; i < end; i++) { 9 frequency.set(s[i], frequency.has(s[i]) ? frequency.get(s[i]) + 1 : 1); 10 } 11 for(let pivot = start; pivot < end; pivot++) { 12 if(frequency.get(s[pivot]) < k) { 13 let nextPivot = pivot + 1; 14 while(nextPivot < end && frequency.get(s[nextPivot]) < k) nextPivot++; 15 return Math.max(recursion(s, k, start, pivot), recursion(s, k, nextPivot, end)); 16 } 17 } 18 return end - start; 19 }
解法二:滑动窗口(sliding window)
分析解法一的时候我们讨论过,符合条件的子串中至少有一个不同的字母。那么至多有多少个不同的字母呢?因为题目说了s中全部都是小写英文字母,英文字母最多有26个,所以至多也不会超过26。还能再缩小一下最大值么?可以。这个最大值和原字符串中不同字母的个数是相同的(即原字符串中所有不同字母的出现次数都>=k),设这个值为maxUniqueChar。于是,uniqueChar的范围就是[1, maxUniqueChar]。我们从1开始遍历这个区间,假设当前遍历的是curUniqueChar,我们用滑动窗口去圈一个子串,要求窗口内的子串有且仅有curUniqueChar个不同的字母,并且这些字母出现的次数都要>=k。过程中用一个maxLen去记录全局最大值。字母的出现次数用hashmap来记录(或者是一个长度为26的数组)
我们知道滑动窗口算法的核心是左右边界start、end的移动条件:窗口内的东西满足条件时右移end扩大窗口,一旦不满足就右移start缩小窗口。对应这道题,如果[start, end]范围内的子串中不同字母的个数unqiue <= curUniqueChar,则右移end扩大窗口,否则右移left缩小窗口。每次扩大或缩小的时候,都要注意新加入或移除的字母对unqiue的影响(如果新加入窗口的字母之前没出现过,unique就要自增1;如果移出的元素不再出现在缩小后的窗口内,unique就要自减1)。此外不要忘记另一个限制——每个不同的字母在窗口中的出现次数。我们用一个计数器kCnt来记录,右移end过程中一旦某一个字母出现次数等于k了,kCnt就自增1;右移start过程中,一旦,如果移出的元素恰好已经出现了k次,说明移出它后该字母在缩小后的窗口中出现的次数就要小于k了,此时需要将kCnt自减1。最后,当unqiue == curUniqueChar && unqiue == kCnt时,说明找到了符合所有条件的子串,更新maxLen。
1 var longestSubstring = function(s, k) { 2 let maxLen = 0; 3 let maxUniqueChar = (new Set(s)).size; // 用set来快速求出最大边界 4 let frequency = new Array(26); 5 for(let curUniqueChar = 1; curUniqueChar <= maxUniqueChar; curUniqueChar++) { 6 let start = 0, end = 0, kCnt = 0, unique = 0, idx = 0; 7 frequency.fill(0); // 重置计数数组 8 while(end < s.length) { 9 if(unique <= curUniqueChar) { 10 idx = s[end].charCodeAt() - 'a'.charCodeAt(); 11 if(frequency[idx] == 0) { 12 unique++; 13 } 14 if(++frequency[idx] == k) kCnt++; 15 end++; 16 } 17 else { 18 idx = s[start].charCodeAt() - 'a'.charCodeAt(); 19 if(frequency[idx] == k) kCnt--; 20 if(--frequency[idx] == 0) { 21 unique--; 22 } 23 start++; 24 } 25 if(unique == curUniqueChar && unique == kCnt) { 26 maxLen = Math.max(maxLen, end - start); 27 } 28 } 29 } 30 return maxLen; 31 };