计数二进制子串 图解题解
这道题到底在问什么
- 输入
- s = "00110011"
- 输出
- 6
最优解:为什么这么做
一句话答案:LeetCode 696 计数二进制子串:把 s 按连续相同字符切成一段段,相邻两段贡献 min(前段长,本段长) 个合法子串,一遍扫描累加,时间 O(n)、空间 O(1)。
要数的是哪一种子串
给一个只有 0 和 1 的字符串 s,数出有多少个子串(连续的一段字符)满足 0 和 1 个数相等、且所有 0 挨在一起、所有 1 也挨在一起。题面例子 s = "00110011",答案 6,"01"、"0011"、"1100" 都算,位置不同的相同子串各算一个;"0101" 这种来回交替的不算,没做到每种字符各自连续。
把每个子串截出来验一遍,慢在哪
长 n 的字符串有大约 n²/2 个子串,逐个截出来既数两种字符是否等量、又查是否各自连续,光子串就 O(n²) 个,每个再花 O(n),合起来 O(n³)。s 一长到十万级就跑不完,得找不逐串枚举的做法。
为什么合法子串只能卡在两段交界上
把 s 按连续相同字符切段,"00110011" 就是 00、11、00、11 四段。合法子串左半全是一种字符、右半全是另一种,中间只跨一个「段与段的交界」——跨两个交界,中间必冒出第三段,某种字符就断开、不再连续。所以每个合法子串都夹在相邻两段之间。
相邻两段设长度为 a 和 b,合法子串是「左边取几个、右边就取同样几个」,从 1 个到较短那段用光,一共 min(a, b) 个。两段各 2 个字符,能拼出 "01" 和 "0011"。把每对相邻段的 min 加起来就是答案。
一遍扫描怎么切段、边切边累加
不必把段长存成数组,两个变量就够。i 指向当前段开头,j 从 i 后一位出发,只要 s[j] 和 s[i] 相同就往右走,停下时这段长度 cur = j − i。用 pre 记前一段长度,每切出一段就把 ans 加上 min(pre, cur),再把 pre 换成 cur,i 跳到 j 开下一段。
两处得盯住:贡献取 min 不是相加,合法子串两侧必须一样长、被较短那段卡死;结算完必须 pre = cur,下一段要配的是刚封口这段的长度,忘了换就拿旧值去配。第一段前面没有段,pre 起始为 0,min(0, cur) 是 0,只把长度交给 pre。
"00110011" 切成四段,6 是一步步加出来的
整串切成四段,长度都是 2。第一段 "00":pre=0、cur=2,ans 加 min(0, 2)=0,pre 更新成 2。往后三段 cur 都是 2,每段 ans 加 min(2, 2)=2、pre 保持 2:第二段到 2,第三段到 4,第四段到 6。扫到末尾返回 6。
数着数着多出一倍,问题都出在结算那一步
最常见的错是把贡献写成 pre + cur、两段直接相加,六个子串一下变十几个——合法子串两边等长,只能被较短那段限制,取 min 不取和。其次是结算后忘了 pre = cur,下一段拿着旧长度去配,min 算在错的数上。还有内层没把相同字符走到底就结算,段没封口、cur 少数几个,往后全错。
时间上 i 和 j 都只朝右走、各扫一遍,O(n);只用 pre、cur、ans 几个变量,空间 O(1)。边界也想清:空串没有段,答案 0;整串同字符如 "0000" 只一段、配不成对,也是 0;最短一对 "01" 给出 1。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记牢一句话:相邻两段贡献 min(前段长, 本段长),全部加起来。下面每一帧都在套它。
- 4先看整串八个字符。接下来从左往右扫,把挨在一起、字符相同的归成同一段。
- 5第 1 段开始:下标 0 是字符 "0",本段长度先记成 1,把它涂绿。
- 6下标 1 还是 "0",和前一个相同,归进这一段,本段长度变成 2。
- 7下一个字符变了,第 1 段到此封口,"0" 一共连续 2 个。现在该和前一段配对了。
- 8它前面还没有任何段,pre = 0,配不出子串,本段贡献 0,ans 仍是 0。接着把本段长度 2 存进 pre,留给下一段配对。
- 9第 2 段开始:下标 2 是字符 "1",本段长度先记成 1,把它涂绿。
- 10下标 3 还是 "1",和前一个相同,归进这一段,本段长度变成 2。
- 11下一个字符变了,第 2 段到此封口,"1" 一共连续 2 个。现在该和前一段配对了。
- 12前一段长 pre = 2,本段长 run = 2,两段能拼出 min(2, 2) = 2 个合法子串。ans 从 0 累加到 2,再把 pre 更新成 2。
- 13第 3 段开始:下标 4 是字符 "0",本段长度先记成 1,把它涂绿。
- 14下标 5 还是 "0",和前一个相同,归进这一段,本段长度变成 2。
- 15下一个字符变了,第 3 段到此封口,"0" 一共连续 2 个。现在该和前一段配对了。
- 16前一段长 pre = 2,本段长 run = 2,两段能拼出 min(2, 2) = 2 个合法子串。ans 从 2 累加到 4,再把 pre 更新成 2。
- 17第 4 段开始:下标 6 是字符 "1",本段长度先记成 1,把它涂绿。
- 18下标 7 还是 "1",和前一个相同,归进这一段,本段长度变成 2。
- 19已经扫到末尾,第 4 段到此封口,"1" 一共连续 2 个。现在该和前一段配对了。
- 20前一段长 pre = 2,本段长 run = 2,两段能拼出 min(2, 2) = 2 个合法子串。ans 从 4 累加到 6,再把 pre 更新成 2。
- 21整串扫完,分成了四段,长度依次是 2、2、2、2。蓝色就是已经数过的段。
- 22四段产生三对相邻段,每对取较短段:min(2,2) 三次,每次给 2 个,加起来 6 个。
- 23把所有相邻段的 min 累加,得到答案 6,和题目给的输出对上了。
⚠️ 容易写错的地方
✗ 错:把两段长度相加当贡献
✓ 对:取 min(pre, cur)
合法子串两侧必须等长,受较短那段限制
✗ 错:结算后忘了把 pre 换成 cur
✓ 对:每段算完 pre = cur
不更新会拿错的前段长度去配下一段
✗ 错:内层 j 没吃完相同字符就结算
✓ 对:while 把同字符走到底再算 cur
段没封口,长度就是错的
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def countBinarySubstrings(self, s: str) -> int:
n = len(s)
ans = i = 0
pre = 0
while i < n:
j = i + 1
while j < n and s[j] == s[i]:
j += 1
cur = j - i
ans += min(pre, cur)
pre = cur
i = j
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int countBinarySubstrings(string s) {
int n = s.size();
int ans = 0;
int i = 0;
int pre = 0;
while (i < n) {
int j = i + 1;
while (j < n && s[j] == s[i]) {
++j;
}
int cur = j - i;
ans += min(pre, cur);
pre = cur;
i = j;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int countBinarySubstrings(String s) {
int n = s.length();
int ans = 0;
int i = 0;
int pre = 0;
while (i < n) {
int j = i + 1;
while (j < n && s.charAt(j) == s.charAt(i)) {
j++;
}
int cur = j - i;
ans += Math.min(pre, cur);
pre = cur;
i = j;
}
return ans;
}
}复杂度
时间
O(n)
i 与 j 都只单向走一遍,每个字符看一次
空间
O(1)
只用 pre、cur、ans 几个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 计数二进制子串 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
非得把每段的长度都存进数组,才能算出答案吗?+
不用,只留两个变量就行,这也是最省内存的写法。扫描时维护 pre(前一段长)和 cur(当前段长):遇到字符变化就结算 ans 加 min(pre, cur),再把 pre 换成 cur、cur 从头数起。全程 O(1) 空间,不必把 [2,2,2,2] 这个段长数组整个存下来;一边切段一边把答案加出来即可。
第一段为什么总是白扫、一个子串都不产生?+
因为合法子串靠相邻两段拼出来,而第一段前面没有任何段可配。代码里 pre 起始是 0,第一段结算时 min(0, cur) 恒为 0,ans 一动不动,它唯一的作用是把自己的长度存进 pre,留给第二段来配对。从第二段起才真正开始产生子串,所以第一段「白扫」是对的,不是漏算。
这题和求「最长连续相同段」是一个套路吗?+
都属于「按连续相同元素分组」这一类:先把序列切成若干等值段,再在段长上做文章。差别在段长上做什么运算——这题是相邻段取 min 累加,最长连续段那类是取所有段长里的最大值。认出「先分组、再在段长上运算」这个母题,一整类题都能顺着套。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 计数二进制子串 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。