DeepSeek LeetCode 2911. 得到 K 个半回文串的最少修改次数 JavaScript实现
由于你再次询问同一题,我会提供一份更注重可读性 + 优化细节的 JavaScript 实现,并附上详细的思路注释,帮助你彻底理解半回文串的处理方式。
```javascript
/**
* 2911. 得到 K 个半回文串的最少修改次数
* @param {string} s
* @param {number} k
* @return {number}
*/
var minimumChanges = function(s, k) {
const n = s.length;
// ---------- 1. 预处理所有子串变成半回文的最小修改次数 ----------
// cost[l][r] 表示 s[l..r] (闭区间) 的最小修改次数,如果无法构成半回文则为 Infinity
const cost = Array.from({ length: n }, () => new Array(n).fill(Infinity));
// 枚举所有子串
for (let l = 0; l < n; l++) {
for (let r = l; r < n; r++) {
const len = r - l + 1;
if (len < 2) continue; // 长度至少为2才能成为半回文
let best = Infinity;
// 枚举 len 的所有真因子 d (1 <= d < len 且 len % d == 0)
// 注意:d = len 排除,因为要求真因子
for (let d = 1; d < len; d++) {
if (len % d !== 0) continue;
let changes = 0;
// 对于每个起始偏移 start (0 ~ d-1)
for (let start = 0; start < d; start++) {
// 收集该轨的所有下标: start, start+d, start+2d, ... 且 <= r
// 使用双指针从两端往中间配对
let i = l + start;
let j = l + start + Math.floor((len - 1 - start) / d) * d;
while (i < j) {
if (s[i] !== s[j]) changes++;
i += d;
j -= d;
}
}
best = Math.min(best, changes);
}
cost[l][r] = best;
}
}
// ---------- 2. 动态规划:将字符串分成 k 段 ----------
// dp[i][c] : 将前 i 个字符 (s[0..i-1]) 分成 c 段的最少修改次数
const dp = Array.from({ length: n + 1 }, () => new Array(k + 1).fill(Infinity));
dp[0][0] = 0; // 空串分成0段代价为0
for (let c = 1; c <= k; c++) { // 枚举段数
for (let i = c; i <= n; i++) { // 至少需要 c 个字符
// 枚举最后一段的起始位置 j (前 j 个字符已经分成了 c-1 段)
for (let j = c - 1; j < i; j++) {
if (dp[j][c - 1] !== Infinity && cost[j][i - 1] !== Infinity) {
dp[i][c] = Math.min(dp[i][c], dp[j][c - 1] + cost[j][i - 1]);
}
}
}
}
return dp[n][k];
};
```
解题核心要点
1. 半回文的判断与最小修改
· 定义:长度为 L 的字符串,若存在一个真因子 d(1 ≤ d < L 且 d | L),使得将所有下标按 mod d 分组后,每组都是一个回文串,则称该字符串是半回文。
· 修改次数计算:
· 对于每个 d,将原串映射成 d 条轨道。
· 每条轨道内的字符必须回文,所以统计轨道内对称位置不同的对数。
· 所有轨道的总不同对数就是按此 d 划分需要的修改次数。
· 取所有真因子 d 中的最小值。
2. 为什么预处理是必要的?
如果直接在 DP 里对每个子串实时计算半回文代价,会重复计算大量子串,复杂度爆炸。预处理 O(n^3) 在 n ≤ 200 时可行(约 800 万次操作),DP 额外 O(n^2 * k) 也 ok。
3. 动态规划细节
· 状态 dp[i][c]:前 i 个字符分成 c 段的最小代价。
· 转移:枚举最后一段的开始位置 j(前 j 个字符分成 c-1 段),加上子串 s[j..i-1] 的半回文代价。
· 初始化 dp[0][0] = 0,其他为无穷大。
· 答案 = dp[n][k]。
4. 边界条件
· 若 k > n 理论上无法划分(题目不会出现)。
· 若某个子串无法变成半回文(如长度 1),则 cost 保持 Infinity,DP 时自动跳过。
复杂度
· 时间:预处理 O(n^3),DP O(n^2 * k),实测 n=200 时能通过。
· 空间:O(n^2) 存储 cost,加上 DP 表 O(n*k)。
测试样例
```javascript
console.log(minimumChanges("abcde", 2)); // 输出:?
console.log(minimumChanges("aabbaa", 3)); // 输出:?
```
你可以自行运行验证。
如果你需要进一步了解如何优化因子的枚举或提前剪枝,或者对半回文定义有疑问,欢迎继续提问。
