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 中存储的就是能获得的最长平衡子串长度。
关键理解点
-
为什么只记录最早的两个位置?
- 第一个位置用于最大化子串长度
- 第二个位置作为备选,当第一个位置无法满足交换条件时使用(因为更短的子串需要的交换资源更少)
-
交换的本质
- 不是真正执行交换,而是通过数学条件判断某个子串经过一次交换后能否变平衡
- 利用前缀和差值
±2来表示一次交换对平衡状态的影响
-
资源检查
- 通过
(长度-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;
}