3234. Count the Number of Substrings With Dominant Ones
Problem Description
You are given a binary string s (a string consisting only of the characters 0 and 1).
Your task is to count how many non-empty substrings of s have dominant ones.
A string is said to have dominant ones if the number of ones in it is greater than or equal to the square of the number of zeros in it. In other words, if a substring contains cnt1 ones and cnt0 zeros, it has dominant ones when:
cnt1 >= cnt0 * cnt0
Return the total number of substrings that satisfy this condition.
Key points to understand:
- A substring is a contiguous sequence of characters within the string. Different starting and ending positions are counted as different substrings, even if their contents look identical.
- For each candidate substring, you compare the count of
1s against the square of the count of0s. - Notice that if a substring contains zero
0s (cnt0 = 0), then the condition becomescnt1 >= 0, which is always true. So substrings made up entirely of1s always qualify. - Because the count of ones cannot exceed the total length
n, the number of zeroscnt0in any valid substring is bounded bysqrt(n). This observation keeps the number of distinct zero-counts small and is the basis for an efficient enumeration.
How We Pick the Algorithm
Why Sliding Window?
This problem maps to Sliding Window through a short path in the full flowchart.
The problem asks to count substrings with dominant ones, requiring a sliding window that expands and contracts based on a zero-count condition.
Open in FlowchartIntuition
The first thing to notice is the relationship between the number of zeros and the number of ones. A substring is valid when cnt1 >= cnt0 * cnt0. Since the total length of any substring is at most n, the number of ones can never exceed n. This means cnt0 * cnt0 <= n, so the number of zeros in any valid substring is at most sqrt(n). This is a powerful constraint: instead of dealing with arbitrarily many zeros, we only ever care about a small number of them (roughly sqrt(n)).
This observation suggests that we should enumerate by the number of zeros, rather than checking every substring one by one. If we fix the starting position i and gradually increase how many zeros the substring is allowed to contain, the number of distinct zero-counts we need to consider is tiny.
To make this enumeration fast, we need to know where the zeros are. Specifically, from any starting position, we want to quickly jump to the next zero. We precompute an array nxt, where nxt[i] tells us the position of the first 0 at or after index i (or n if there is none). With this, starting from position i, we can hop from one zero to the next, and each hop increases cnt0 by exactly one.
Now think about what happens when we fix the start i and a certain number of zeros cnt0. Suppose j marks the current zero boundary. Then nxt[j + 1] is the position of the next zero. Any right endpoint we choose between the current segment and that next zero keeps the zero count the same at cnt0. The number of ones available in this range is cnt1 = (nxt[j + 1] - i) - cnt0.
For the substring to be valid, we need cnt1 >= cnt0 * cnt0. The interesting part is figuring out how many valid right endpoints exist. As we extend the right endpoint, the number of ones grows. We want the smallest stretch that already satisfies cnt1 >= cnt0 * cnt0, and then count how far we can keep extending while staying inside the current run of ones (before hitting the next zero). This gives min(nxt[j + 1] - j, cnt1 - cnt0 * cnt0 + 1) valid endpoints:
cnt1 - cnt0 * cnt0 + 1represents how many extra ones we can include beyond the minimum required, each adding one more valid substring.nxt[j + 1] - jcaps this by the length of the current run, since going further would bring in another zero and changecnt0.
By accumulating this count for every start i and every feasible cnt0, and stopping as soon as cnt0 * cnt0 exceeds n, we efficiently count all dominant substrings without examining each one individually.
Solution Approach
We use preprocessing combined with enumeration to count all dominant substrings efficiently. Let n be the length of the string s.
Step 1: Precompute the next-zero positions.
We build an array nxt of size n + 1, where nxt[i] is the position of the first 0 at or after index i, or n if no zero exists. We fill it from right to left:
nxt = [n] * (n + 1)
for i in range(n - 1, -1, -1):
nxt[i] = nxt[i + 1]
if s[i] == "0":
nxt[i] = i
If s[i] is 0, then the nearest zero from position i is i itself. Otherwise, we inherit the value from nxt[i + 1]. This lets us jump from one zero to the next in constant time.
Step 2: Enumerate each starting position i.
For every starting index i, we treat it as the left endpoint of substrings. We initialize cnt0 to 1 if s[i] is 0, otherwise 0, accounting for whether the start itself is a zero:
cnt0 = int(s[i] == "0")
j = i
Step 3: Hop through zeros and count valid endpoints.
Using pointer j, we move from one zero to the next. The loop continues only while cnt0 * cnt0 <= n, because beyond that no substring can ever satisfy the condition:
while j < n and cnt0 * cnt0 <= n:
cnt1 = (nxt[j + 1] - i) - cnt0
if cnt1 >= cnt0 * cnt0:
ans += min(nxt[j + 1] - j, cnt1 - cnt0 * cnt0 + 1)
j = nxt[j + 1]
cnt0 += 1
Let us break down what happens inside the loop for a fixed i and current zero count cnt0:
nxt[j + 1]is the position of the next zero after the current boundary. The segment fromiup to just beforenxt[j + 1]contains exactlycnt0zeros.cnt1 = (nxt[j + 1] - i) - cnt0is the maximum number of ones available in this segment (total length minus the zeros).- If
cnt1 >= cnt0 * cnt0, the dominant condition can be met, so we count the valid right endpoints.
Step 4: Count valid right endpoints.
The number of valid choices for the right endpoint is:
min(nxt[j + 1] - j, cnt1 - cnt0 * cnt0 + 1)
cnt1 - cnt0 * cnt0 + 1is how many extra ones we can include beyond the minimum required countcnt0 * cnt0, where each extra one yields another valid substring.nxt[j + 1] - jlimits this to the length of the current run of ones, since extending past it would absorb the next zero and changecnt0.
We accumulate this count into ans. Then we advance j to the next zero (j = nxt[j + 1]) and increase cnt0 by one, repeating until the square of cnt0 exceeds n.
Complexity.
For each starting position i, the inner loop runs at most O(sqrt(n)) times, since cnt0 is bounded by sqrt(n). With n starting positions, the overall time complexity is O(n * sqrt(n)). The space complexity is O(n) for the nxt array.
Example Walkthrough
Let's trace through the algorithm with a small example string:
s = "0110" n = 4
We want to count all non-empty substrings where cnt1 >= cnt0 * cnt0.
Step 1: Precompute nxt (next-zero positions)
We fill nxt from right to left. nxt[i] = position of the first 0 at or after index i, else n.
index i | s[i] | reasoning | nxt[i] |
|---|---|---|---|
| 4 (sentinel) | — | base value | 4 |
| 3 | 0 | it's a zero → nxt[3] = 3 | 3 |
| 2 | 1 | inherit nxt[3] | 3 |
| 1 | 1 | inherit nxt[2] | 3 |
| 0 | 0 | it's a zero → nxt[0] = 0 | 0 |
So nxt = [0, 3, 3, 3, 4].
Step 2–4: Enumerate each start i
We keep a running total ans = 0. The loop runs while cnt0 * cnt0 <= n (i.e. cnt0 * cnt0 <= 4).
Start i = 0 (s[0] = '0' → cnt0 = 1, j = 0)
- Iteration A (
cnt0 = 1,j = 0):1*1 = 1 <= 4✓nxt[j+1] = nxt[1] = 3cnt1 = (3 - 0) - 1 = 2- Check
cnt1 >= cnt0² → 2 >= 1✓ - Endpoints:
min(nxt[1] - j, cnt1 - cnt0² + 1) = min(3 - 0, 2 - 1 + 1) = min(3, 2) = 2 - These correspond to substrings
"01"and"011". ans += 2 → ans = 2 - Advance:
j = nxt[1] = 3,cnt0 = 2
- Iteration B (
cnt0 = 2,j = 3):2*2 = 4 <= 4✓nxt[j+1] = nxt[4] = 4cnt1 = (4 - 0) - 2 = 2- Check
2 >= 4✗ → no substrings counted - Advance:
j = nxt[4] = 4,cnt0 = 3
- Iteration C (
cnt0 = 3):3*3 = 9 > 4→ loop stops.
Start i = 1 (s[1] = '1' → cnt0 = 0, j = 1)
- Iteration A (
cnt0 = 0,j = 1):0 <= 4✓nxt[j+1] = nxt[2] = 3cnt1 = (3 - 1) - 0 = 2- Check
2 >= 0✓ (a run of only ones always qualifies) - Endpoints:
min(nxt[2] - j, cnt1 - 0 + 1) = min(3 - 1, 2 + 1) = min(2, 3) = 2 - Substrings
"1"and"11". ans += 2 → ans = 4 - Advance:
j = nxt[2] = 3,cnt0 = 1
- Iteration B (
cnt0 = 1,j = 3):1 <= 4✓nxt[j+1] = nxt[4] = 4cnt1 = (4 - 1) - 1 = 2- Check
2 >= 1✓ - Endpoints:
min(4 - 3, 2 - 1 + 1) = min(1, 2) = 1 - Substring
"110". ans += 1 → ans = 5 - Advance:
j = nxt[4] = 4,cnt0 = 2
- Iteration C (
cnt0 = 2):4 <= 4✓ butj = 4not< n, so loop stops.
Start i = 2 (s[2] = '1' → cnt0 = 0, j = 2)
- Iteration A (
cnt0 = 0,j = 2):0 <= 4✓nxt[j+1] = nxt[3] = 3cnt1 = (3 - 2) - 0 = 1- Check
1 >= 0✓ - Endpoints:
min(3 - 2, 1 + 1) = min(1, 2) = 1 - Substring
"1". ans += 1 → ans = 6 - Advance:
j = nxt[3] = 3,cnt0 = 1
- Iteration B (
cnt0 = 1,j = 3):1 <= 4✓nxt[j+1] = nxt[4] = 4cnt1 = (4 - 2) - 1 = 1- Check
1 >= 1✓ - Endpoints:
min(4 - 3, 1 - 1 + 1) = min(1, 1) = 1 - Substring
"10". ans += 1 → ans = 7 - Advance:
j = 4,cnt0 = 2
- Loop stops (
j = 4not< n).
Start i = 3 (s[3] = '0' → cnt0 = 1, j = 3)
- Iteration A (
cnt0 = 1,j = 3):1 <= 4✓nxt[j+1] = nxt[4] = 4cnt1 = (4 - 3) - 1 = 0- Check
0 >= 1✗ → not counted (substring"0"has 0 ones, 1 zero) - Advance:
j = 4,cnt0 = 2
- Loop stops.
Final Result
ans = 7
The 7 valid substrings are:
| substring | indices | cnt1 | cnt0 | cnt1 >= cnt0² |
|---|---|---|---|---|
"01" | [0,1] | 1 | 1 | 1 >= 1 ✓ |
"011" | [0,2] | 2 | 1 | 2 >= 1 ✓ |
"1" | [1,1] | 1 | 0 | 1 >= 0 ✓ |
"11" | [1,2] | 2 | 0 | 2 >= 0 ✓ |
"110" | [1,3] | 2 | 1 | 2 >= 1 ✓ |
"1" | [2,2] | 1 | 0 | 1 >= 0 ✓ |
"10" | [2,3] | 1 | 1 | 1 >= 1 ✓ |
This confirms that the enumeration—hopping zero-by-zero from each start and counting valid right endpoints in bulk—correctly produces 7 without ever inspecting every substring individually.
Solution Implementation
1class Solution:
2 def numberOfSubstrings(self, s: str) -> int:
3 n = len(s)
4
5 # next_zero[i] stores the index of the next '0' at or after position i.
6 # If there is no '0' from i onward, it stays at n (sentinel).
7 next_zero = [n] * (n + 1)
8 for i in range(n - 1, -1, -1):
9 next_zero[i] = next_zero[i + 1]
10 if s[i] == "0":
11 next_zero[i] = i
12
13 answer = 0
14
15 # Fix the left boundary i of the substring.
16 for i in range(n):
17 # zero_count is the number of '0's currently included in the window
18 # that starts at i. We begin by counting whether s[i] itself is '0'.
19 zero_count = int(s[i] == "0")
20 j = i
21
22 # We only need to continue while zero_count^2 can still be satisfied
23 # by at most n ones; once zero_count^2 > n it is impossible.
24 while j < n and zero_count * zero_count <= n:
25 # next_zero[j + 1] is the position of the next '0' after j.
26 # The segment from i up to (but not including) next_zero[j + 1]
27 # contains exactly zero_count zeros; the remaining characters
28 # in that span are ones.
29 one_count = (next_zero[j + 1] - i) - zero_count
30
31 # We need ones >= zeros^2 for the substring to be valid.
32 if one_count >= zero_count * zero_count:
33 # Two upper limits on how many valid right endpoints we can take:
34 # 1) next_zero[j + 1] - j : positions before hitting the next '0',
35 # which would otherwise increase zero_count.
36 # 2) one_count - zero_count^2 + 1 : how many extra ones we can
37 # still append while keeping ones >= zeros^2.
38 answer += min(next_zero[j + 1] - j, one_count - zero_count * zero_count + 1)
39
40 # Jump j to the next '0' position, since the next iteration
41 # accounts for including one more '0' in the window.
42 j = next_zero[j + 1]
43 zero_count += 1
44
45 return answer
461class Solution {
2 public int numberOfSubstrings(String s) {
3 int length = s.length();
4
5 // nextZero[i] holds the index of the first '0' at or after position i.
6 // If there is no '0' from i onward, it equals length.
7 int[] nextZero = new int[length + 1];
8 nextZero[length] = length;
9 for (int i = length - 1; i >= 0; --i) {
10 nextZero[i] = nextZero[i + 1];
11 if (s.charAt(i) == '0') {
12 nextZero[i] = i;
13 }
14 }
15
16 int answer = 0;
17
18 // Fix the left boundary 'start' of the substring.
19 for (int start = 0; start < length; ++start) {
20 // zeroCount: number of '0's currently considered in the prefix.
21 int zeroCount = s.charAt(start) == '0' ? 1 : 0;
22 int current = start;
23
24 // Iterate while the required number of '1's (zeroCount^2)
25 // does not exceed the array size (an upper bound for feasibility).
26 while (current < length && 1L * zeroCount * zeroCount <= length) {
27 // oneCount: number of '1's in the window [start, nextZero[current + 1]).
28 // (nextZero[current + 1] - start) is the total length of that window,
29 // and subtracting zeroCount leaves the count of '1's.
30 int oneCount = nextZero[current + 1] - start - zeroCount;
31
32 // The substring is valid only when oneCount >= zeroCount^2.
33 if (oneCount >= zeroCount * zeroCount) {
34 // Number of valid right endpoints in this segment:
35 // - (nextZero[current + 1] - current): the count of trailing
36 // '1's available before the next '0',
37 // - (oneCount - zeroCount^2 + 1): the count of right endpoints
38 // that still satisfy the ones-count constraint.
39 // We take the smaller of the two so we don't overcount.
40 answer += Math.min(nextZero[current + 1] - current,
41 oneCount - zeroCount * zeroCount + 1);
42 }
43
44 // Advance to the next '0' position and include it in the count.
45 current = nextZero[current + 1];
46 ++zeroCount;
47 }
48 }
49
50 return answer;
51 }
52}
531class Solution {
2public:
3 int numberOfSubstrings(string s) {
4 int n = s.size();
5
6 // next_zero[i] stores the index of the first '0' at or after position i.
7 // If no '0' exists from i onward, it stores n.
8 vector<int> next_zero(n + 1);
9 next_zero[n] = n;
10 for (int i = n - 1; i >= 0; --i) {
11 next_zero[i] = next_zero[i + 1];
12 if (s[i] == '0') {
13 next_zero[i] = i;
14 }
15 }
16
17 int ans = 0;
18
19 // Enumerate each starting index i of the substring.
20 for (int i = 0; i < n; ++i) {
21 // zero_count: number of '0's currently considered in the substring
22 // starting at i. Initialize based on whether s[i] is '0'.
23 int zero_count = (s[i] == '0') ? 1 : 0;
24
25 // j marks the position of the last '0' included (we iterate '0' by '0').
26 int j = i;
27
28 // We only need to consider substrings where zero_count^2 <= n,
29 // since the count of '1's can never exceed n.
30 while (j < n && 1LL * zero_count * zero_count <= n) {
31 // one_count: number of '1's between i and the next '0'
32 // (next_zero[j + 1]), excluding the zeros already counted.
33 int one_count = next_zero[j + 1] - i - zero_count;
34
35 // A valid substring requires one_count >= zero_count^2.
36 if (one_count >= zero_count * zero_count) {
37 // We can extend the right end through the run of '1's that
38 // follow position j. The right boundary can move up to
39 // next_zero[j + 1] (the next '0'), but is also limited by
40 // how many surplus '1's we have beyond zero_count^2.
41 ans += min(next_zero[j + 1] - j,
42 one_count - zero_count * zero_count + 1);
43 }
44
45 // Move j to the next '0' and increase the zero count by one,
46 // preparing to evaluate substrings containing one more '0'.
47 j = next_zero[j + 1];
48 ++zero_count;
49 }
50 }
51
52 return ans;
53 }
54};
551/**
2 * Counts the number of substrings where the count of '1's is greater than or
3 * equal to the square of the count of '0's.
4 *
5 * @param s - The binary input string.
6 * @returns The total number of qualifying substrings.
7 */
8function numberOfSubstrings(s: string): number {
9 const length = s.length;
10
11 // nextZero[i] holds the index of the next '0' at or after position i.
12 // If no '0' exists from i onward, it equals `length`.
13 const nextZero: number[] = Array(length + 1).fill(0);
14 nextZero[length] = length;
15 for (let i = length - 1; i >= 0; --i) {
16 nextZero[i] = nextZero[i + 1];
17 if (s[i] === '0') {
18 nextZero[i] = i;
19 }
20 }
21
22 let answer = 0;
23
24 // Enumerate each possible starting index of the substring.
25 for (let start = 0; start < length; ++start) {
26 // zeroCount tracks how many '0's are currently in the prefix [start..end].
27 let zeroCount = s[start] === '0' ? 1 : 0;
28 let end = start;
29
30 // We only continue while zeroCount^2 can still be satisfied by enough '1's.
31 // Once zeroCount^2 exceeds the total length, no valid substring can exist.
32 while (end < length && zeroCount * zeroCount <= length) {
33 // Number of '1's in the range up to the position just before the next '0'.
34 // nextZero[end + 1] is the index of the next '0' after `end`.
35 // Subtracting `start` gives the span length, and removing zeroCount
36 // leaves the count of '1's.
37 const oneCount = nextZero[end + 1] - start - zeroCount;
38
39 // The required threshold of '1's is zeroCount^2.
40 if (oneCount >= zeroCount * zeroCount) {
41 // For this fixed zeroCount, the substring's right boundary can extend
42 // across the block of consecutive '1's. Two limits apply:
43 // 1) nextZero[end + 1] - end: how far we can extend before hitting
44 // the next '0' (which would increase zeroCount).
45 // 2) oneCount - zeroCount^2 + 1: how many extra '1's we can include
46 // while still keeping ones >= zeroCount^2.
47 // The valid count is the minimum of these two constraints.
48 answer += Math.min(nextZero[end + 1] - end, oneCount - zeroCount * zeroCount + 1);
49 }
50
51 // Jump directly to the next '0' position and account for it.
52 end = nextZero[end + 1];
53 ++zeroCount;
54 }
55 }
56
57 return answer;
58}
59Time and Space Complexity
Time Complexity
The time complexity is O(n × √n), where n is the length of the string s.
-
The preprocessing loop that builds the
nxtarray runs inO(n)time, wherenxt[i]records the position of the next"0"at or after indexi. -
The main computation lies in the double loop. The outer loop iterates over each starting index
ifrom0ton - 1, contributingO(n). -
For each starting index
i, the innerwhileloop jumps from one"0"to the next viaj = nxt[j + 1], while incrementingcnt0(the count of zeros) by1each iteration. The loop conditioncnt0 * cnt0 <= nensures that the number of zeros considered satisfiescnt0 ≤ √n. Therefore, the inner loop executes at mostO(√n)times for eachi.This bound comes from the requirement that a valid substring needs
cnt1 ≥ cnt0 * cnt0, so oncecnt0exceeds√n, even the entire remaining string cannot supply enough1s, and the loop terminates. -
Combining the outer loop with the inner loop gives
O(n) × O(√n) = O(n × √n).
Hence, the overall time complexity is O(n × √n).
Space Complexity
The space complexity is O(n). The only auxiliary data structure is the nxt array of size n + 1, which requires O(n) space. All other variables (ans, cnt0, cnt1, i, j) use constant O(1) space.
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Off-by-one error when counting valid right endpoints
The most error-prone part of this solution is computing how many valid substrings end inside the current run of ones. A naive attempt might simply count all the ones available (one_count) as valid endpoints, or forget that there are two competing upper bounds that must both be respected.
Why it goes wrong:
There are two independent constraints on how far the right endpoint can extend:
-
The dominance constraint: Once we have fixed
zero_countzeros, we needone_count >= zero_count². The number of extra ones we can include beyond the minimum requirement isone_count - zero_count² + 1(the+1accounts for the endpoint that exactly meetsone_count == zero_count²). -
The structural constraint: We must stop before reaching the next
0, because including it would bumpzero_countup by one and change the whole calculation. This gives at mostnext_zero[j + 1] - jpositions.
If you take only the first bound, you may overcount substrings that actually swallow the next zero. If you take only the second, you may overcount substrings that don't have enough ones. The correct answer is the minimum of the two:
answer += min(next_zero[j + 1] - j, one_count - zero_count * zero_count + 1)
A common mistake is also forgetting the +1, which would undercount by exactly one valid substring per qualifying segment.
Pitfall 2: Using a per-substring O(n) recount instead of the jump trick
A tempting but inefficient approach is to fix both endpoints and recount zeros/ones from scratch for every (i, j) pair, leading to O(n³) or, with prefix sums, O(n²) time. For large n this times out.
Solution: Exploit the fact that any valid substring can contain at most √n zeros (since cnt1 <= n forces cnt0² <= n). By precomputing next_zero and hopping from one zero to the next, the inner loop runs only O(√n) times per start index, giving an overall O(n·√n) solution.
while j < n and zero_count * zero_count <= n: ... j = next_zero[j + 1] # jump straight to the next zero zero_count += 1
The early-termination guard zero_count * zero_count <= n is essential—without it, the loop keeps iterating even when no further valid substring can ever exist, wasting work (and, if next_zero were misindexed, risking an infinite loop).
Pitfall 3: Mishandling the all-ones case (cnt0 = 0)
When a substring contains no zeros, the condition cnt1 >= 0 is trivially satisfied, so every such substring qualifies. This case is handled implicitly on the first iteration of the inner loop when zero_count starts at 0 (i.e., s[i] == '1').
On that iteration:
one_count = next_zero[j + 1] - i(the full run of ones up to the next zero),zero_count² = 0, so the bound becomesmin(next_zero[j+1] - i, one_count + 1).
Why it's subtle: If you initialize zero_count incorrectly (for example, always starting at 1, or skipping the zero-zero case entirely), you will silently drop all the all-ones substrings, producing a wrong answer that's hard to spot because the code still runs and returns a plausible-looking number. Always initialize:
zero_count = int(s[i] == "0")
Pitfall 4: Indexing next_zero out of bounds
The next_zero array must have size n + 1 (not n) so that next_zero[j + 1] is always safe to access, including when j == n - 1. The extra sentinel slot next_zero[n] = n guarantees that once we pass the last character, all "next zero" lookups correctly return n, signaling "no more zeros."
Solution: Allocate one extra slot and rely on the sentinel:
next_zero = [n] * (n + 1) # size n + 1, sentinel at index n
Forgetting the extra slot triggers an IndexError exactly on the longest substrings—an edge case easily missed in small test runs.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapConsider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity describes how the time needed
Want a Structured Path to Master System Design Too? Don’t Miss This!