LeetCode 2981: 找出出现至少三次的最长特殊子串 I

1. 题目详解

1.1 原始题目

给你一个仅由小写英文字母组成的字符串 s

如果一个字符串仅由单一字符组成,则称它是一个 特殊字符串 。例如,字符串 “abc” 不是特殊字符串,而字符串 “ddd”、”zz” 和 “f” 是特殊字符串。

返回在 s 中出现 至少三次 的最长特殊子串的长度,如果不存在出现至少三次的特殊子串,则返回 -1

子串 是一个字符串中一段连续的字符序列。

1.2 题目约束

  • 3 <= s.length <= 50
  • s 仅由小写英文字母组成

1.3 题意分析

  1. 特殊字符串的定义

    • 只包含单一字符的字符串
    • 长度可以是任意的(1到字符串长度)
    • 例如:”aaa”是特殊字符串,”abc”不是
  2. 子串要求

    • 必须是连续的字符序列
    • 需要在原字符串中出现至少三次
    • 寻找满足条件的最长长度
  3. 返回值

    • 如果存在符合条件的特殊子串,返回最长的长度
    • 如果不存在,返回-1

1.4 示例详解

示例1:

1
2
3
4
5
6
7
8
输入:s = "aaaa"
输出:2
解释:
- 长度为1的特殊子串"a"出现4次
- 长度为2的特殊子串"aa"出现3次
- 长度为3的特殊子串"aaa"出现2次
- 长度为4的特殊子串"aaaa"出现1次
所以最长的出现至少三次的特殊子串长度为2

示例2:

1
2
3
4
5
输入:s = "abcdef"
输出:-1
解释:
- 所有特殊子串(单个字符)都只出现一次
- 不存在出现至少三次的特殊子串

示例3:

1
2
3
4
5
6
输入:s = "abcaba"
输出:1
解释:
- 字符'a'在位置0、3、5出现三次
- 没有更长的特殊子串出现三次
- 所以返回长度1

2. 解法1:基础哈希表法

2.1 思路

  1. 使用哈希表存储每个特殊子串及其出现次数
  2. 遍历字符串,对于每个位置:
    • 如果当前字符与前一个字符不同,开始新的特殊子串
    • 如果相同,扩展当前特殊子串并统计所有可能长度
  3. 最后筛选出现至少三次的最长子串

2.2 详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution:
def maximumLength(self, s: str) -> int:
# 存储特殊子串及其出现次数
dict_special_substrings = {s[0]: 1}
sub_start = 0 # 当前特殊子串的起始位置
sub_end = 1 # 当前特殊子串的结束位置

# 辅助函数:更新子串计数
def add_to_dict(substrings):
if substrings not in dict_special_substrings:
dict_special_substrings[substrings] = 1
else:
dict_special_substrings[substrings] += 1

# 遍历字符串
for idx in range(1, len(s)):
if s[idx] != s[idx - 1]: # 遇到不同字符
sub_start = idx
sub_end = idx + 1
add_to_dict(s[sub_start:sub_end])
else: # 遇到相同字符
sub_end += 1
# 统计所有可能长度的子串
for j in range(sub_start + 1, sub_end + 1):
add_to_dict(s[sub_start:j])

# 筛选出现至少3次的子串
dict_special_substrings_filter = {
k: v for k, v in dict_special_substrings.items() if v >= 3
}

# 返回最长长度或-1
if len(dict_special_substrings_filter) == 0:
return -1
else:
# 按长度降序排序
sorted_items = sorted(
dict_special_substrings_filter.items(),
key=lambda item: len(item[0]),
reverse=True,
)
k, v = sorted_items[0]
return len(k)

2.3 示例执行过程

以输入 "aaaa" 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
初始状态:dict_special_substrings = {"a": 1}

第1次循环 (idx=1):
- s[1] == s[0],扩展当前子串
- 添加 "a" 和 "aa"
- dict_special_substrings = {"a": 2, "aa": 1}

第2次循环 (idx=2):
- s[2] == s[1],扩展当前子串
- 添加 "a"、"aa" 和 "aaa"
- dict_special_substrings = {"a": 3, "aa": 2, "aaa": 1}

第3次循环 (idx=3):
- s[3] == s[2],扩展当前子串
- 添加 "a"、"aa"、"aaa" 和 "aaaa"
- dict_special_substrings = {"a": 4, "aa": 3, "aaa": 2, "aaaa": 1}

筛选后:{"a": 4, "aa": 3}
返回:2("aa"的长度)

2.4 复杂度分析

  • 时间复杂度:O(n²)
    • 外层循环遍历字符串:O(n)
    • 内层循环统计子串:O(n)
    • 排序操作:O(k log k),其中k是特殊子串数量
  • 空间复杂度:O(n²)
    • 需要存储所有可能的特殊子串

3. 解法2:优化的计数法

3.1 思路

  1. 使用(字符, 长度)作为键来统计频率,避免存储实际子串
  2. 通过数学公式计算子串数量,避免实际生成子串
  3. 一次遍历完成所有计算,大幅提升效率

