题解:洛谷 AT_abc426_d Pop and Insert
【题目来源】
洛谷:AT_abc426_d [ABC426D] Pop and Insert - 洛谷
【题目描述】
给定一个长度为 \(N\) 只包含 0 和 1 的字符串 \(S\)。
你可以对 \(S\) 任意次(可以为零次)执行以下操作:
- 删除第一个或最后一个字符,将其翻转(将
0变成1,将1变成0),并插入回任意位置。更严格地说,设 \(r(\)0$) = \(`1`,\)r(\(`1`\)) = $0,你可以执行以下两种选择之一(其中 \(S_i\) 表示第 \(i\) 个字符):- 任选 \(i\ (1\leq i\leq N)\),将 \(S\) 变为 \(S_2\dots S_i\,r(S_1)\,S_{i+1}\dots S_N\)。
- 任选 \(i\ (0\leq i\leq N-1)\),将 \(S\) 变为 \(S_1\dots S_i\,r(S_N)\,S_{i+1}\dots S_{N-1}\)。
请计算,最少需要多少次操作才能使 \(S\) 的所有字符都相同。可以证明,总能通过合法操作使 \(S\) 全部变为相同字符。
有 \(T\) 组测试数据,请依次解决。
【输入】
输入按如下格式给出:
\(T\) \(\text{case}_1\) \(\text{case}_2\) \(\vdots\) \(\text{case}_T\)
\(\text{case}_i\) 表示第 \(i\) 组测试数据,格式如下:
\(N\) \(S\)
【输出】
输出 \(T\) 行,第 \(i\) 行输出第 \(i\) 组测试数据的答案。
【输入样例】
3
5
01001
3
000
15
110010111100101
【输出样例】
4
0
16
【核心思想】
-
问题分析:给定长度为 \(N\) 的 \(01\) 字符串 \(S\),每次操作可删除首字符或尾字符,翻转后插入任意位置。求使 \(S\) 全变为
0或全变为1的最少操作次数。这是一个前缀和 + 分治优化问题,关键在于将操作转化为"删除某段连续区间并翻转"的代价模型,利用前缀和与后缀和数组分治计算最小代价。 -
算法选择:
- 双方案枚举:分别计算全部变为
0和全部变为1的最小代价,取两者较小值 - 前缀和/后缀和数组:\(sa[i]\) 表示将前缀 \([1, i]\) 全部变为目标字符的代价,\(sb[i]\) 表示将后缀 \([i, N]\) 全部变为目标字符的代价
- 分治式最小值:对于目标字符
0,需要消除所有1;每个1可通过左半部分前缀操作或右半部分后缀操作消除,用last记录前一段的累积代价,与当前后缀代价组合求最小
- 双方案枚举:分别计算全部变为
-
关键步骤:
- 特判:若 \(S\) 已全相同,答案为 \(0\)
- 方案一:全部变为
0:- 计算前缀代价:\(sa[i] = sa[i-1] + 1 + (S_i = \text{`0'})\),表示处理到 \(i\) 且 \(S_i\) 为
0时的累积代价 - 计算后缀代价:\(sb[i] = sb[i+1] + 1 + (S_i = \text{`0'})\),表示从 \(i\) 到 \(N\) 的累积代价
- 正向遍历:遇到
1时,\(ans = \min(ans, last + sb[i])\),更新 \(last = sa[i]\) - 反向遍历:遇到
1时,\(ans = \min(ans, last + sa[i])\),更新 \(last = sb[i]\)
- 计算前缀代价:\(sa[i] = sa[i-1] + 1 + (S_i = \text{`0'})\),表示处理到 \(i\) 且 \(S_i\) 为
- 方案二:全部变为
1:对称处理,将上述逻辑中的0和1互换 - 输出 \(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O(T \cdot N)\),每组数据两次前缀和 + 两次遍历
- 空间复杂度:\(O(N)\),前缀和与后缀和数组
-
前缀和分治的核心思想:
- 操作等价转化:每次"删首/尾 + 翻转 + 插入"等价于将某位置的字符取出并翻转后重新放置,代价为 \(1\) 次操作;对于连续的目标异色段,可通过分段处理优化
- 分治组合:将字符串分为若干段,每段要么通过前缀操作处理、要么通过后缀操作处理,
last记录前一段的前缀代价,与当前段的后缀代价相加形成完整方案 - 双向扫描:正向和反向各扫描一次,覆盖所有可能的分段方式
- 适用于"区间操作代价最小化 + 双目标选择"类问题,核心是将操作抽象为可组合的前缀/后缀代价
【算法标签】
普及 #前缀和
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 500005; // 定义最大字符串长度
int T; // 测试用例数量
int sa[N], sb[N]; // 前缀和后缀数组,用于存储修改代价
/**
* 检查字符串是否已经全部相同
* @param n 字符串长度
* @param s 字符串(1-based索引)
* @return 如果所有字符相同返回true,否则false
*/
bool check(int n, string s)
{
for (int i = 2; i <= n; i++)
{
if (s[i] != s[i - 1])
{
return false;
}
}
return true;
}
int main()
{
cin >> T; // 输入测试用例数量
while (T--)
{
int n; // 当前字符串长度
string s; // 当前字符串
cin >> n >> s;
s = " " + s; // 转换为1-based索引
// 如果字符串已经全部相同,直接输出0
if (check(n, s))
{
cout << 0 << endl;
continue;
}
int ans = 1e9; // 初始化答案为极大值
int last; // 临时变量,记录前驱代价
/********** 方案1:全部修改为0 **********/
// 计算前缀代价(从左到右)
for (int i = 1; i <= n; i++)
{
sa[i] = sa[i - 1] + 1 + (s[i] == '0');
}
// 计算后缀代价(从右到左)
for (int i = n; i >= 1; i--)
{
sb[i] = sb[i + 1] + 1 + (s[i] == '0');
}
// 正向遍历计算最小代价
last = 0;
for (int i = 1; i <= n; i++)
{
if (s[i] == '1')
{
ans = min(ans, last + sb[i]);
last = sa[i];
}
}
// 反向遍历计算最小代价
last = 0;
for (int i = n; i >= 1; i--)
{
if (s[i] == '1')
{
ans = min(ans, last + sa[i]);
last = sb[i];
}
}
/********** 方案2:全部修改为1 **********/
// 计算前缀代价(从左到右)
for (int i = 1; i <= n; i++)
{
sa[i] = sa[i - 1] + 1 + (s[i] == '1');
}
// 计算后缀代价(从右到左)
for (int i = n; i >= 1; i--)
{
sb[i] = sb[i + 1] + 1 + (s[i] == '1');
}
// 正向遍历计算最小代价
last = 0;
for (int i = 1; i <= n; i++)
{
if (s[i] == '0')
{
ans = min(ans, last + sb[i]);
last = sa[i];
}
}
// 反向遍历计算最小代价
last = 0;
for (int i = n; i >= 1; i--)
{
if (s[i] == '0')
{
ans = min(ans, last + sa[i]);
last = sb[i];
}
}
// 输出最小修改代价
cout << ans << endl;
}
return 0;
}
【运行结果】
3
5
01001
4
3
000
0
15
110010111100101
16
浙公网安备 33010602011771号