Facebook Pixel

3234. Count the Number of Substrings With Dominant Ones

MediumStringEnumeration
LeetCode ↗

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 of 0s.
  • Notice that if a substring contains zero 0s (cnt0 = 0), then the condition becomes cnt1 >= 0, which is always true. So substrings made up entirely of 1s always qualify.
  • Because the count of ones cannot exceed the total length n, the number of zeros cnt0 in any valid substring is bounded by sqrt(n). This observation keeps the number of distinct zero-counts small and is the basis for an efficient enumeration.
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Sliding Window?

This problem maps to Sliding Window through a short path in the full flowchart.

Subarraysorsubstrings?yesSlidingwindowwith twoyesSliding Window

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 Flowchart

Intuition

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 + 1 represents how many extra ones we can include beyond the minimum required, each adding one more valid substring.
  • nxt[j + 1] - j caps this by the length of the current run, since going further would bring in another zero and change cnt0.

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 from i up to just before nxt[j + 1] contains exactly cnt0 zeros.
  • cnt1 = (nxt[j + 1] - i) - cnt0 is 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 + 1 is how many extra ones we can include beyond the minimum required count cnt0 * cnt0, where each extra one yields another valid substring.
  • nxt[j + 1] - j limits this to the length of the current run of ones, since extending past it would absorb the next zero and change cnt0.

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 is[i]reasoningnxt[i]
4 (sentinel)base value4
30it's a zero → nxt[3] = 33
21inherit nxt[3]3
11inherit nxt[2]3
00it's a zero → nxt[0] = 00

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] = 3
    • cnt1 = (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] = 4
    • cnt1 = (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] = 3
    • cnt1 = (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] = 4
    • cnt1 = (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 ✓ but j = 4 not < 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] = 3
    • cnt1 = (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] = 4
    • cnt1 = (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 = 4 not < n).

Start i = 3 (s[3] = '0'cnt0 = 1, j = 3)

  • Iteration A (cnt0 = 1, j = 3): 1 <= 4
    • nxt[j+1] = nxt[4] = 4
    • cnt1 = (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:

substringindicescnt1cnt0cnt1 >= cnt0²
"01"[0,1]111 >= 1 ✓
"011"[0,2]212 >= 1 ✓
"1"[1,1]101 >= 0 ✓
"11"[1,2]202 >= 0 ✓
"110"[1,3]212 >= 1 ✓
"1"[2,2]101 >= 0 ✓
"10"[2,3]111 >= 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
46
1class 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}
53
1class 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};
55
1/**
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}
59

Time 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 nxt array runs in O(n) time, where nxt[i] records the position of the next "0" at or after index i.

  • The main computation lies in the double loop. The outer loop iterates over each starting index i from 0 to n - 1, contributing O(n).

  • For each starting index i, the inner while loop jumps from one "0" to the next via j = nxt[j + 1], while incrementing cnt0 (the count of zeros) by 1 each iteration. The loop condition cnt0 * cnt0 <= n ensures that the number of zeros considered satisfies cnt0 ≤ √n. Therefore, the inner loop executes at most O(√n) times for each i.

    This bound comes from the requirement that a valid substring needs cnt1 ≥ cnt0 * cnt0, so once cnt0 exceeds √n, even the entire remaining string cannot supply enough 1s, 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:

  1. The dominance constraint: Once we have fixed zero_count zeros, we need one_count >= zero_count². The number of extra ones we can include beyond the minimum requirement is one_count - zero_count² + 1 (the +1 accounts for the endpoint that exactly meets one_count == zero_count²).

  2. The structural constraint: We must stop before reaching the next 0, because including it would bump zero_count up by one and change the whole calculation. This gives at most next_zero[j + 1] - j positions.

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 becomes min(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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More