题目
给你一个字符串 s
,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 ‘a’,‘e’,‘i’,‘o’,‘u’ ,在子字符串中都恰好出现了偶数次。
示例 1:
输入:s = "eleetminicoworoep"
输出:13
解释:最长子字符串是 "leetminicowor" ,它包含 e,i,o 各 2 个,以及 0 个 a,u 。
示例 2:
输入:s = "leetcodeisgreat"
输出:5
解释:最长子字符串是 "leetc" ,其中包含 2 个 e 。
示例 3:
输入:s = "bcbcbc"
输出:6
解释:这个示例中,字符串 "bcbcbc" 本身就是最长的,因为所有的元音 a,e,i,o,u 都出现了 0 次。
提示:
1 <= s.length <= 5 x 10^5
s
只包含小写英文字母。
解题思路
注:参考Leetcode官方解答。
先考虑暴力方法,首先枚举所有子串,遍历子串中的所有字符,统计元音字母出现的个数。如果符合条件,我们就更新答案,这是O(n^3)的解法,显然不能通过。
考虑怎么优化,对于一个区间,我们可以用两个前缀和的差值,得到其中某个字母的出现次数。对每个元音字母维护一个前缀和,定义 pre[i][k] 表示在字符串前 i 个字符中,第 k 个元音字母一共出现的次数。假设我们需要求出 [j,i] 这个区间的子串是否满足条件,那么我们可以用 pre[i][k]-pre[j-1][k],在 O(1) 的时间得到第 k 个元音字母出现的次数。对于每一个元音字母,我们都判断一下其是否出现偶数次即可。
虽然利用前缀和优化了统计子串的时间复杂度,但是枚举所有子串的复杂度仍需要 O(n^2),还需要继续进行优化,避免枚举所有子串。我们考虑枚举字符串的每个位置 i,计算以它结尾的满足条件的最长字符串长度。其实我们要做的就是快速找到最小的j∈[0,i),满足 pre[i][k]-pre[j][k](即每一个元音字母出现的次数)均为偶数,那么以 i 结尾的最长字符串 s[j+1,i]的长度就是 i-j。
因为有一个数学性质:奇数减奇数等于偶数,偶数减偶数等于偶数。所以对于满足条件的子串来说,两个前缀和 pre[i][k] 和 pre[j][k]的奇偶性一定是相同的。因此我们可以对前缀和稍作修改,从维护元音字母出现的次数改作维护元音字母出现次数的奇偶性。这样我们只要实时维护每个元音字母出现的奇偶性,那么 s[j+1,i] 满足条件当且仅当对于所有的 k,pre[i][k] 和 pre[j][k] 的奇偶性相同,此时我们就可以利用哈希表存储每一种奇偶性(即考虑所有的元音字母)对应最早出现的位置,边遍历边更新答案。
又因为出现次数的奇偶性无非就两个值,0 代表出现了偶数次,1 代表出现了奇数次,我们可以将其压缩到一个二进制数中,第 k 位的 0 或 1 代表了第 k 个元音字母出现的奇偶性。这样我们就可以将 5 个元音字母出现次数的奇偶性压缩到了一个二进制数中,且连续对应了二进制数的 [00000,11111]的范围,转成十进制数即 [0,31]。因此可以不用哈希表,直接用一个长度为 32 的数组来存储对应状态出现的最早位置即可。
具体做法如下:如果坐标 j 对应的状态码与坐标 i 对应的状态码相同,那么他们俩中间的元音字母数一定是偶数。所以我们每次求出一个坐标的状态码的时候就去查询这个状态码前面是否存在,如果存在,那么就计算一下之间子串的长度,并更新最大子串长度。
(详细步骤见代码注解)
复杂度分析:
时间复杂度:O(n),其中 n 为字符串 s 的长度。我们只需要遍历一遍字符串即可求得答案,因此时间复杂度为 O(n)。
空间复杂度:O(S),其中 S 表示元音字母压缩成一个状态数的最大值,在本题中 S = 32。我们需要对应 S 大小的空间来存放每个状态第一次出现的位置,因此需要 O(S) 的空间复杂度。
代码
Python代码如下:
class Solution:
def findTheLongestSubstring(self, s: str) -> int:
n = len(s)
res = 0 # 记录最大长度
status = 0 # 初始状态为0,表示所有元音字母出现了0次
pos = [-2]*32 # 记录状态出现的位置,初值设置为-2(比-1小的数都可以)
pos[0] = -1 # 状态码为0设置在第一个元素的左边,即-1的位置
for i in range(n):
if s[i] == 'a':
status ^= 1<<0 # 00001
elif s[i] == 'e':
status ^= 1<<1 # 00010
elif s[i] == 'i':
status ^= 1<<2 # 00100
elif s[i] == 'o':
status ^= 1<<3 # 01000
elif s[i] == 'u':
status ^= 1<<4 # 10000
if pos[status] != -2: # 如果状态码不为-2,就表示重复出现了这个状态码,那么更新一下最大长度
res = max(res, i-pos[status])
else: # 否则就表示第一次出现
pos[status] = i
return res
Java代码如下:
class Solution {
public int findTheLongestSubstring(String s) {
int n = s.length();
int res = 0;
int status = 0;
int[] pos = new int[32];
Arrays.fill(pos, -2);
pos[0] = -1;
for(int i=0; i<n; i++){
char c = s.charAt(i);
if(c == 'a'){
status ^= 1<<0;
}else if(c == 'e'){
status ^= 1<<1;
}else if(c == 'i'){
status ^= 1<<2;
}else if(c == 'o'){
status ^= 1<<3;
}else if(c == 'u'){
status ^= 1<<4;
}
if(pos[status] != -2){
res = Math.max(res, i-pos[status]);
}else{
pos[status] = i;
}
}
return res;
}
}