2026-07-30:一次交换后的最长平衡子串。用go语言,给你一个只包含字符 ‘0‘ 和 ‘1‘ 的字符串 s。 定义平衡字符串为其中 ‘0‘ 和 ‘1‘ 的

15 阅读7分钟

2026-07-30:一次交换后的最长平衡子串。用go语言,给你一个只包含字符 '0' 和 '1' 的字符串 s。

定义平衡字符串为其中 '0' 和 '1' 的数量相等的字符串。

你可以在 s 上执行最多一次操作:选择任意两个位置并交换它们的字符(也可以不交换)。完成这步之后,你需要从 s 中截取一个连续的平衡子串。

目标是求出通过上述方式能得到的最长平衡子串的长度。

1 <= s.length <= 100000。

s 仅由字符 '0' 和 '1' 组成。

输入: s = "100001"。

输出: 4。

解释:

交换 "100001" 中标出的两个字符,字符串变为 "101000"。

选择子串 "101000",它是平衡的,因为其中包含两个 '0' 和两个 '1'。

题目来自力扣3900。

详细步骤分解

第一步:预处理全局信息

  • 统计整个字符串中 '0' 的总数 total0'1' 的总数 total1
  • 这两个值用于后续判断:当我们考虑交换操作时,需要确保有足够的字符可供交换

第二步:初始化数据结构

  • 创建一个哈希表 pos,键是前缀和的值,值是一个列表,用于存储该前缀和最早出现的两个位置索引
  • 初始时,将前缀和 0 对应的位置设为 -1(表示在字符串开始之前)
  • 设置当前前缀和 sum = 0,答案 ans = 0

第三步:遍历字符串,维护前缀和

从左到右遍历字符串的每个位置 i 和字符 ch

3.1 更新当前前缀和

  • 如果 ch'1'sum += 1
  • 如果 ch'0'sum -= 1

3.2 记录前缀和出现位置

  • 对于当前的 sum 值,如果它对应的位置列表长度还不足2个,就将当前索引 i 加入该列表
  • 这样我们保留了每个前缀和值最早出现的两个位置,第一个位置用于常规匹配,第二个位置作为备选

3.3 情况一:不进行交换

  • 查找之前是否出现过相同的 sum
  • 如果出现过,说明从该位置到当前位置的子串,0和1的数量差为0,即它是平衡的
  • 计算子串长度:i - pos[sum][0](当前位置减去最早出现位置)
  • 更新答案 ans

