题目
题目来源:力扣(LeetCode)
给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。
示例 1:
输入:s = “aaabb”, k = 3
输出:3
解释:最长子串为 “aaa” ,其中 ‘a’ 重复了 3 次。
示例 2:
输入:s = “ababbc”, k = 2
输出:5
解释:最长子串为 “ababb” ,其中 ‘a’ 重复了 2 次, ‘b’ 重复了 3 次。
思路分析
对于字符串 s,如果存在某个字符 ch,它的出现次数大于 0 且小于 k,则任何包含 ch 的子串都不可能满足要求。那么我们将字符串按照 ch 切分成若干段,则满足要求的最长子串一定出现在某个被切分的段内,而不能跨越一个或多个段。因此,可以考虑分治的方式求解本题。
算法
- 遍历字符串统计所有字符出现的次数
- 遍历所有字符,找到出现次数小于 k 的字符,以当前字符为分割点,将字符串 s 切分成若干个子串
- 继续对子串执行步骤1 和步骤2,找出符合要求的最长子串
举个简单例子:
假设字符串 str = “eaaabcbcbcdeeeee”, k = 3的时候, 各个字符出现频率为:
a:3, b:3, c:3, d:1, e: 5
由于 d 出现的次数小于 k,因此用 d 作为切割点, 将字符串分割为两个子串然后再对子串进行查找:
str1 = “aaabcbcbc”, 子串 str1 中的每个字符出现的次数都大于等于 k,符合要求
str2 = “eeeee”, 子串 str2 中的字符出现的次数大于 k,符合要求,但由于其长度为 5,小于子串 str1 的长度 9, str2 不是最长子串,因此 str2 不是最优解。
代码
/**
* @param {string} s
* @param {number} k
* @return {number}
*/
var longestSubstring = function (s, k) {
const n = s.length;
// 对原始字符串s的切割过程
return dfs(s, k);
}
// 对字符串进行tokenize,每个片段重复计算是否满足要求
const dfs = (s, k) => {
if (!s) return 0;
const cnt = new Array(26).fill(0);
// 统计当前片段中字符出现的次数
for (const ch of s) {
cnt[ch.charCodeAt() - 'a'.charCodeAt()]++;
}
// 遍历所有字符
for (let i = 0; i < 26; i++) {
if (cnt[i] && cnt[i] < k) {
// 当前字符出现的次数小于 k,那么任何包含当前字符的 子串都不满足要求,按照当前字符将字符串切分成若干段
const subStrings = s.split(String.fromCharCode(i + 'a'.charCodeAt()))
// 最后答案的字符串的长度
let ret = 0;
// 遍历切分后的子串
for (const subString of subStrings) {
// 递归求解符合要求的子串
const len = dfs(subString, k)
// 符合要求的最长子串
ret = Math.max(len, ret)
}
return ret;
}
}
// 如果每个字符都都满足大于 k,会走到此处,当前串满足条件,直接返回字符串长度即可
return s.length;
};