LeetCode 1542. 找出最长的超赞子字符串
题目描述


题意分析
在只包含数字字符的字符串中,选择一段连续、非空子串,使它的字符能够通过任意重排形成回文,返回最长长度。
原子串不需要已经是回文,只看重排是否可能;但选取范围必须连续,不能从多个分散位置拼出候选。返回长度即可,不需要构造回文内容。
解法:前缀奇偶位掩码
核心思路
[!blue]
回文中左右对称位置必须放同一种字符,因此除中间位置外,所有字符都应成对。一个多重集合能重排成回文,当且仅当至多一种字符出现奇数次:成对部分分放两侧,唯一的奇数余项放中央,就能构造成功。
数字只有十种,用十位掩码记录前缀中每种数字出现次数的奇偶,一表示奇数、零表示偶数。遇到数字
d就执行mask ^= 1 << d,同一种数字每再出现一次都会翻转状态。两个前缀掩码异或,表示它们之间子串的各数字次数奇偶。想让至多一位为一,历史前缀只能与当前掩码相同,或者恰好相差一个位。所以每个右端只需检查当前掩码及逐一翻转十个位后的十种候选。
用
first[mask]保存真实出现过该掩码的最早前缀长度。对于相同右端和历史状态,越早的前缀产生越长区间,后来再次出现就不应覆盖。空前缀长度为零、状态为零,提前登记first[0] = 0。处理字符下标
i后,当前前缀长度是i + 1,与历史长度之差就是候选子串长度。只有当前真实掩码可以在首次出现时登记;翻转一位得到的状态只是查询条件,不能伪装成已出现状态写回。
解题步骤
- 创建 1024 个状态的最早位置表,初始化为未出现,登记空前缀。
- 从左到右处理数字,异或翻转对应位。
- 当前状态以前出现过时,用最早位置更新答案;否则登记当前前缀长度。
- 依次翻转十个位,查询已经出现过的相差一位状态并更新长度。
- 返回扫描过程中记录的最大长度。
代码实现
class Solution {
public int longestAwesome(String s) {
int[] first = new int[1 << 10];
for (int i = 0; i < first.length; i++) {
first[i] = -1;
}
// 位置统一使用前缀长度,空前缀长度为零。
first[0] = 0;
int mask = 0;
int answer = 0;
for (int i = 0; i < s.length(); i++) {
int d = s.charAt(i) - '0';
mask ^= 1 << d;
// 同状态对应全部偶数次数,只在首次出现时登记最早位置。
if (first[mask] != -1) {
answer = Math.max(answer, (i + 1) - first[mask]);
} else {
first[mask] = i + 1;
}
// 查询仅差一位的历史状态,候选状态本身不能登记为已出现。
for (int k = 0; k < 10; k++) {
int m2 = mask ^ (1 << k);
if (first[m2] != -1) {
answer = Math.max(answer, (i + 1) - first[m2]);
}
}
}
return answer;
}
}
func longestAwesome(s string) int {
first := make([]int, 1<<10)
for i := 0; i < len(first); i++ {
first[i] = -1
}
// 位置统一使用前缀长度,空前缀长度为零。
first[0] = 0
mask := 0
answer := 0
for i := 0; i < len(s); i++ {
d := int(s[i] - '0')
mask ^= 1 << d
// 同状态对应全部偶数次数,只在首次出现时登记最早位置。
if first[mask] != -1 {
if i+1-first[mask] > answer {
answer = i + 1 - first[mask]
}
} else {
first[mask] = i + 1
}
// 查询仅差一位的历史状态,候选状态本身不能登记为已出现。
for k := 0; k < 10; k++ {
m2 := mask ^ (1 << k)
if first[m2] != -1 {
if i+1-first[m2] > answer {
answer = i + 1 - first[m2]
}
}
}
}
return answer
}
复杂度分析
- 时间复杂度:$O(10n+2^{10})$,初始化全部状态,每个字符检查固定的十一种历史状态。
- 空间复杂度:$O(2^{10})$,保存十位掩码的最早前缀长度。
关键点总结
[!green]
- 重排回文只取决于各字符次数奇偶,不需要保存准确频次。
- 前缀异或给出区间奇偶,最多一个奇数等价于掩码最多一个置位。
- 固定右端时保留最早历史状态,才能使长度最大。
- 查询候选与登记真实状态必须区分。
易错点总结
[!yellow]
- 按位或只能置一不能翻回零,同数字出现两次时无法恢复偶数状态,必须使用异或。
- 只查询同掩码会漏掉恰好一种数字出现奇数次的合法子串。
- 覆盖已经保存的最早位置,会缩短以后可得到的区间。
- 把翻转一位的候选也写入表,会制造根本没有出现的历史前缀。
- 存的是前缀长度,当前应使用
i + 1;不能混入字符下标的长度公式。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 1177. 构建回文串检测 | 中等 | 同样用字符频次奇偶掩码判断能否重排回文,本题保存每种前缀掩码的最早位置来求最长区间。 |
| 525. 连续数组 | 中等 | 两题都保留每种前缀状态的最早位置以求最长区间;525只匹配相同的0/1数量差,本题使用数码频次奇偶掩码,可匹配相同掩码或仅相差一位的掩码。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!