3.4 情况二:交换一个子串内的 '1' 与子串外的一个 '0'

  • 查找前缀和 sum - 2 的出现位置(记为 p
  • 为什么是 sum - 2?因为将子串内的一个 '1'(贡献+1)换成 '0'(贡献-1),前缀和会减少2
  • 这意味着如果从 p[0]i 的子串经过这样的交换后,可以变成平衡子串

判断条件:

  • 计算把这个子串变成平衡需要多少个 '0'(i - p[0] - 2) / 2
    • 推导:当前子串内 '1''0' 多2个,交换一个 '1' 出去能减少这个差值
    • 公式 (长度 - 2) / 2 表示需要的 '0' 数量(交换后子串长度不变,只是内部字符变了)
  • 如果需要的 '0' 数量小于全局 total0,说明有足够的 '0' 可以在子串外进行交换
    • 满足条件,就用最早位置 p[0] 计算长度
  • 如果不满足(即需要的 '0' 数量 >= total0),但该前缀和值有第二个出现位置 p[1]
    • 使用备选位置 p[1] 计算长度(更短的子串,需要的 '0' 更少,可能满足条件)
  • 更新答案 ans

3.5 情况三:交换一个子串内的 '0' 与子串外的一个 '1'

  • 查找前缀和 sum + 2 的出现位置
  • 为什么是 sum + 2?因为将子串内的一个 '0'(贡献-1)换成 '1'(贡献+1),前缀和会增加2
  • 判断逻辑与情况二对称:
    • 计算需要的 '1' 数量:(i - p[0] - 2) / 2
    • 如果小于全局 total1,用 p[0];否则如果有备选位置,用 p[1]
  • 更新答案 ans

第四步:返回结果

遍历完成后,ans 中存储的就是能获得的最长平衡子串长度。

关键理解点

  1. 为什么只记录最早的两个位置?

    • 第一个位置用于最大化子串长度
    • 第二个位置作为备选,当第一个位置无法满足交换条件时使用(因为更短的子串需要的交换资源更少)
  2. 交换的本质

    • 不是真正执行交换,而是通过数学条件判断某个子串经过一次交换后能否变平衡
    • 利用前缀和差值 ±2 来表示一次交换对平衡状态的影响
  3. 资源检查

    • 通过 (长度-2)/2 计算所需的另一种字符数量
    • 与全局总量比较,确保字符串中有足够的字符可供交换

复杂度分析

时间复杂度:O(n)

  • 遍历字符串一次,每次迭代执行常数次哈希表操作和简单计算
  • 哈希表的查找和插入都是 O(1) 均摊时间复杂度
  • n 是字符串长度(最大100000)

额外空间复杂度:O(n)

  • 哈希表 pos 存储了所有出现过的前缀和值及其最早的两个位置
  • 前缀和的变化范围是 [-n, n],最坏情况下可能存储 O(n) 个不同的前缀和值
  • 每个前缀和值最多存储2个位置索引,因此总存储量为 O(n)

Go完整代码如下:

package main

import (
	"fmt"
	"strings"
)

func longestBalanced(s string) (ans int) {
	total0 := strings.Count(s, "0")
	total1 := len(s) - total0

	pos := map[int][]int{0: {-1}}
	sum := 0 // 前缀和
	for i, ch := range s {
		sum += int(ch-'0')*2 - 1

		if p := pos[sum]; len(p) < 2 {
			pos[sum] = append(p, i)
		}

		// 不交换
		ans = max(ans, i-pos[sum][0])

		// 交换子串内的一个 1 和子串外的一个 0
		if p, ok := pos[sum-2]; ok {
			if (i-p[0]-2)/2 < total0 {
				ans = max(ans, i-p[0])
			} else if len(p) > 1 {
				ans = max(ans, i-p[1])
			}
		}

		// 交换子串内的一个 0 和子串外的一个 1
		if p, ok := pos[sum+2]; ok {
			if (i-p[0]-2)/2 < total1 {
				ans = max(ans, i-p[0])
			} else if len(p) > 1 {
				ans = max(ans, i-p[1])
			}
		}
	}
	return
}

func main() {
	s := "100001"
	result := longestBalanced(s)
	fmt.Println(result)
}

在这里插入图片描述

Python完整代码如下:

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

def longest_balanced(s: str) -> int:
    total0 = s.count('0')
    total1 = len(s) - total0

    pos = {0: [-1]}
    prefix_sum = 0
    ans = 0

    for i, ch in enumerate(s):
        # 将 '0' 映射为 -1,'1' 映射为 +1
        prefix_sum += 1 if ch == '1' else -1

        # 记录每个前缀和最早出现的两个位置
        if prefix_sum not in pos:
            pos[prefix_sum] = []
        if len(pos[prefix_sum]) < 2:
            pos[prefix_sum].append(i)

        # 情况1:不交换
        ans = max(ans, i - pos[prefix_sum][0])

        # 情况2:交换子串内的一个 '1' 和子串外的一个 '0'
        if prefix_sum - 2 in pos:
            p = pos[prefix_sum - 2]
            # 需要的 '0' 数量 = (子串长度 - 2) / 2,实际是 (i - p[0] - 2) // 2
            if (i - p[0] - 2) // 2 < total0:
                ans = max(ans, i - p[0])
            elif len(p) > 1:
                ans = max(ans, i - p[1])

        # 情况3:交换子串内的一个 '0' 和子串外的一个 '1'
        if prefix_sum + 2 in pos:
            p = pos[prefix_sum + 2]
            if (i - p[0] - 2) // 2 < total1:
                ans = max(ans, i - p[0])
            elif len(p) > 1:
                ans = max(ans, i - p[1])

    return ans


if __name__ == "__main__":
    s = "100001"
    result = longest_balanced(s)
    print(result)

在这里插入图片描述

C++完整代码如下:

#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <algorithm>

using namespace std;

int longestBalanced(string s) {
    int total0 = count(s.begin(), s.end(), '0');
    int total1 = s.length() - total0;

    unordered_map<int, vector<int>> pos;
    pos[0] = {-1};
    int prefix_sum = 0;
    int ans = 0;

    for (int i = 0; i < s.length(); i++) {
        // 将 '0' 映射为 -1,'1' 映射为 +1
        prefix_sum += (s[i] == '1') ? 1 : -1;

        // 记录每个前缀和最早出现的两个位置
        if (pos[prefix_sum].size() < 2) {
            pos[prefix_sum].push_back(i);
        }

        // 情况1:不交换
        ans = max(ans, i - pos[prefix_sum][0]);

        // 情况2:交换子串内的一个 '1' 和子串外的一个 '0'
        if (pos.count(prefix_sum - 2)) {
            auto& p = pos[prefix_sum - 2];
            if ((i - p[0] - 2) / 2 < total0) {
                ans = max(ans, i - p[0]);
            } else if (p.size() > 1) {
                ans = max(ans, i - p[1]);
            }
        }

        // 情况3:交换子串内的一个 '0' 和子串外的一个 '1'
        if (pos.count(prefix_sum + 2)) {
            auto& p = pos[prefix_sum + 2];
            if ((i - p[0] - 2) / 2 < total1) {
                ans = max(ans, i - p[0]);
            } else if (p.size() > 1) {
                ans = max(ans, i - p[1]);
            }
        }
    }

    return ans;
}

int main() {
    string s = "100001";
    int result = longestBalanced(s);
    cout << result << endl;
    return 0;
}

在这里插入图片描述