Count substrings with equal consecutive groups of 0s and 1s using run lengths.
Problem Restatement
We are given a binary string s.
We need to count non-empty substrings that satisfy both rules:
| Rule | Meaning |
|---|---|
| Equal counts | The substring has the same number of 0s and 1s |
| Grouped characters | All 0s are consecutive and all 1s are consecutive |
Substrings that appear multiple times are counted multiple times. For example, "00110011" has two separate occurrences of "0011" and two separate occurrences of "01", and each occurrence counts.
Input and Output
| Item | Meaning |
|---|---|
| Input | A binary string s |
| Output | Number of valid substrings |
| Characters | Only '0' and '1' |
| Constraint | 1 <= s.length <= 10^5 |
Example function shape:
def countBinarySubstrings(s: str) -> int:
...Examples
Example 1:
s = "00110011"Valid substrings are:
"0011"
"01"
"1100"
"10"
"0011"
"01"Answer:
6The full string "00110011" is not valid because the 0s and 1s are split into multiple groups.
Example 2:
s = "10101"Valid substrings are:
"10"
"01"
"10"
"01"Answer:
4First Thought: Check Every Substring
A direct solution is to generate every substring and test whether:
- it has the same number of
0s and1s, - it has only one group of
0s and one group of1s.
This works logically, but there are O(n^2) substrings.
Checking each substring also costs time, so this approach is too slow for large input.
Key Insight
A valid substring must cross exactly one boundary between two groups.
For example:
000111has two groups:
| Group | Length |
|---|---|
000 | 3 |
111 | 3 |
Valid substrings around this boundary are:
01
0011
000111There are 3 of them.
Now consider:
00011The group lengths are 3 and 2.
Valid substrings are:
01
0011There are:
min(3, 2) = 2So every adjacent pair of groups contributes:
min(previous_group_length, current_group_length)valid substrings.
Algorithm
Track two group lengths while scanning:
| Variable | Meaning |
|---|---|
prev | Length of the previous consecutive character group |
curr | Length of the current consecutive character group |
Initialize:
prev = 0
curr = 1
answer = 0Then scan from index 1 to the end.
If the current character equals the previous character:
curr += 1we are still inside the same group.
Otherwise, the group changed.
At that boundary:
answer += min(prev, curr)
prev = curr
curr = 1After the loop, add the contribution from the final adjacent group pair:
answer += min(prev, curr)Return answer.
Walkthrough
Consider:
s = "00110011"The groups are:
00 | 11 | 00 | 11Group lengths:
[2, 2, 2, 2]Adjacent group contributions:
| Adjacent groups | Contribution |
|---|---|
00, 11 | min(2, 2) = 2 |
11, 00 | min(2, 2) = 2 |
00, 11 | min(2, 2) = 2 |
Total:
2 + 2 + 2 = 6Answer:
6Now consider:
s = "10101"The groups are:
1 | 0 | 1 | 0 | 1Group lengths:
[1, 1, 1, 1, 1]There are four adjacent group pairs, each contributing 1.
Answer:
4Correctness
Every valid substring contains exactly two consecutive groups: one group of 0s and one group of 1s.
It cannot contain more than two groups, because then either the 0s or the 1s would be split apart and would not be grouped consecutively.
For any two adjacent groups with lengths a and b, a valid substring can use x characters from the end of the first group and x characters from the start of the second group.
The value of x can be any integer from 1 through min(a, b).
So this adjacent pair contributes exactly min(a, b) valid substrings.
The algorithm scans the string and computes each group length. At every group boundary, it adds the contribution from the previous adjacent pair. After the scan, it adds the last pair.
Therefore, every valid substring is counted once, and no invalid substring is counted.
Complexity
Let n = len(s).
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We scan the string once |
| Space | O(1) | Only counters are stored |
Implementation
class Solution:
def countBinarySubstrings(self, s: str) -> int:
answer = 0
prev = 0
curr = 1
for i in range(1, len(s)):
if s[i] == s[i - 1]:
curr += 1
else:
answer += min(prev, curr)
prev = curr
curr = 1
answer += min(prev, curr)
return answerCode Explanation
We use:
prev = 0
curr = 1At the start, there is no previous group yet, and the current group contains the first character.
When adjacent characters are equal:
if s[i] == s[i - 1]:
curr += 1we extend the current group.
When the character changes:
answer += min(prev, curr)we finish one adjacent group pair and add its contribution.
Then the old current group becomes the previous group:
prev = currand the new current group starts with length 1:
curr = 1After the loop, the final pair has not been added yet, so we add it:
answer += min(prev, curr)Testing
def run_tests():
s = Solution()
assert s.countBinarySubstrings("00110011") == 6
assert s.countBinarySubstrings("10101") == 4
assert s.countBinarySubstrings("0") == 0
assert s.countBinarySubstrings("01") == 1
assert s.countBinarySubstrings("00110") == 3
assert s.countBinarySubstrings("000111") == 3
assert s.countBinarySubstrings("00011") == 2
assert s.countBinarySubstrings("111000111") == 6
print("all tests passed")
run_tests()Test meaning:
| Test | Expected | Why |
|---|---|---|
"00110011" | 6 | Official example |
"10101" | 4 | Official example |
"0" | 0 | No opposite bit group |
"01" | 1 | One valid substring |
"00110" | 3 | Contributions 2 + 1 |
"000111" | 3 | Three balanced substrings |
"00011" | 2 | Limited by smaller group |
"111000111" | 6 | Two boundaries, each contributes 3 |