LeetCode 1542. Find Longest Awesome Substring【异或前缀和,哈希表,字符串】困难

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给你一个字符串 s 。请返回 s 中最长的 超赞子字符串 的长度。「超赞子字符串」需满足满足下述两个条件:

  • 该字符串是 s 的一个非空子字符串
  • 进行任意次数的字符交换后,该字符串可以变成一个回文字符串

示例 1:

输入:s = "3242415"
输出:5
解释:"24241" 是最长的超赞子字符串,交换其中的字符后,可以得到回文 "24142"

示例 2:

输入:s = "12345678"
输出:1

示例 3:

输入:s = "213123"
输出:6
解释:"213123" 是最长的超赞子字符串,交换其中的字符后,可以得到回文 "231132"

示例 4:

输入:s = "00"
输出:2

提示:

  • 1 <= s.length <= 10^5
  • s 仅由数字组成

与本题相似的题目(前缀和+异或)如下:

  • 1371. 每个元音包含偶数次的最长子字符串
  • 1915. 最美子字符串的数目
  • 1177. 构建回文串检测,与本题非常相似,只是本题不允许替换,且是为了找「重排后最长的回文子串」。

解法 异或前缀和+哈希表

首先分析一下「超赞子字符串」的性质:字符串 s s s 中的一个子串 s ′ s' s ,如果其中的字符能以某种顺序组成一个回文串,那么该串就是一个「超赞子字符串」。

这就需要, s ′ s' s 中最多只有一个字符出现了奇数次,其余的所有字符都出现了偶数次。因为对于任意一个回文串,如果它的回文中心是单个字符(例如 a b c b a abcba abcba ),回文中心的字符出现了奇数次,其余所有字符根据对称性出现了偶数次;如果它的回文中心是两个相同字符(例如 a b c c b a abccba abccba ),那么所有字符根据对称性都出现了偶数次。

因此,对于任意一个子串 s ′ s' s 而言,我们只需要关心:它的每一个字符出现了奇数次还是偶数次。由于字符串 s s s 中仅包含字符 0 0 0 9 9 9 ,因此我们可以用一个长度为 10 10 10 0 − 1 0−1 01 序列来表示任意一个子串 s ′ s' s该序列从低到高的第 i i i 位对应着 s ′ s' s 中出现字符 i i i 的奇偶性:具体地, 0 0 0 对应着出现了偶数次, 1 1 1 对应着出现了奇数次。

举个例子,对于子串 s ′ = 0366613544 s'= \text{0366613544} s=0366613544 ,其中 0 , 1 , 3 , 4 , 5 , 6 0,1,3,4,5,6 0,1,3,4,5,6 分别出现了 1 , 1 , 2 , 2 , 1 , 3 1,1,2,2,1,3 1,1,2,2,1,3 次,那么对应的 0 − 1 0-1 01 序列即为 0001100011 0001100011 0001100011 。注意序列的最高位对应字符 9 9 9 ,最低位对应字符 0 0 0

经过上面的分析,就可以对「超赞子字符串」进行枚举,这里用 s [ i : j ] s[i:j] s[i:j] 表示字符串 s s s 中从位置 i i i 到位置 j j j 组成的子串, t [ i : j ] t[i:j] t[i:j] 表示 s [ i : j ] s[i:j] s[i:j] 对应的 0 − 1 0−1 01 序列。

可以从小到大依次枚举「超赞子字符串」的右端点 j j j 。如果存在某个满足 i < j i < j i<j 的位置 i i i ,并且 s [ i : j ] s[i:j] s[i:j] 是「超赞子字符串」,那么必然满足下面的条件:

  • t [ 0 : i − 1 ] t[0:i-1] t[0:i1] t [ 0 : j ] t[0:j] t[0:j] 最多只有一位不同。
  • 特别地,如果 i = 0 i=0 i=0,那么 s [ 0 : i − 1 ] s[0:i-1] s[0:i1] 是一个空子串, t [ 0 : i − 1 ] t[0:i-1] t[0:i1]为全 0 0 0 序列。

