本文属于「征服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
仅由数字组成与本题相似的题目(前缀和+异或)如下:
首先分析一下「超赞子字符串」的性质:字符串 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 0−1 序列来表示任意一个子串 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 0−1 序列即为 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 0−1 序列。
可以从小到大依次枚举「超赞子字符串」的右端点 j j j 。如果存在某个满足 i < j i < j i<j 的位置 i i i ,并且 s [ i : j ] s[i:j] s[i:j] 是「超赞子字符串」,那么必然满足下面的条件:
这是为什么呢?其实和「前缀和」的思想很类似。 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:i−1] 中的出现次数。如果只考虑出现次数的奇偶性,那么 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:i−1] 中出现的次数均为奇数或者均为偶数,也就是 t [ 0 : j ] t[0:j] t[0:j] 和 t [ 0 : i − 1 ] t[0:i-1] t[0:i−1] 对应的位是相同的。因此,如果 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:i−1]最多只有一位不同。
这样一来就可以在遍历枚举 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 0−1 序列,查询到的每一个值 i − 1 i-1 i−1 ,都对应着一个「超赞子字符串」 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 0−1 序列相同时,应当保留较短的前缀,而忽略较长的前缀。
细节:可用一个 [ 0 , 1024 ) [0,1024) [0,1024) 之间的整数来维护 0 − 1 0-1 0−1 序列,这实际上就是 0 − 1 0-1 0−1 序列对应的二进制表示。对于一次翻转操作,如果我们要将 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;
}
};
复杂度分析: