← 返回题目页

P17828 回文串:双指针在行程编码上向中间走

节点是“数字 + 字符”的一段(如 [100 a]),代表 100 个连续的 a。左指针(蓝)从最左段、右指针(橙)从最右段同时往中间走,每配掉一批字符,对应段就变短:[4 a] → [2 a]。

L
R
块上写的是这一段的原始段长,下面小字是现在还剩多少个字符;紫色虚线是字符串中点。L 从左边往右吃,R 从右边往左吃。

左指针所在的段

—

右指针所在的段

—
已配对 0 / 0 组
答案 0

过程记录

蓝纹是左端已经吃掉的字符,橙纹是右端已经吃掉的字符。两侧吃到的字符数相同时,指针正好在中点相遇。

为什么答案就是“字符不同的对称位置对数”

回文的唯一要求是:位置 i 与位置 L+1-i 的字符相同。这些位置对互不重叠,所以可以一对一对地看。

  • 下界:一对里两个字符不同,至少要改动其中至少一个位置。
  • 可达:把这一对里的一方改成另一方,只需要 1 次。

两个方向合起来:最少修改次数 = 字符不同的位置对数。于是问题变成“数不匹配对数”,与具体改成什么字母无关。

样例 #3 的答案是 3000000000,已经超过 int 上限;而 L 最大约 2×10^14,所以 L、done、ans 全部要用 long long。

对应代码(main.cpp 第 31–50 行,原文片段)
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 应该写哪一个?

用键盘 ← → 也可以翻步;空格播放/暂停。