这是为什么呢?其实和「前缀和」的思想很类似。 s [ i : j ] s[i:j] s[i:j] 中某个字符的出现次数,就等于它在 s [ 0 : j ] s[0:j] s[0:j] 中的出现次数减去它在 s [ 0 : i − 1 ] s[0:i-1] s[0:i1] 中的出现次数。如果只考虑出现次数的奇偶性,那么 s [ i : j ] s[i:j] s[i:j] 中某个字符出现的次数为偶数,当且仅当它在 s [ 0 : j ] s[0:j] s[0:j] s [ 0 : i − 1 ] s[0:i-1] s[0:i1] 中出现的次数均为奇数或者均为偶数,也就是 t [ 0 : j ] t[0:j] t[0:j] t [ 0 : i − 1 ] t[0:i-1] t[0:i1] 对应的位是相同的。因此,如果 s [ i : j ] s[i:j] s[i:j] 是一个「超赞子字符串」,那么 t [ 0 : j ] t[0:j] t[0:j] t [ 0 : i − 1 ] t[0:i-1] t[0:i1]最多只有一位不同。

这样一来就可以在遍历枚举 j j j 的同时维护 t [ 0 : j ] t[0:j] t[0:j],并用哈希映射存储所有之前出现过的 t [ 0 : i ] t[0:i] t[0:i] ,便于后续的查找。对于哈希映射中的每个键值对,键为 t [ 0 : i ] t[0:i] t[0:i] ,值为 i i i 。在枚举 j j j 时,在哈希映射中查询 t [ 0 : j ] t[0:j] t[0:j] 本身、以及 t [ 0 : j ] t[0:j] t[0:j] 的某一位翻转后得到的 0 − 1 0-1 01 序列,查询到的每一个值 i − 1 i-1 i1 ,都对应着一个「超赞子字符串」 s [ i : j ] s[i:j] s[i:j]

在这之后,将 ( t [ 0 : j ] , j ) (t[0:j],j) (t[0:j],j) 这一键值对放入哈希映射。注意:如果 t [ 0 : j ] t[0:j] t[0:j] 已经是哈希映射中的一个键,需要忽略这一步操作,这是因为求的是最长的「超赞子字符串」,所以当两个前缀的 0 − 1 0-1 01 序列相同时,应当保留较短的前缀,而忽略较长的前缀。

细节:可用一个 [ 0 , 1024 ) [0,1024) [0,1024) 之间的整数来维护 0 − 1 0-1 01 序列,这实际上就是 0 − 1 0-1 01 序列对应的二进制表示。对于一次翻转操作,如果我们要将 t [ 0 : j ] t[0:j] t[0:j] 的第 k k k 位进行翻转,只要使用
t [ 0 : j ] ⊕ ( 2 k ) t[0:j] \oplus (2^k) t[0:j](2k) 即可。其中 ⊕ ⊕ 表示按位异或运算, 2 k 2^k 2k 可用左移运算 1 < < k 1 << k 1<<k 快速得到。

此外,空前缀(即 i = 0 i = 0 i=0 )也对应着哈希映射中的一个键值对 ( 0 , − 1 ) (0, -1) (0,1)

class Solution {
public:
    int longestAwesome(string s) {
        int n = s.size(), xor_sum = 0, ans = 0;
        int rec[1024];
        memset(rec, -1, sizeof(rec)); // 空前缀对应键值对(0,-1)
        for (int i = 0; i < n; ++i) {
            xor_sum ^= 1 << (s[i] - '0'); // 特判异或前缀和为0的情况
            if (xor_sum == 0 || ~rec[xor_sum]) ans = max(ans, i - rec[xor_sum]);
            else rec[xor_sum] = i;
            for (int j = 0; j < 10; ++j) {
                int t = xor_sum ^ (1 << j);
                if (t == 0 || ~rec[t]) // 特判翻转后为0的情况
                    ans = max(ans, i - rec[t]);
            }
        }
        return ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( n ∣ Σ ∣ ) O(n |\Sigma|) O(n∣Σ∣) ,其中 n n n 是字符串 s s s 的长度, Σ \Sigma Σ 表示字符集,在本题中字符串只包含 10 10 10 个数字字符, ∣ Σ ∣ = 10 |\Sigma| = 10 ∣Σ∣=10
  • 空间复杂度: O ( S ) O(S) O(S) S = 2 10 = 1024 S=2^{10} = 1024 S=210=1024 为10种小写字母所有奇偶性对应的状态数。

你可能感兴趣的:(#,哈希映射,算法技巧-前缀和,位操作,leetcode,散列表,算法)