题目描述
思路解析
一句话答案: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。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记牢一句话:相邻两段贡献 min(前段长, 本段长),全部加起来。下面每一帧都在套它。
先看整串八个字符。接下来从左往右扫,把挨在一起、字符相同的归成同一段。
第 1 段开始:下标 0 是字符 "0",本段长度先记成 1,把它涂绿。
下标 1 还是 "0",和前一个相同,归进这一段,本段长度变成 2。
下一个字符变了,第 1 段到此封口,"0" 一共连续 2 个。现在该和前一段配对了。
它前面还没有任何段,pre = 0,配不出子串,本段贡献 0,ans 仍是 0。接着把本段长度 2 存进 pre,留给下一段配对。
第 2 段开始:下标 2 是字符 "1",本段长度先记成 1,把它涂绿。
下标 3 还是 "1",和前一个相同,归进这一段,本段长度变成 2。
下一个字符变了,第 2 段到此封口,"1" 一共连续 2 个。现在该和前一段配对了。
前一段长 pre = 2,本段长 run = 2,两段能拼出 min(2, 2) = 2 个合法子串。ans 从 0 累加到 2,再把 pre 更新成 2。
第 3 段开始:下标 4 是字符 "0",本段长度先记成 1,把它涂绿。
下标 5 还是 "0",和前一个相同,归进这一段,本段长度变成 2。
下一个字符变了,第 3 段到此封口,"0" 一共连续 2 个。现在该和前一段配对了。
前一段长 pre = 2,本段长 run = 2,两段能拼出 min(2, 2) = 2 个合法子串。ans 从 2 累加到 4,再把 pre 更新成 2。
第 4 段开始:下标 6 是字符 "1",本段长度先记成 1,把它涂绿。
下标 7 还是 "1",和前一个相同,归进这一段,本段长度变成 2。
已经扫到末尾,第 4 段到此封口,"1" 一共连续 2 个。现在该和前一段配对了。
前一段长 pre = 2,本段长 run = 2,两段能拼出 min(2, 2) = 2 个合法子串。ans 从 4 累加到 6,再把 pre 更新成 2。
整串扫完,分成了四段,长度依次是 2、2、2、2。蓝色就是已经数过的段。
四段产生三对相邻段,每对取较短段:min(2,2) 三次,每次给 2 个,加起来 6 个。
把所有相邻段的 min 累加,得到答案 6,和题目给的输出对上了。
边界先想清:空串、整串同字符、最短的一对。
面试重点:认出「分组 + 在段长上运算」,并能压到只用两个变量。
参考代码
from __future__ import annotationsfrom 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 ans复杂度
- 时间:O(n),i 与 j 都只单向走一遍,每个字符看一次
- 空间:O(1),只用 pre、cur、ans 几个变量
易错点
面试追问把动画讲成自己的话
追问能不能不存所有段长,只用两个变量?
追问这题和「最长连续段」类题是一个套路吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
字符的最短距离
LeetCode 821 · 简单 · 沿着 双指针套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题