前端算法第一八二弹-每个元音包含偶数次的最长子字符串

134 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

给你一个字符串 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 次。

前缀和 + 状态压缩

我们对每个元音字母维护一个前缀和,定义 pre[i][k]\textit{pre}[i][k] 表示在字符串前 ii 个字符中,第 kk 个元音字母一共出现的次数。假设我们需要求出 [l,r][l,r] 这个区间的子串是否满足条件,那么我们可以用 pre[r][k]pre[l1][k]\textit{pre}[r][k]-pre[l-1][k],在 O(1)O(1) 的时间得到第 kk 个元音字母出现的次数。对于每一个元音字母,我们都判断一下其是否出现偶数次即可。

我们利用前缀和优化了统计子串的时间复杂度,然而枚举所有子串的复杂度仍需要 O(n2)O(n^2),不足以通过本题,还需要继续进行优化,避免枚举所有子串。我们考虑枚举字符串的每个位置 iii ,计算以它结尾的满足条件的最长字符串长度。其实我们要做的就是快速找到最小的 j[0,i)j \in [0,i),满足 pre[i][k]pre[j][k]\textit{pre}[i][k]-pre[j][k](即每一个元音字母出现的次数)均为偶数,那么以 iii 结尾的最长字符串 s[j+1,i]s[j+1,i]长度就是 iji-j

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

var findTheLongestSubstring = function(s) {
    const n = s.length;
    const pos = new Array(1 << 5).fill(-1);
    let ans = 0, status = 0;
    pos[0] = 0;
    for (let i = 0; i < n; ++i) {
        const ch = s.charAt(i);
        if (ch === 'a') {
            status ^= 1<<0;
        } else if (ch === 'e') {
            status ^= 1<<1;
        } else if (ch === 'i') {
            status ^= 1<<2;
        } else if (ch === 'o') {
            status ^= 1<<3;
        } else if (ch === 'u') {
            status ^= 1<<4;
        }
        if (~pos[status]) {
            ans = Math.max(ans, i + 1 - pos[status]);
        } else {
            pos[status] = i + 1;
        }
    }
    return ans;
};