1. 问题背景与核心挑战
这道LeetCode 1371题出现在2020年周赛第179场,属于字符串处理类的中等难度题目。题目要求我们找到给定字符串中最长的子串,其中包含的每个元音字母(a,e,i,o,u)都必须出现偶数次(包括0次)。乍看简单,实则暗藏玄机。
我第一次看到这个题目时,直觉想到的是暴力解法——枚举所有可能的子串,然后检查每个子串的元音计数。对于一个长度为n的字符串,这样的时间复杂度是O(n³),当n达到10^5时显然无法承受。这迫使我必须寻找更聪明的办法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 关键突破:状态压缩与前缀和
2.1 状态表示的精妙设计
解决这个问题的核心在于如何高效地表示和比较元音的奇偶状态。我们注意到只需要关心每个元音出现次数的奇偶性,而非具体次数。这提示我们可以用5位二进制数来表示状态:
- 每位对应一个元音(顺序可以是a,e,i,o,u)
- 0表示该元音出现偶数次
- 1表示奇数次
例如:
- 00000表示所有元音都出现偶数次(包括没出现)
- 00101表示i和u出现奇数次,其他偶数次
2.2 前缀和的应用
我们定义前缀和数组prefix,其中prefix[i]表示从字符串开始到第i个字符(索引i-1)的状态。这样,子串s[j...i]的状态可以通过prefix[i+1] XOR prefix[j]得到。如果结果为0,说明这个子串满足所有元音出现偶数次。
这个XOR操作的精妙之处在于:相同状态的前缀相减(XOR)会抵消中间的变化,只保留子串本身的奇偶变化。
3. 算法实现详解
3.1 初始化与状态转移
python复制def findTheLongestSubstring(s: str) -> int:
vowel_map = {'a': 1, 'e': 2, 'i': 4, 'o': 8, 'u': 16}
state_map = {0: -1} # 初始状态00000出现在索引-1处
max_len = 0
current_state = 0
for i, char in enumerate(s):
if char in vowel_map:
current_state ^= vowel_map[char]
if current_state in state_map:
max_len = max(max_len, i - state_map[current_state])
else:
state_map[current_state] = i
return max_len
3.2 代码逐行解析
vowel_map定义了每个元音对应的二进制位state_map记录每种状态第一次出现的索引,初始状态00000在索引-1处- 遍历字符串时:
- 遇到元音就翻转对应位的状态(使用XOR)
- 如果当前状态之前出现过,计算当前索引与首次出现的差值得子串长度
- 否则记录这个状态的首次出现位置
3.3 复杂度分析
- 时间复杂度:O(n),只需一次遍历
- 空间复杂度:O(1),因为状态最多有2^5=32种可能
4. 边界条件与测试案例
4.1 典型测试案例
python复制测试案例1:
输入:"eleetminicoworoep"
输出:13
解释:最长子串"leetminicowor"中,e,i,o各出现2次
测试案例2:
输入:"leetcodeisgreat"
输出:5
解释:"leetc"中e出现2次,其他元音0次
测试案例3:
输入:"bcbcbc"
输出:6
解释:整个字符串没有元音,满足条件
4.2 特殊边界处理
- 全辅音字符串:应返回整个字符串长度
- 单个字符的元音:无法形成有效子串(除非允许长度为1,但题目要求偶数次)
- 交替出现的元音:需要验证状态转移的正确性
5. 算法优化与变种
5.1 空间优化技巧
虽然状态数固定为32,但可以使用数组代替哈希表:
python复制state_array = [-1] * 32
state_array[0] = -1 # 初始状态
这样查找时间从O(1)变为真正的常数时间。
5.2 类似问题扩展
这种方法可以推广到:
- 其他字符集的条件统计
- 出现次数为k的倍数(而不仅是奇偶)
- 多个条件的组合统计
例如,LeetCode 1542题"找出最长的超赞子字符串"就是此方法的进阶应用。
6. 常见错误与调试技巧
6.1 典型错误模式
- 初始状态设置错误:忘记将state_map初始化为
- 索引偏移错误:混淆子串长度计算时的i和i+1
- 元音判断不完整:漏掉某些元音字母的大小写
6.2 调试建议
- 打印状态变化轨迹:
python复制print(f"i={i}, char={char}, state={bin(current_state)}")
- 验证小测试案例手工计算
- 特别注意第一个和最后一个字符的处理
7. 实际应用场景
这种状态压缩技巧不仅在算法题中有用,在实际开发中也有广泛应用:
- 网络协议中的奇偶校验
- 数据同步中的版本比对
- 配置系统的状态追踪
- 游戏开发中的成就系统检查
理解这种位操作的思想,可以帮助我们在处理复杂状态问题时找到更高效的解决方案。
8. 个人解题心得
我在第一次遇到这个问题时,花了整整两个小时才理解状态压缩的妙处。关键突破点是意识到:
- 不需要计数,只需要奇偶状态
- 相同状态之间的子串必然满足条件
- XOR操作可以高效计算状态变化
建议初学者从简单案例入手,比如字符串"a","aa","ab"等,手工模拟算法过程,体会状态变化的规律。这种位运算的技巧需要一定时间的训练才能熟练掌握,但一旦掌握,可以解决一大类看似复杂的问题。
