首页 > 试题广场 >

小红的华撃串

[编程题]小红的华撃串
  • 热度指数:705 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 1024M,其他语言2048M
  • 算法知识视频讲解
\hspace{15pt}小红定义一个仅由字符 \texttt{`0'}\texttt{`1'} 组成的字符串(简称 01 串)为华撃串,当且仅当其恰好包含三个 \texttt{\texttt{ 子串。换句话说,一个 01 串为华撃串,当且仅当 \operatorname{count}(\texttt{ 等于 3。其中,\operatorname{count}(\texttt{ 表示字符串中 \texttt{ 子串的数量,\operatorname{count}(\texttt{ 表示字符串中 \texttt{ 子串的数量。

\hspace{15pt}现在,小红拿到了一个长为 n 的 01 串,他想要将其变为一个华撃串。为此,他可以进行以下操作:
\hspace{23pt}\bullet\,选择任意一个位置,将其反置(若该位置的字符为 \texttt{`1'},反置后为 \texttt{`0'};若该位置的字符为 \texttt{`0'},反置后为 \texttt{`1'})。

\hspace{15pt}我们可以证明,当字符串长度大于等于 4 时,一定可以通过若干次这样的操作,将原串变为华撃串。小红想知道最少的操作次数是多少。

输入描述:
\hspace{15pt}第一行输入一个整数 n\left(4 \leqq n \leqq 500\right),表示字符串的长度。
\hspace{15pt}第二行输入一个长度为 n 的 01 串 s


输出描述:
\hspace{15pt}输出一个整数,表示最少翻转次数。
示例1

输入

4
1010

输出

0

说明

\hspace{15pt}在这个样例中,原 01 串已经是华撃串,不需要操作。
示例2

输入

5
11111

输出

2

这道题其实很考验思路喵~ 要用到动态规划才能优雅解决呢!

经过猫猫的细心观察发现,如果把字符串中连续的 0 或 1 看作一个“大块”,那么华撃串恰好需要分成 4 个大块 喵!
比如 000110111 可以分成 "000"、"11"、"0"、"111" 这四大块,相邻对数量就是 3 啦(每两个块之间产生一个“01”或“10”)。

是不是觉得 DP 也没那么难啦?来看看猫猫的思路吧:

我们定义两个数组 f1 和 f0,分别表示以 1 开头以 0 开头的字符串,下标代表当前已经分成了多少个大块
例如 f1[2] 表示当前前缀已经构成 2 个大块,并且开头是 1,所需要的最少翻转次数。

现在,我们要处理新来的字符 now(0 或 1),并更新各个块数对应的最少代价。转移的关键在于:每次加入一个新字符,要么延续当前块(块数不变),要么开启一个新块(块数 +1),同时需要根据目标字符决定是否需要翻转。

以 f1[4] 为例:在 f1[4] 的状态下,最后一块一定是 0(因为块数为偶数且开头为 1,结尾与开头相反)。

~~如果新来的字符是 0,那么它可以直接接在末尾(不产生新块),从 f1[3] 转移过来时也不需要翻转(因为 f1[3] 最后一位是 1,现在接 0 正好开启新块),而 f1[4] 自己延续的话则要翻转当前字符为 0(代价 now)。
~~如果新来的字符是 1,那么从 f1[3] 转移时就需要把 1 翻成 0(代价 1-now)才能接到 1 后面开启新块,而 f1[4] 自己延续则要把 1 翻成 0(代价 1-now)。

不过仔细分析后会发现,我们其实可以用统一的形式表达:
f1[4] = min(f1[3], f1[4]) + now

这里的 now 是当前字符的值,因为在 f1[4] 中目标字符是 0,翻转代价就是 now(如果当前是 1 则 now=1 代价 1,如果当前是 0 则 now=0 代价 0)。其他状态也是类似的道理,只要记住每个下标对应的目标结尾字符(由奇偶性决定)就好啦!
按照这个思路,我们就能写出所有转移公式。
最后,块数为 1 时,整个前缀就是同一个字符,所以:
f1[1] += 1-now (保持全 1,当前字符如果是 0 就要翻转)
f0[1] += now (保持全 0,当前字符如果是 1 就要翻转)

初始时,f1[1] = f0[1] = 0,其他都设为一个很大的数(比如 1e5)。
遍历完整个字符串后,答案就是 min(f0[4], f1[4]) 喵~

这样就简单多啦!下面是猫猫的代码实现喵!

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    vector<int> f1(5, 1e5), f0(5, 1e5);
    //假设目标结果以一和零开头
    f1[1] = f0[1] = 0;
    for (int i = 0; i < n; i++) {
        int now = s[i] - '0';
        if (i > 2) 
        {
            f1[4] = min(f1[3],f1[4])+now;
            f0[4] = min(f0[3],f0[4])+1-now;
        }
        if (i > 1) 
        {
            f1[3] = min(f1[2],f1[3])+1-now;
            f0[3] = min(f0[2],f0[3])+now;
        }
        if (i > 0) 
        {
            f1[2] = min(f1[1],f1[2])+now;
            f0[2] = min(f0[1],f0[2])+1-now;
        }
        f1[1] += 1 - now;
        f0[1] += now;
    }
    cout << min(f0[4], f1[4]);
    return 0;
}
/*
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣤⡀⣀⣠⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡀⢀⣴⣾⣷⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣷⣾⣿⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⠿⠛⠛⠉⠉⠉⠉⠉⠉⠛⠻⠿⣿⣿⣿⣿⣿⣶⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⡿⠿⠛⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠿⣿⣿⣿⣷⣄⡀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣀⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⣰⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣠⣾⣿⣿⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣶⣄⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣻⣿⣿⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢹⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⠁⠈⢢⡀⠀⠀⠀⢸⡇⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⡟⠒⢦⡀⠀⠀⠀
⠀⠀⣠⣤⣤⣼⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⡇⠀⠀⠀⠉⢢⣄⠀⠀⢿⠊⠳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣷⡄⠀⢷⠀⠀⠀
⠀⢰⠇⠀⣰⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⡌⣹⠗⠦⣬⣇⠀⠉⢢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⡀⢸⡄⠀⠀
⠀⡟⠀⣼⣯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣆⢹⡀⠀⠀⠀⠉⠁⠀⠀⢀⣀⡁⠀⠀⠉⠳⢴⡆⠀⠀⠀⠀⠀⠀⢹⣧⠈⡇⠀⠀
⠀⡇⠀⠀⢻⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣾⠻⠉⠛⠂⠀⠀⠀⠀⠀⠀⠻⠿⣿⣿⣿⣶⣦⡀⠛⣇⠀⠀⠀⠀⠀⣈⣿⠀⡇⠀⠀
⢸⡇⠀⠀⢠⣿⣷⣦⣀⡸⣷⣦⣶⡂⠉⠉⠉⢁⣤⣶⡶⠀⠀⠀⣀⣀⡴⠀⠀⠀⠀⠀⠀⠈⠉⠉⠁⠀⡟⢀⣴⣟⣰⣾⣿⣏⣠⠇⠀⠀
⠈⡇⠀⠀⢸⣿⠁⠉⣿⠛⠛⠃⡇⠀⠀⢠⣶⣿⡿⠛⠁⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠼⢿⠟⠿⢿⡏⠀⠘⣿⡀⠀⠀⠀
⠀⢷⣀⣀⣿⠇⠀⠀⢿⡇⠀⢀⢱⡀⠀⠛⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠀⠀⢸⠇⠀⠀⢹⣿⣄⠀⠀
⠀⠀⣉⣿⡏⠀⠀⠀⠀⠀⠀⢸⣇⣳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⣿⠃⠀⠀⠀⠀⠀⠀⣿⠈⢧⠀
⠀⠘⣿⣿⠁⠀⠀⠀⠀⠀⠀⠘⣿⡛⣶⠀⠀⣠⠔⠒⠛⠒⠦⡀⠀⠀⠀⠀⣠⡤⠶⠤⢤⣀⠀⠀⠀⢀⣏⡄⠀⠀⠀⠀⠀⡀⣿⡆⠈⣧
⣠⡾⠛⣿⣿⣧⠀⠀⠀⠀⢸⣿⠾⢿⡿⠀⣰⠃⠀⠀⠀⠀⠀⢹⡄⠀⠀⡼⠁⠀⠀⠀⠀⠈⠙⣦⠀⢸⣿⡇⣾⣣⡀⠀⢰⣿⣿⣿⣤⠾
⡟⠀⠀⠻⣿⡟⢷⡄⣤⡀⠈⣿⡀⣸⠇⠀⠏⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⡇⢀⡀⠀⠀⠀⠀⢀⡟⠀⠀⠋⣿⣿⣿⡇⣠⣿⠿⠛⢷⡀⠀
⠀⠀⠀⠀⣿⣇⣨⣿⣿⣿⣦⣽⣷⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠃⠀⠙⠢⠤⠤⠴⢾⠀⠀⠀⠀⢸⣷⣿⣿⠟⠁⠀⠀⠈⣧⠀
⠀⠀⠀⠀⠈⠉⠉⠁⠈⠉⠈⢉⣿⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⣿⠀
*/


发表于 2026-02-22 02:07:08 回复(1)
DP,详见注释。
from functools import cache
import sys 

sys.setrecursionlimit(114514)
n = int(input())
s = list(map(int, input().strip()))

@cache
# 从 i 位置开始,当前段的数字是 d,还需要 c 个段
# 做两件事:将当前位置纳入本段,然后决定下一个数字的选择
def f(i, d, c):
    global n 
    # 遍历完成
    # 如果还有未划分出的段,则非法
    if i == n:
        return 1145141919810 if c > 1 else 0
    # 未遍历完成,但是已经提前划分出所有段,原串有剩余,则非法
    if c == 0:
        return 1145141919810
    # 枚举下一个数字的选择:
    # 1. 延续当前段
    ans = f(i + 1, d, c)
    # 2. 切换到下一段(当需要更多段时才进行此项)
    if c > 0:
        ans = min(ans, f(i + 1, 1 - d, c - 1))
    # 如果数字不是需要的,那需要增加一次操作
    return ans + (s[i] ^ d)

print(min(f(0, 0, 4), f(0, 1, 4)))


发表于 2026-02-22 01:04:24 回复(0)
史山代码 样例都过不了 为什么提交过了
#include<bits/stdc++.h>
using namespace std;
const int maxn = 510;
int n,ans;
string s;
int sum1[maxn],sum0[maxn];
int main()
{
    scanf("%d",&n),ans = n;
    cin >> s,s = ' ' + s;
    for(int i=1;i<=n;i++)
    {
        sum0[i] = sum0[i - 1] + (s[i] == '0');
        sum1[i] = sum1[i - 1] + (s[i] == '1');
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=i+1;j<=n;j++)
        {
            for(int k=j+1;k<=n;k++)
            {
                int ans1 = sum1[i] + (sum0[j] - sum0[i]) + (sum1[k] - sum1[j]) + (sum0[n] - sum0[k]);
                int ans2 = sum0[i] + (sum1[j] - sum1[i]) + (sum0[k] - sum0[j]) + (sum1[n] - sum1[k]);
                ans = min(ans,min(ans1,ans2));
            }
        }
    }
    printf("%d\n",ans);
    return 0;
}


发表于 2026-02-22 17:24:42 回复(0)