LeetCode 1371. 每个元音包含偶数次的最长子字符串

1371. 每个元音包含偶数次的最长子字符串

给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 'a','e','i','o','u' ,在子字符串中都恰好出现了偶数次。

示例 1:

输入:s = "eleetminicoworoep"
输出:13
解释:最长子字符串是 "leetminicowor" ,它包含 e,i,o 各 2 个,以及 0 个 au 

示例 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 只包含小写英文字母。

提示 1

Represent the counts (odd or even) of vowels with a bitmask.


提示 2

Precompute the prefix xor for the bitmask of vowels and then get the longest valid substring.

解法:哈希表 + 状态压缩

我们先来考虑暴力方法怎么做。最直观的方法无非就是枚举所有子串,遍历子串中的所有字符,统计元音字母出现的个数。如果符合条件,我们就更新答案,但这样肯定会因为超时而无法通过所有测试用例。

再回顾一下上面的操作,其实每个子串对应着一个区间,那么有什么方法可以在不重复遍历子串的前提下,快速求出这个区间里元音字母出现的次数呢?答案就是前缀和,对于一个区间,我们可以用两个前缀和的差值,得到其中某个字母的出现次数。

我们对每个元音字母维护一个前缀和,定义 pre[i][k] 表示在字符串前 i 个字符中,第 k 个元音字母一共出现的次数。假设我们需要求出 [l,r] 这个区间的子串是否满足条件,那么我们可以用 pre[r][k]−pre[l−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。

单单利用前缀和,我们无法找到 i 和 j 相关的恒等式。

这道题我们还有一个性质没有充分利用:我们需要找的子串中,每个元音字母都恰好出现了偶数次

偶数这个条件其实告诉了我们,对于满足条件的子串而言,两个前缀和 pre[i][k] 和 pre[j][k] 的奇偶性一定是相同的,因为小学数学的知识告诉我们:奇数减奇数等于偶数,偶数减偶数等于偶数。因此我们可以对前缀和稍作修改,从维护元音字母出现的次数改作维护元音字母出现次数的奇偶性。这样我们只要实时维护每个元音字母出现的奇偶性,那么 s[j+1,i] 满足条件当且仅当对于所有的 k,pre[i][k] 和 pre[j][k] 的奇偶性都相等,此时我们就可以利用哈希表存储每一种奇偶性(即考虑所有的元音字母)对应最早出现的位置,边遍历边更新答案。

题目做到这里基本上做完了,但是我们还可以进一步优化我们的编码方式,如果直接以每个元音字母出现次数的奇偶性为哈希表中的键,难免有些冗余,我们可能需要额外定义一个状态:

{
  a: cnta, // a 出现次数的奇偶性
  e: cnte, // e 出现次数的奇偶性
  i: cnti, // i 出现次数的奇偶性
  o: cnto, // o 出现次数的奇偶性
  u: cntu  // u 出现次数的奇偶性
}

将这么一个结构当作我们哈希表存储的键值,如果题目稍作修改扩大了字符集,那么维护起来可能会比较吃力。考虑到出现次数的奇偶性其实无非就两个值,0 代表出现了偶数次,1 代表出现了奇数次,我们可以将其压缩到一个二进制数中,第 k 位的 0 或 1 代表了第 k 个元音字母出现的奇偶性。举一个例子,假如到第 i 个位置,u o i e a 出现的奇偶性分别为 1 1 0 0 1,那么我们就可以将其压成一个二进制数 (11001)2=(25)10 作为它的状态。这样我们就可以将 5 个元音字母出现次数的奇偶性压缩到了一个二进制数中,且连续对应了二进制数的 [(00000)2, (11111)2 ] 的范围,转成十进制数即 [0,31]。因此我们也不再需要使用哈希表,直接用一个长度为 32 的数组来存储对应状态出现的最早位置即可。

Java版:

class Solution {
    public int findTheLongestSubstring(String s) {
        int n = s.length();
        int ans = 0;
        int status = 0;
        int[] pos = new int[1 << 5];
        Arrays.fill(pos, -1);
        pos[0] = 0;
        for (int i = 0; i < n; i++) {
            switch(s.charAt(i)) {
                case 'a':
                    status ^= 1 << 0;
                    break;
                case 'e':
                    status ^= 1 << 1;
                    break;
                case 'i':
                    status ^= 1 << 2;
                    break;
                case 'o':
                    status ^= 1 << 3;
                    break;
                case 'u':
                    status ^= 1 << 4;
                    break;
            }
            if (pos[status] >= 0) {
                ans = Math.max(ans, i + 1 - pos[status]);
            } else {
                pos[status] = i + 1;
            }
        }
        return ans;
    }
}

Python3版:

class Solution:
    def findTheLongestSubstring(self, s: str) -> int:
        ans = 0
        status = 0
        pos = [-1] * (1 << 5)
        pos[0] = 0
        for i, c in enumerate(s):
            if c == 'a':
                status ^= (1 << 0)
            elif c == 'e':
                status ^= (1 << 1)
            elif c == 'i':
                status ^= (1 << 2)
            elif c == 'o':
                status ^= (1 << 3)
            elif c == 'u':
                status ^= (1 << 4)
            
            if pos[status] >= 0:
                ans = max(ans, i + 1 - pos[status])
            else:
                pos[status] = i + 1
        return ans

复杂度分析

  • 时间复杂度:O(n),其中 n 为字符串 s 的长度。我们只需要遍历一遍字符串即可求得答案,因此时间复杂度为 O(n)。
  • 空间复杂度:O(S),其中 S 表示元音字母压缩成一个状态数的最大值,在本题中 S=32。我们需要对应 S 大小的空间来存放每个状态第一次出现的位置,因此需要 O(S) 的空间复杂度。
  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值