节点是“数字 + 字符”的一段(如 [100 a]),代表 100 个连续的 a。左指针(蓝)从最左段、右指针(橙)从最右段同时往中间走,每配掉一批字符,对应段就变短:[4 a] → [2 a]。
蓝纹是左端已经吃掉的字符,橙纹是右端已经吃掉的字符。两侧吃到的字符数相同时,指针正好在中点相遇。
回文的唯一要求是:位置 i 与位置 L+1-i 的字符相同。这些位置对互不重叠,所以可以一对一对地看。
两个方向合起来:最少修改次数 = 字符不同的位置对数。于是问题变成“数不匹配对数”,与具体改成什么字母无关。
样例 #3 的答案是 3000000000,已经超过 int 上限;而 L 最大约 2×10^14,所以 L、done、ans 全部要用 long long。
31 ll half = L / 2; // 一共要配几组,奇数长度时正中间的字符配给自己
32 ll done = 0; // 已经配好几组
33 ll ans = 0;
34 int li = 0, ri = n - 1; // 左端当前段、右端当前段
35
36 while (done < half) {
37 if (li == ri) {
38 // 两侧已经走进同一段:还没配的字符全在这段里,字符都一样,
39 // 这些组一组都不用改,答案不会再变,直接结束。
40 break;
41 }
42 // 两侧还在不同的段:各能提供多少个字符就吃多少,但别超过还差的组数
43 ll take = min(min(a[li], a[ri]), half - done);
44 if (c[li] != c[ri]) ans += take; // 这一批 take 组对称位置全都要改
45 a[li] -= take;
46 a[ri] -= take;
47 done += take;
48 if (a[li] == 0) ++li; // 这一段用完了,换成下一段
49 if (a[ri] == 0) --ri;
50 }
页面里的动画就是这段循环:a[] 就是块下面的“还剩多少”,每轮减掉 take(第 45、46 行);ans 只在这一对段字符不同时累加(第 44 行);段减到 0 就换段(第 48、49 行);两侧撞进同一段就直接 break(第 37–41 行)。
还有一个小细节:页面为了把两端的染色画对,另外记了“左端吃了多少、右端吃了多少”;代码里没有这两个变量,它只记“这一段还剩多少”。两种记法描述的是同一件事。
每一轮要么让某一侧的段减到 0(换段),要么让 done 达标结束循环,两个指针最多各换 n 次段,所以时间是 O(n),空间是 O(n)(段长就地减在 a 里,没有额外开数组)。
暴力做法把字符串展开再逐对比较,需要 O(L) 的时间和空间。而 n ≤ 2×10^5、a_i ≤ 10^9,L 可以到 2×10^14,展开这一步就已经放不下。动画里那些大数字([250 b])正是提醒:段是按“整批”记账的,不能逐个字符走。
1. 新例推演:[2 a] [1 b] [2 a] [3 c],最少要改几个字符?
2. 条件解释:代码里 li == ri 时为什么可以直接 break?
3. 核心代码应用:两侧还在不同段时,第 43 行的 take 应该写哪一个?
用键盘 ← → 也可以翻步;空格播放/暂停。