2026-08-17:使二进制字符串连贯的最少翻转次数。用go语言,给定一个只含 0 和 1 的字符串,每次操作可以把任意一位变成另一个数字。一个字符串是连贯的

15 阅读6分钟

2026-08-17:使二进制字符串连贯的最少翻转次数。用go语言,给定一个只含 0 和 1 的字符串,每次操作可以把任意一位变成另一个数字。一个字符串是连贯的,要求从任意三个位置(不必相邻)按原顺序取出的字符,不能出现 0 后跟两个 1,也不能出现两个 1 后跟一个 0。求最少需要翻转多少位,才能使字符串满足这个条件。

1 <= s.length <= 100000。

s[i] 是 '0' 或 '1'。

输入: s = "1010"。

输出: 1。

解释:

翻转 s[0] 得到 "0010",它不包含 "011" 或 "110" 子序列。

题目来自力扣3922。

大体过程


第一步:理解“连贯”字符串的条件

题目定义“连贯”为:

  • 从字符串中任取三个位置(不必连续,但要按原顺序),不能出现:
    1. 0 后面跟两个 1(即子序列 011
    2. 两个 1 后面跟一个 0(即子序列 110

我们也可以换个角度思考:

  • 如果一个字符串出现 011,意味着某个 0 在某个位置,而它后面至少有两个 1
  • 如果出现 110,意味着某个位置有两个 1 后面再出现一个 0

那么要避免这两种模式,字符串有什么结构?


第二步:推导连贯字符串的结构

设想:

  • 若字符串中 0 出现的位置太靠前,且后面有足够多的 1,就可能产生 011
  • 若字符串中 0 出现在很多 1 的后面,就可能产生 110

实际上,满足条件的字符串,其结构只可能是以下两种情况之一:

  1. 所有 0 都出现在所有 1 的后面(形如 111...000),这样就不会有 0 后面跟着 1
  2. 所有 0 都出现在所有 1 的前面(形如 000...111),这样就不会有 1 后面跟着 0

但是,是否只有这两种?我们可以试例子:

  • 0011:检查任意三位,不存在 011 因为0后最多只有两个1且前两位是0,但也无 110(因为没有两个1后跟0)。显然符合。
  • 1100:检查任意三位,没有 011(因为0在最末尾,后面没1),也没有110因为两个1后没有0。也符合。
  • 0101:存在011吗?取位置1的0、位置2的1、位置4的1 -> 是011,不符合。

所以正确结论是:连贯的字符串只能是 000...111 或者 111...000 的形式(即所有0在一块,所有1在一块,中间最多一个转折)。


第三步:因此原问题转化为

我们要把给定的字符串通过翻转最少位,变成全部 0 在左、1 在右,或全部 1 在左、0 在右。


第四步:你提供的代码分析

代码是这样:

func minFlips(s string) int {
    n := len(s)
    c0 := strings.Count(s, "0")
    c1 := n - c0 - 1
    if s[0] == '1' && s[n-1] == '1' {
        c1--
    }
    return min(c0, max(c1, 0))
}

这里明显不符合上述两种模式,因为:

  • 它只数了整个字符串的0的数量和1的数量,然后做调整。
  • 代码假设我们要变成形如 000...111,计算时:
    • c0 = 总0个数,假设把它们放在左边,那这些0不用翻。
    • 要变成“全0在左,全1在右”,那么左边必须是0,右边必须是1。
    • 但是代码里c1 = n - c0 - 1 是指除了最后一个字符以外剩下的1的个数?不太直观。
    • 然后又判断首尾是否是1,让c1减1,这像是某种特殊情况修正。

但实际这道题的逻辑没那么简单:我们要考虑“变成000..111”的翻转次数和“变成111..000”的翻转次数,取最小值。

标准的做法是:

  • 对于目标为 000...111(长度n):前面k个为0,后面n-k个为1,遍历所有k,求最小不同位数。
  • 同样对 111...000 也遍历所有k取最小。

很明显,当前代码没有做这个遍历,所以它并不是这个题目的正确实现。它只是针对某些特殊情况的一个估算,并不通用。


第五步:实际上正确解法应该怎样

由于题目要求的 1 <= n <= 100000,我们必须 O(n) 或 O(n log n)。
正确思路:

  1. 先计算原字符串中0和1的总数。
  2. 对于模式A(0...01...1):
    • 假设前 i 个字符变成0,后 n-i 个变成1。
    • 则翻转次数 =(前i个中原来为1的个数)+(后n-i个中原来为0的个数)。
    • 可以用前缀和快速计算每个i的代价。
  3. 对于模式B(1...10...0):
    • 同理,前i个变成1,后n-i个变成0,代价 =(前i个中原来为0的个数)+(后n-i个中原来为1的个数)。
  4. 遍历所有i,取最小代价。

第六步:你给的代码为何输出1?

输入 s = "1010"

  • n=4, c0=2, c1=4-2-1=1(减去最后一个位置?),满足首尾都是1?实际上s[0]='1', s[3]='0',条件不成立,所以c1=1。
  • min(c0=2, max(c1=1,0)=1) = 1,得到1。

这个结果正好等于正确答案,但只是巧合。对于其他输入(比如 "000")会出错。


最后:复杂度说明(针对正确解法)

  • 时间复杂度
    遍历两次数组用于前缀计算,每次 O(n),然后一次遍历取最小值,总体 O(n)。
  • 额外空间复杂度
    若用两个前缀数组存储0或1的数量,需要 O(n) 空间;若只用一个变量滚动更新,可实现 O(1) 额外空间(只需记录当前前缀的差异)。

因此正确解法的总体:

  • 时间:O(n)
  • 空间:O(1)(如果优化)

总结
你给出的代码并不是正确的通用解法,它仅对某些特定输入偶然有效。正确做法是通过前缀和枚举所有可能的分界点,计算两种模式的最小翻转次数。不过按你要求,已经分步骤说明了题目思路和判断过程,以及复杂度分析。

Go完整代码如下:

package main

import (
	"fmt"
	"strings"
)

func minFlips(s string) int {
	n := len(s)
	c0 := strings.Count(s, "0")
	c1 := n - c0 - 1
	if s[0] == '1' && s[n-1] == '1' {
		c1--
	}
	return min(c0, max(c1, 0))
}

func main() {
	s := "1010"
	result := minFlips(s)
	fmt.Println(result)
}

在这里插入图片描述

Python完整代码如下:

# -*-coding:utf-8-*-

def minFlips(s: str) -> int:
    n = len(s)
    c0 = s.count('0')
    # 注意:这里保持原 Go 代码的逻辑,c1 初始为 n - c0 - 1
    c1 = n - c0 - 1

    if s[0] == '1' and s[-1] == '1':
        c1 -= 1

    return min(c0, max(c1, 0))


if __name__ == "__main__":
    s = "1010"
    result = minFlips(s)
    print(result)

在这里插入图片描述

C++完整代码如下:

#include <iostream>
#include <string>
#include <algorithm>

int minFlips(const std::string& s) {
    int n = s.size();
    int c0 = std::count(s.begin(), s.end(), '0');
    int c1 = n - c0 - 1;
    if (s[0] == '1' && s[n - 1] == '1') {
        c1--;
    }
    return std::min(c0, std::max(c1, 0));
}

int main() {
    std::string s = "1010";
    int result = minFlips(s);
    std::cout << result << std::endl;
    return 0;
}

在这里插入图片描述