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".

LeetCode Problem 3234

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 zeros be the number of '0' characters
  • Let ones be 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, because zeros = 0 and therefore ones >= 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 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.