LeetCode 1177. 构建回文串检测
题目描述


题意分析
对每个查询
[left, right, k],单独考虑原字符串的这个闭区间。可以任意重排其中字符,并至多把k个字符各替换为某个小写字母,判断是否能得到回文串。重排不消耗替换次数,替换按单个字符计数,不是一次修改该字母的所有出现。各查询相互独立,不会把某次查询假设的修改写回原串,所以可以共享一次预处理。
解法:前缀奇偶掩码
核心思路
[!blue]
能任意重排后,只需关心各字母的次数奇偶。回文两侧的字符成对出现,偶数长度时所有次数都必须为偶数,奇数长度时最多允许一种奇数次数字母放在中心。设当前出现奇数次的字母有
odd种,它与区间长度一定同奇偶,因为偶数次数不影响总长度的奇偶性。一次替换至多改变两种字母的奇偶性,所以最多消去两种奇数次数,这是所需操作数的下界。同时也确实能做到:从两种奇数次字母中取一份,把它替换成另一种,两者的次数就都变成偶数。将奇数种类两两配对,偶数长度全部消去,奇数长度留下一个中心,最少需要
floor(odd / 2)次。因为只要奇偶性,不需要保存完整次数。用二十六位整数掩码表示前缀状态,某一位为一表示对应字母出现奇数次;每读一个字符,就把该位异或翻转。
pre[i]表示前i个字符的状态,空前缀pre[0]为零。查询闭区间
[l, r]时,用pre[r + 1] ^ pre[l]抵消共同前缀,剩下恰好是区间各字母的奇偶位。统计结果中一的个数就得到odd,再与可用替换数比较。掩码固定二十六位,每个查询都不再依赖区间长度。
解题步骤
- 创建长度为
n + 1的前缀掩码数组,初值为零。- 遍历字符,用上一前缀异或当前字符对应的一位,得到下一前缀状态。
- 对每个查询,异或右端后一位和左端的前缀状态,求出区间掩码。
- 统计置一位数,判断整数除法
odd / 2 <= k,按查询顺序保存结果。
代码实现
class Solution {
public List<Boolean> canMakePaliQueries(String s, int[][] queries) {
int n = s.length();
// pre[i] 是 s[0..i-1] 各字母出现次数奇偶性的 26 位掩码,pre[0] 代表空前缀。
int[] pre = new int[n + 1];
for (int i = 0; i < n; i++) {
int bit = 1 << (s.charAt(i) - 'a');
pre[i + 1] = pre[i] ^ bit;
}
List<Boolean> res = new ArrayList<>(queries.length);
for (int[] q : queries) {
int l = q[0];
int r = q[1];
int k = q[2];
// 异或自反,公共前缀 pre[l] 被抵消,剩下的正是区间 [l, r] 的奇偶信息。
int mask = pre[r + 1] ^ pre[l];
int odd = Integer.bitCount(mask);
// 奇数字母两两配对消除,向下取整同时覆盖了长度为奇数的情况。
res.add(odd / 2 <= k);
}
return res;
}
}
import "math/bits"
func canMakePaliQueries(s string, queries [][]int) []bool {
n := len(s)
// pre[i] 是 s[0..i-1] 各字母出现次数奇偶性的 26 位掩码,pre[0] 代表空前缀。
pre := make([]int, n+1)
for i := 0; i < n; i++ {
bit := 1 << (s[i] - 'a')
pre[i+1] = pre[i] ^ bit
}
res := make([]bool, len(queries))
for i, q := range queries {
l, r, k := q[0], q[1], q[2]
// 异或自反,公共前缀 pre[l] 被抵消,剩下的正是区间 [l, r] 的奇偶信息。
mask := pre[r+1] ^ pre[l]
odd := bits.OnesCount(uint(mask))
// 奇数字母两两配对消除,向下取整同时覆盖了长度为奇数的情况。
res[i] = odd/2 <= k
}
return res
}
复杂度分析
- 时间复杂度:$O(n + q)$,预处理原串一次,
q次查询各执行固定宽度的异或和位计数。- 空间复杂度:$O(n)$ 保存前缀掩码,返回结果另占 $O(q)$。
关键点总结
[!green]
- 可重排使回文判定只剩频次奇偶,无需保留原位置关系。
- 一次替换最多消除两个奇数种类,也总能按这种方式配对,所以下界可以达到。
- 区间长度与奇数种类数同奇偶,向下取整自然覆盖中心位置。
- 前缀异或消去公共部分,固定字母范围让每次查询成为常数操作。
易错点总结
[!yellow]
- 使用
pre[r]而不是pre[r + 1],会漏掉闭区间最后一个字符。- 先统计两个前缀的一位数再相减,不能得到区间奇偶;必须先对整个位掩码异或。
- 将所需替换次数写成
odd,忽略一次替换可以同时修正两种奇数次数。- 对一半奇数种类向上取整,会错误要求替换原本能放在回文中心的那一个字符。
- 将查询的假设修改保留给后续查询,违反它们彼此独立的要求。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 面试题 01.04. 回文排列 | 简单 | 可重排回文取决于奇数频次的种数,本题进一步允许k次替换并支持大量子串查询。 |
| 1310. 子数组异或查询 | 中等 | 用前缀异或可快速得到任意区间的奇偶位掩码,再统计奇数频次字符数。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!