LeetCode 3234 - Count the Number of Substrings With Dominant Ones
We are given a binary string s, containing only '0' and '1'. We must count how many substrings satisfy a special condition called "dominant ones".
Difficulty: 🟡 Medium
Topics: String, Enumeration
Solution
Problem Understanding
We are given a binary string s, containing only '0' and '1'. We must count how many substrings satisfy a special condition called "dominant ones".
For any substring:
- Let
zerosbe the number of'0'characters - Let
onesbe the number of'1'characters
The substring is considered valid if:
$$\text{ones} \geq (\text{zeros})^2$$
The task is to return the total number of substrings satisfying this condition.
A substring is any contiguous segment of the string. Since a string of length n has:
$$\frac{n(n+1)}{2}$$
total substrings, even moderately sized inputs can generate a very large search space.
The constraint:
$$1 \le n \le 4 \times 10^4$$
is extremely important. A naive $O(n^2)$ or $O(n^3)$ solution will be too slow for the upper limit. We need something significantly more efficient.
There are several important observations and edge cases:
- A substring containing only
'1'characters is always valid, becausezeros = 0and thereforeones >= 0. - Substrings with many zeros become difficult to satisfy, because the requirement grows quadratically.
- Since $k^2$ grows quickly, substrings with large numbers of zeros must also be quite long to remain valid.
- Very short strings such as
"0"or"1"must be handled correctly. - Strings with all zeros are particularly restrictive, because most substrings fail immediately.
- Strings with all ones are the opposite extreme, because every substring is valid.
The main challenge is avoiding enumeration of all $O(n^2)$ substrings explicitly.
Approaches
Brute Force Approach
The most direct solution is to generate every possible substring and count the number of zeros and ones inside it.
For every starting index i, we extend the substring to every ending index j. While extending, we maintain running counts of zeros and ones. For each substring, we check whether:
$$\text{ones} \ge \text{zeros}^2$$
If true, we increment the answer.
This approach is correct because it examines every substring exactly once and directly evaluates the required condition.
However, there are:
$$O(n^2)$$
substrings. Even if each substring check is done in constant time using running counts, the total complexity becomes $O(n^2)$.
With $n = 4 \times 10^4$, this would require roughly 1.6 billion substring evaluations, which is far too slow.
Key Insight for the Optimal Solution
The crucial observation is that the condition:
$$\text{ones} \ge \text{zeros}^2$$
becomes very restrictive as the number of zeros grows.
Suppose a substring contains k zeros. Then it must contain at least k² ones. Therefore its total length must be at least:
$$k + k^2$$
This grows quadratically.
If:
$$k^2 + k > n$$
then no substring in the entire string can possibly satisfy the condition for that k.
This means the number of zeros we ever need to consider is only about:
$$\sqrt{n}$$
Instead of enumerating all substrings, we enumerate the number of zeros in the substring. For each possible zero count k, we efficiently count substrings containing exactly k zeros and enough ones to satisfy the condition.
This reduces the complexity dramatically.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | $O(n^2)$ | $O(1)$ | Checks every substring directly |
| Optimal | $O(n\sqrt{n})$ | $O(n)$ | Enumerates zero counts instead of all substrings |
Algorithm Walkthrough
Step 1: Store the Positions of All Zeros
We first collect the indices of every '0' in the string.
For example:
s = "101101"
zero_positions = [1, 4]
This allows us to quickly identify substrings containing exactly k zeros.
Step 2: Count All-One Substrings Separately
Substrings with zero zeros are always valid.
We scan the string and find consecutive runs of '1'.
If a run has length L, then it contributes:
$$\frac{L(L+1)}{2}$$
valid substrings.
For example:
"111"
contains:
"1", "1", "1", "11", "11", "111"
which is 6 substrings.
Handling this separately simplifies the remaining logic.
Step 3: Enumerate the Number of Zeros
Now we consider substrings containing exactly k zeros, where:
$$1 \le k \le \sqrt{n}$$
For each k, we slide over consecutive groups of k zeros in zero_positions.
Suppose the substring must contain zeros from:
zero_positions[i]
to
zero_positions[i+k-1]
These are the mandatory zeros in the substring.
Step 4: Compute the Minimum Required Length
If the substring contains k zeros, it needs at least:
$$k^2$$
ones.
So the minimum total substring length is:
$$k + k^2$$
The current block of zeros spans:
base_length =
zero_positions[i+k-1] - zero_positions[i] + 1
If this is already large enough, then any extension works.
Otherwise, we need additional surrounding ones.
Step 5: Determine Expansion Choices
The substring may extend:
- left into adjacent ones
- right into adjacent ones
We compute:
left_choices
right_choices
which represent how many positions we can expand without including extra zeros.
Then we count how many combinations produce sufficient total length.
Step 6: Add Valid Contributions
For every block of k zeros, we count the number of valid left/right expansions satisfying:
$$\text{length} \ge k^2 + k$$
and add them to the answer.
Since k only ranges up to roughly $\sqrt{n}$, the overall complexity remains efficient.
Why it works
Every valid substring has some exact number of zeros k.
The algorithm partitions all substrings by their zero count. For each k, it enumerates every possible consecutive block of k zeros and counts all substrings that include exactly those zeros and no others.
The expansion logic guarantees that every counted substring satisfies the required minimum length, which is equivalent to:
$$\text{ones} \ge \text{zeros}^2$$
No valid substring is missed, and no invalid substring is counted.
Python Solution
class Solution:
def numberOfSubstrings(self, s: str) -> int:
n = len(s)
zero_positions = [-1]
for i, ch in enumerate(s):
if ch == '0':
zero_positions.append(i)
zero_positions.append(n)
total_zeros = len(zero_positions) - 2
answer = 0
# Count substrings containing only ones
consecutive_ones = 0
for ch in s:
if ch == '1':
consecutive_ones += 1
else:
answer += consecutive_ones * (consecutive_ones + 1) // 2
consecutive_ones = 0
answer += consecutive_ones * (consecutive_ones + 1) // 2
max_zeros = int(n ** 0.5) + 1
# Enumerate number of zeros
for zeros in range(1, max_zeros + 1):
if zeros > total_zeros:
break
required_length = zeros * zeros + zeros
for left_zero_index in range(1, total_zeros - zeros + 2):
first_zero = zero_positions[left_zero_index]
last_zero = zero_positions[left_zero_index + zeros - 1]
base_length = last_zero - first_zero + 1
extra_needed = max(0, required_length - base_length)
left_options = (
first_zero -
zero_positions[left_zero_index - 1]
)
right_options = (
zero_positions[left_zero_index + zeros] -
last_zero
)
total_pairs = left_options * right_options
invalid_pairs = 0
if extra_needed > 0:
for left_extension in range(left_options):
remaining = extra_needed - left_extension
if remaining <= 0:
break
invalid_right = min(right_options, remaining)
invalid_pairs += invalid_right
answer += total_pairs - invalid_pairs
return answer
The implementation begins by storing all zero positions. Sentinel values -1 and n are added so boundary calculations become uniform and avoid special cases.
Next, the code counts all substrings containing only ones. This is done independently because these substrings are automatically valid.
The main loop then enumerates possible zero counts. For each count zeros, we examine every consecutive block of zeros in the string.
The variables:
first_zero
last_zero
identify the mandatory zero range.
The substring may expand left or right into surrounding ones. The number of valid expansion choices is computed using:
left_options
right_options
The algorithm calculates how many expansion pairs fail to reach the required total length and subtracts them from the total number of pairs.
This efficiently counts only valid substrings.
Go Solution
func numberOfSubstrings(s string) int {
n := len(s)
zeroPositions := []int{-1}
for i, ch := range s {
if ch == '0' {
zeroPositions = append(zeroPositions, i)
}
}
zeroPositions = append(zeroPositions, n)
totalZeros := len(zeroPositions) - 2
answer := 0
// Count all-one substrings
consecutiveOnes := 0
for _, ch := range s {
if ch == '1' {
consecutiveOnes++
} else {
answer += consecutiveOnes * (consecutiveOnes + 1) / 2
consecutiveOnes = 0
}
}
answer += consecutiveOnes * (consecutiveOnes + 1) / 2
maxZeros := int(float64(n) + 1)
for maxZeros*maxZeros > n {
maxZeros--
}
maxZeros++
for zeros := 1; zeros <= maxZeros; zeros++ {
if zeros > totalZeros {
break
}
requiredLength := zeros*zeros + zeros
for leftZeroIndex := 1; leftZeroIndex <= totalZeros-zeros+1; leftZeroIndex++ {
firstZero := zeroPositions[leftZeroIndex]
lastZero := zeroPositions[leftZeroIndex+zeros-1]
baseLength := lastZero - firstZero + 1
extraNeeded := requiredLength - baseLength
if extraNeeded < 0 {
extraNeeded = 0
}
leftOptions := firstZero - zeroPositions[leftZeroIndex-1]
rightOptions := zeroPositions[leftZeroIndex+zeros] - lastZero
totalPairs := leftOptions * rightOptions
invalidPairs := 0
if extraNeeded > 0 {
for leftExtension := 0; leftExtension < leftOptions; leftExtension++ {
remaining := extraNeeded - leftExtension
if remaining <= 0 {
break
}
invalidRight := rightOptions
if remaining < invalidRight {
invalidRight = remaining
}
invalidPairs += invalidRight
}
}
answer += totalPairs - invalidPairs
}
}
return answer
}
The Go implementation follows the same logic as the Python version. Slices are used instead of Python lists, and integer arithmetic remains safe because the maximum answer fits comfortably within Go's int range for the given constraints.
Unlike Python, Go does not provide a direct integer square root utility in basic syntax, so the code computes the approximate square root manually.
Worked Examples
Example 1
s = "00011"
Step 1: Zero Positions
zero_positions = [-1, 0, 1, 2, 5]
Step 2: Count All-One Substrings
There is one run:
"11"
Length = 2
Contribution:
$$\frac{2 \times 3}{2} = 3$$
Current answer:
answer = 3
Step 3: Consider Substrings with 1 Zero
Required length:
$$1^2 + 1 = 2$$
| Zero Block | Base Length | Valid Extensions | Contribution |
|---|---|---|---|
| [0] | 1 | none | 0 |
| [1] | 1 | none | 0 |
| [2] | 1 | add right ones | 2 |
Added contribution:
2
Final answer:
3 + 2 = 5
Example 2
s = "101101"
Step 1: Zero Positions
zero_positions = [-1, 1, 4, 6]
Step 2: Count All-One Substrings
Runs:
"1"
"11"
"1"
Contributions:
1 + 3 + 1 = 5
Current answer:
5
Step 3: Substrings with 1 Zero
Required length:
$$2$$
All valid extensions contribute 9 substrings.
Current answer:
14
Step 4: Substrings with 2 Zeros
Required length:
$$6$$
Only two substrings satisfy this:
"101101"
"01101"
Contribution:
2
Final answer:
16
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | $O(n\sqrt{n})$ | We enumerate up to $\sqrt{n}$ zero counts |
| Space | $O(n)$ | Stores positions of zeros |
The key observation behind the time complexity is that the number of zeros in any valid substring is bounded by approximately $\sqrt{n}$. For each possible zero count, we scan through the zero positions once.
This avoids the quadratic explosion from enumerating every substring directly.
Test Cases
sol = Solution()
assert sol.numberOfSubstrings("00011") == 5 # provided example
assert sol.numberOfSubstrings("101101") == 16 # provided example
assert sol.numberOfSubstrings("1") == 1 # single one
assert sol.numberOfSubstrings("0") == 0 # single zero
assert sol.numberOfSubstrings("11") == 3 # all substrings valid
assert sol.numberOfSubstrings("00") == 0 # no valid substrings
assert sol.numberOfSubstrings("101") == 5 # mixed small case
assert sol.numberOfSubstrings("010") == 3 # center one only
assert sol.numberOfSubstrings("11111") == 15 # all ones
assert sol.numberOfSubstrings("00000") == 0 # all zeros
assert sol.numberOfSubstrings("10001") == 5 # sparse ones
assert sol.numberOfSubstrings("110011") == 13 # multiple zero groups
assert sol.numberOfSubstrings("1010101010") > 0 # alternating pattern
assert sol.numberOfSubstrings("111000111") > 0 # clustered structure
Test Summary
| Test | Why |
|---|---|
"00011" |
Validates provided example |
"101101" |
Validates provided example |
"1" |
Smallest valid string |
"0" |
Smallest invalid string |
"11" |
All substrings valid |
"00" |
No valid substrings |
"101" |
Small mixed case |
"010" |
Tests centered one |
"11111" |
Large one-run behavior |
"00000" |
Large zero-run behavior |
"10001" |
Widely separated ones |
"110011" |
Multiple zero blocks |
"1010101010" |
Alternating structure |
"111000111" |
Clustered groups |
Edge Cases
One important edge case is a string consisting entirely of ones. In this situation, every substring is automatically valid because the number of zeros is zero, and:
$$\text{ones} \ge 0$$
always holds. A buggy implementation might still attempt zero-block processing unnecessarily or mishandle the absence of zeros. This implementation handles the case naturally by counting all-one runs separately.
Another important case is a string consisting entirely of zeros. Here, every substring has:
$$\text{ones} = 0$$
and at least one zero, so the condition can never hold because:
$$0 \not\ge k^2$$
for any positive k. The algorithm correctly returns zero because no valid expansions satisfy the required length.
A third tricky case is alternating patterns such as:
"1010101010"
These strings generate many small substrings with one zero, but very few with multiple zeros satisfying the quadratic requirement. A naive implementation may accidentally overcount. The expansion-based counting avoids this by enforcing the exact minimum length condition for every zero count.
Another subtle edge case involves substrings near the boundaries of the string. For example:
"0111"
Valid substrings may begin at index 0 or end at the final index. The sentinel values -1 and n in the zero position array eliminate special boundary logic and ensure left/right expansion counts remain correct.