3.2 详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution:
def maximumLength(self, s: str) -> int:
# 使用字典存储(字符,长度)对应的出现次数
counts = {}
curr_len = 1 # 当前连续相同字符的长度

# 遍历字符串(包含最后一个字符的处理)
for i in range(1, len(s) + 1):
# 当前字符与前一个字符相同,增加连续长度
if i < len(s) and s[i] == s[i-1]:
curr_len += 1
else:
char = s[i-1]
# 计算所有可能长度的子串数量
for length in range(1, curr_len + 1):
# 核心公式:curr_len - length + 1 计算特定长度的子串出现次数
counts[(char, length)] = counts.get((char, length), 0) + curr_len - length + 1
curr_len = 1

# 找出最大的满足条件的长度
result = -1
for (char, length), freq in counts.items():
if freq >= 3:
result = max(result, length)

return result

3.3 核心公式解释

对于连续序列长度为n的情况,长度为k的子串出现次数计算公式:n - k + 1

例如,对于序列 “aaaa” (n=4):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
长度为1的子串 "a":
- 位置0开始:a
- 位置1开始:a
- 位置2开始:a
- 位置3开始:a
总数 = 4 - 1 + 1 = 4次

长度为2的子串 "aa":
- 位置0开始:aa
- 位置1开始:aa
- 位置2开始:aa
总数 = 4 - 2 + 1 = 3次

长度为3的子串 "aaa":
- 位置0开始:aaa
- 位置1开始:aaa
总数 = 4 - 3 + 1 = 2次

长度为4的子串 "aaaa":
- 位置0开始:aaaa
总数 = 4 - 4 + 1 = 1次

3.4 示例执行过程

以输入 "aaabbb" 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第一段 "aaa":
curr_len = 3
处理 'a':
- counts[('a', 1)] = 3 # 长度1出现3次
- counts[('a', 2)] = 2 # 长度2出现2次
- counts[('a', 3)] = 1 # 长度3出现1次

第二段 "bbb":
curr_len = 3
处理 'b':
- counts[('b', 1)] = 3 # 长度1出现3次
- counts[('b', 2)] = 2 # 长度2出现2次
- counts[('b', 3)] = 1 # 长度3出现1次

最终结果:1(最长的出现至少3次的特殊子串长度为1)

3.5 复杂度分析

  • 时间复杂度:O(n)
    • 只需要遍历一次字符串
    • 内层循环的总执行次数不会超过字符串长度
  • 空间复杂度:O(n)
    • 只需要存储字符和长度的组合

4. 解法3:前缀和法

4.1 思路

  1. 使用嵌套的defaultdict记录每个字符的不同长度出现次数
  2. 通过前缀和快速判断是否存在足够的子串
  3. 从大到小遍历长度,找到符合条件的最大长度

4.2 详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from collections import defaultdict

class Solution:
def maximumLength(self, s: str) -> int:
# 创建嵌套的defaultdict,避免键不存在的检查
freq = defaultdict(lambda: defaultdict(int))
pre = s[0] # 前一个字符
length = 1 # 当前连续长度

# 初始化第一个字符
freq[s[0]][1] = 1

# 遍历字符串
for i in range(1, len(s)):
cur = s[i]
if cur == pre: # 当前字符与前一个相同
length += 1
freq[cur][length] += 1
else: # 遇到新字符
freq[cur][1] += 1
pre = cur
length = 1

# 查找最长满足条件的长度
ans = -1
for k in freq.keys(): # 遍历每个字符
presum = 0
# 从大到小遍历可能的长度
for j in range(len(s), 0, -1):
presum += freq[k][j] # 累加前缀和
if presum >= 3: # 找到满足条件的长度
ans = max(ans, j)
break
return ans

4.3 示例执行过程

以输入 "aabaa" 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
初始化:freq['a'][1] = 1

处理 'a':
freq['a'][2] = 1

处理 'b':
freq['b'][1] = 1

处理 'a':
freq['a'][1] += 1

处理 'a':
freq['a'][2] += 1

最终freq状态:
'a': {1: 2, 2: 2}
'b': {1: 1}

前缀和检查:
对于'a':
- 长度2:presum = 2 < 3
- 长度1:presum = 4 >= 3 ✓

对于'b':
- 长度1:presum = 1 < 3

返回:1

4.4 复杂度分析

  • 时间复杂度:O(n)
    • 遍历字符串:O(n)
    • 对每个字符的前缀和计算:O(1)
  • 空间复杂度:O(n)
    • 存储每个字符的长度频率

5. 总结与比较

5.1 三种解法比较

  1. 基础哈希表法

    • 优点:直观易懂
    • 缺点:时间和空间复杂度较高
  2. 优化的计数法

    • 优点:效率高,实现优雅
    • 缺点:需要理解数学公式
  3. 前缀和法

    • 优点:实现简洁,效率高
    • 缺点:需要理解前缀和概念

5.2 选择建议

  • 面试时建议使用解法2或解法3
  • 解法2更容易解释和理解
  • 实际应用中三种解法都是可接受的

5.3 扩展思考

  • 如何处理更大规模的输入?
  • 如何并行化处理?
  • 如何处理流式输入?