答案是不匹配的对称位置对数;用双指针在行程编码两端同时向内走,整段记账,不展开字符串。
OJ: luogu
题目 ID: P17828
难度:普及-
标签:字符串双指针思维
创建: 2026-10-31 18:55
更新: 2026-10-31 20:14
形式化题目
一个长度为
一次操作把某个位置上的字符改成任意小写字母。要求用最少次数的操作使
这个式子说的是:把字符串从两端同时往里数,位置配成一对对,每一对的左右两个字符必须完全相同。
暴力解法
思路
按定义做:把行程编码展开成真正的字符串,然后从两端往中间扫,数一数有多少组对称位置的字符不同。
brute.cpp 就是这两步,s.push_back(ch) 循环
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-31 18:55
* update_at: 2026-10-31 18:58
*/
// brute.cpp:直接把字符串展开,再一组一组数对称位置上字符不同的组数。
// 只有字符串总长很小的时候才能用,专门用来和 main.cpp 对拍。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
string s;
for (int i = 0; i < n; ++i) {
ll len;
char ch;
cin >> len >> ch;
for (ll k = 0; k < len; ++k) s.push_back(ch);
}
int L = (int)s.size();
ll ans = 0;
for (int i = 0; i < L / 2; ++i) {
if (s[i] != s[L - 1 - i]) ++ans;
}
cout << ans << '\n';
return 0;
}复杂度
展开需要
瓶颈
瓶颈不在算法思路,而在“必须把字符串摊开成每一个位置”这个前提:行程编码只用
所以要想通过全部测试点,做法不能依赖
正解
关键观察
先把“改几个字符”变成“有几组冲突”。
- 位置
与位置 组成一对,回文要求这一对相等。 - 不同的对涉及的位置互不重叠,改动一对不会影响其他对。
- 一对字符不同,至少要改其中一个(改一个就够,把另一个复制过来);一对字符相同,一个都不用改。
于是最少修改次数正好等于字符不相同的对称位置对数,也就是暴力解法数出来的那个数。剩下要做的只是在不展开字符串的前提下数出这个对数。
思路
想先跟着动画把双指针走一遍,可以打开 交互教程:节点是 [100 a] 这样的一段,每走一轮段长就被减短([4 a] → [2 a]),左右指针从两端向中点靠拢。
把两个指针分别放在字符串的两端:左指针从第 1 个字符往右走,右指针从第 half);奇数长度中间那个字符配给自己,不用管。
指针都按段移动。代码里把“这一段还剩几个字符”直接记在 li)、右边停在哪一段(下标 ri)、已经配好了几组(done)。
每一轮能整批吃掉多少组,由三个数取最小决定:左段还剩 a[li],右段还剩 a[ri],还差 half - done 组。
take = min(a[li], a[ri], half - done)
三个候选分别回答:左边这一段还能给几个字符、右边这一段还能给几个字符、还差几组才凑满一半。取最小,才不会多配。
两侧字符不同时,这 take 组对称位置全都要改,答案加上 take;相同时一组都不用改。
然后两段的剩余量各减去 take,减到 0 就换下一段接着走。
以样例 #1 的 2 a、2 b、1 c)为例,
| 轮次 | 左段剩 | 右段剩 | 还差组数 | take |
两侧字符 | 答案 |
|---|---|---|---|---|---|---|
| 1 | 2(a) |
1(c) |
2 | 1 | 不同 | 1 |
| 2 | 1(a) |
2(b) |
1 | 1 | 不同 | 2 |
第 2 轮把 2 组配满,答案 2,与样例一致;中间那个 b 配给自己。
两侧撞进同一段怎么办? 左边那一段的剩余量减到 0 之后,li 加一可能正好等于 ri,两个指针停在同一段上。既然左右指针都在这段里,中间还没配的那段区间也整个落在这一段内:左右各吃掉 done 个字符,剩下的正好是原串的第 done + 1 个到第 L - done 个字符。这一段里的字符全都相同,所以剩下的组一组都不用改,答案不会再变,直接结束。
再拿 3 a、2 b、4 a、3 b 走一遍(
| 轮次 | 左段剩 | 右段剩 | 还差组数 | take |
两侧字符 | 答案 |
|---|---|---|---|---|---|---|
| 1 | 3(a) |
3(b) |
6 | 3 | 不同 | 3 |
| 2 | 2(b) |
4(a) |
3 | 2 | 不同 | 5 |
| 3 | 2(a) |
2(a) |
1 | — | 同一段,字符必然相同 | 5(直接结束) |
第 2 轮打完,第 2 段 2 b 用完,左指针向右换段;右指针本来就停在第 3 段上,已经从这一段右端吃掉了 2 个,还剩 2 个。于是两个指针都落在第 3 段,第 3 行那两列都是 2 就来自这里。这时还差 1 组,但没配的字符全在这一段里,直接收工,答案停在 5。
数据规模决定了两处必须用 long long:int 上限。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-31 18:55
* update_at: 2026-10-31 20:00
*/
// main.cpp:答案等于「字符不同的对称位置对数」。
// 两侧的段指针同时向中间走,每段还剩多少直接记在 a[] 里(边走边减);
// 两侧撞进同一段时,剩下的字符全都在这一段、字符必然相同,直接收工。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 5;
int n;
ll a[maxn]; // 第 i 段的长度,跑的过程中会被减掉已经配掉的字符数
char c[maxn]; // 第 i 段全是字符 c[i]
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
ll L = 0;
for (int i = 0; i < n; ++i) {
cin >> a[i] >> c[i];
L += a[i];
}
ll half = L / 2; // 一共要配几组,奇数长度时正中间的字符配给自己
ll done = 0; // 已经配好几组
ll ans = 0;
int li = 0, ri = n - 1; // 左端当前段、右端当前段
while (done < half) {
if (li == ri) {
// 两侧已经走进同一段:还没配的字符全在这段里,字符都一样,
// 这些组一组都不用改,答案不会再变,直接结束。
break;
}
// 两侧还在不同的段:各能提供多少个字符就吃多少,但别超过还差的组数
ll take = min(min(a[li], a[ri]), half - done);
if (c[li] != c[ri]) ans += take; // 这一批 take 组对称位置全都要改
a[li] -= take;
a[ri] -= take;
done += take;
if (a[li] == 0) ++li; // 这一段用完了,换成下一段
if (a[ri] == 0) --ri;
}
cout << ans << '\n';
return 0;
}复杂度
- 时间:每一轮要么让某一侧的段减到 0(指针换段),要么让
done凑满组,要么两侧撞进同一段直接收工;两个指针最多各换 次段,所以是 。 - 空间:只存行程编码本身;段长是在
数组上就地减的,没有额外开数组, 。
总结
这道题有两次“换个说法”。
第一步是把“最少改几个字符”换成“有几组对称位置冲突”。冲突对数是最小修改量的下界(每组至少改一个),而每组改一个就能达到这个下界,所以两者相等。这一步完成后问题不再关心具体改成什么字母。
第二步是把“数冲突”从展开后的字符串移到行程编码上。编码里一段连续相同的字符,与对方同一段的连续字符配对时,结论整批相同,于是可以按段批量处理,复杂度从
代码最后有两个写法上的选择:
- 段长直接减在
数组里,不另开“已经用掉多少”的偏移变量。于是每轮的三个候选数就是 a[li]、a[ri]、half - done,读出来就是“左边还剩、右边还剩、还差几组”,也不会出现“段内偏移忘了清零”这类错。代价是被改写,本题只要答案,不需要保留原串。 - 两侧走进同一段就直接结束:未配对的字符全在这一段里,字符必然相同,一组都不用改。这样“相遇”从取小运算里的隐式行为变成代码里显式的一行,读者能看见它。
相关 rbook 文章
做这题用到的知识点在 rbook 算法书 上有专门章节:
| 知识点 | 对应文章 | 用在哪 |
|---|---|---|
| 相向双指针 | 《判断回文串》 | 左右指针从两端往中间收,比较对称位置;本题只是把“逐字符比较”换成了“按段整批比较” |
| 双指针的分类与终止条件 | 《双指针算法:优雅地优化循环》 | 相向双指针的定义、循环在哪停(done < half 对应这里的 left < right) |
行程编码本身(一段连续相同的字符打包成“个数 + 字符”)rbook 还没有专文;本题的难点正是“不能把编码展开”。