题解:洛谷 AT_abc426_d Pop and Insert

【题目来源】

洛谷:AT_abc426_d [ABC426D] Pop and Insert - 洛谷

【题目描述】

给定一个长度为 \(N\) 只包含 01 的字符串 \(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

【核心思想】

  1. 问题分析:给定长度为 \(N\)\(01\) 字符串 \(S\),每次操作可删除首字符或尾字符,翻转后插入任意位置。求使 \(S\) 全变为 0 或全变为 1 的最少操作次数。这是一个前缀和 + 分治优化问题,关键在于将操作转化为"删除某段连续区间并翻转"的代价模型,利用前缀和与后缀和数组分治计算最小代价。

  2. 算法选择

    • 双方案枚举:分别计算全部变为 0 和全部变为 1 的最小代价,取两者较小值
    • 前缀和/后缀和数组\(sa[i]\) 表示将前缀 \([1, i]\) 全部变为目标字符的代价,\(sb[i]\) 表示将后缀 \([i, N]\) 全部变为目标字符的代价
    • 分治式最小值:对于目标字符 0,需要消除所有 1;每个 1 可通过左半部分前缀操作或右半部分后缀操作消除,用 last 记录前一段的累积代价,与当前后缀代价组合求最小
  3. 关键步骤

    • 特判:若 \(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]\)
    • 方案二:全部变为 1:对称处理,将上述逻辑中的 01 互换
    • 输出 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(T \cdot N)\),每组数据两次前缀和 + 两次遍历
    • 空间复杂度:\(O(N)\),前缀和与后缀和数组
  5. 前缀和分治的核心思想

    • 操作等价转化:每次"删首/尾 + 翻转 + 插入"等价于将某位置的字符取出并翻转后重新放置,代价为 \(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
posted @ 2026-08-27 11:03  团爸讲算法  阅读(11)  评论(0)    收藏  举报