LeetCode 3261 - Count Substrings That Satisfy K-Constraint II
We are given a binary string s, an integer k, and many range queries. For each query [l, r], we only consider the substring s[l..r]. Among all substrings completely contained inside this range, we must count how many satisfy the k-constraint.
Difficulty: 🔴 Hard
Topics: Array, String, Binary Search, Sliding Window, Prefix Sum
Solution
LeetCode 3261 - Count Substrings That Satisfy K-Constraint II
Problem Understanding
We are given a binary string s, an integer k, and many range queries.
For each query [l, r], we only consider the substring s[l..r]. Among all substrings completely contained inside this range, we must count how many satisfy the k-constraint.
A substring satisfies the constraint if at least one of the following is true:
- The number of
0s is at mostk. - The number of
1s is at mostk.
Notice that a substring becomes invalid only when both counts exceed k.
For example, if k = 2, then:
"00011"is valid because it contains only two1s."001111"is valid because it contains only two0s."000111"is invalid because it contains three0s and three1s.
The input size is large:
|s| ≤ 100,000|queries| ≤ 100,000
A naive solution that examines every substring for every query is completely infeasible. Even a single query can contain roughly O(n²) substrings.
The challenge is therefore to preprocess the string so that each query can be answered efficiently.
Important edge cases include:
kmay be as large asn, making every substring valid.- Query ranges may contain only one character.
- The string may consist entirely of zeros or entirely of ones.
- Many queries may overlap heavily.
- A substring becomes invalid only when both character counts exceed
k, which is easy to misinterpret.
Approaches
Brute Force
For a query [l, r], enumerate every substring inside that interval.
For each substring, count zeros and ones and check whether the k-constraint holds.
This is correct because every candidate substring is explicitly examined.
However, a range of length m contains m(m+1)/2 substrings. With up to 100,000 queries, this approach is far too slow.
Even after using prefix sums to compute character counts in O(1), we still need to inspect all substrings.
Key Observation
A substring is invalid only if:
- zeros > k
- ones > k
Using a sliding window, we can determine for every starting position i the furthest ending position that still produces a valid substring.
Let:
rightBound[i]= first position where validity breaks- equivalently, every ending position
< rightBound[i]forms a valid substring starting ati
This transforms the problem into a geometric counting problem.
For a query [l, r], we need to count pairs:
(i, j)
l ≤ i ≤ j ≤ r
j < rightBound[i]
If we define:
limit[i] = min(rightBound[i] - 1, r)
then the contribution of start index i is:
limit[i] - i + 1
The difficulty is answering this efficiently for many queries.
The crucial insight is that:
rightBound[i]is nondecreasing.- We can preprocess prefix sums of contributions.
- For each query, binary search separates indices whose valid region fully covers
rfrom those clipped byr.
This yields an O((n + q) log n) solution.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(q · n²) | O(1) | Enumerates every substring for every query |
| Optimal | O((n + q) log n) | O(n) | Sliding window, prefix sums, binary search |
Algorithm Walkthrough
Step 1: Compute the first invalid ending for every start position
Use a sliding window.
Maintain:
- left pointer
- right pointer
- count of zeros
- count of ones
Expand the window to the right.
Whenever both counts exceed k, the window becomes invalid. At that moment:
- record
rightBound[left] = right - move
leftforward until validity is restored
After processing the entire string, any remaining start positions never encountered an invalid window, so:
rightBound[i] = n
for those indices.
Thus every substring:
[i, j]
is valid exactly when:
j < rightBound[i]
Step 2: Build contribution values
For each index:
fullCount[i] = rightBound[i] - i
This equals the number of valid substrings starting at i in the entire string.
Build prefix sums:
prefixFull
so ranges of fullCount can be summed quickly.
Step 3: Build another prefix sum of rightBound
Create:
prefixRight
where:
prefixRight[i+1] = prefixRight[i] + rightBound[i]
This will be useful when computing clipped contributions.
Step 4: Process a query
Suppose the query is:
[l, r]
We need:
Σ(min(rightBound[i]-1, r) - i + 1)
for all:
i ∈ [l, r]
Rewrite:
Σ(min(rightBound[i], r+1) - i)
Because rightBound is nondecreasing, we can binary search the first index:
p
such that:
rightBound[p] > r
Then:
- indices
[l, p-1]haverightBound[i] ≤ r - indices
[p, r]haverightBound[i] > r
For the first group:
contribution = rightBound[i] - i
For the second group:
contribution = (r + 1) - i
Both sums can be computed using prefix sums.
Step 5: Compute the first part
For:
[l, p-1]
sum:
rightBound[i] - i
using:
- prefixRight
- prefix index sums
Step 6: Compute the second part
For:
[p, r]
sum:
(r + 1) - i
which becomes:
count * (r + 1) - Σi
Again, prefix sums give this in O(1).
Step 7: Add both parts
The sum of the two sections is the answer for the query.
Why it works
For every start position i, the sliding window computes the exact first ending position where the substring becomes invalid. Therefore all endings before rightBound[i] are valid and all endings at or after it are invalid.
For a query range [l, r], every valid substring corresponds uniquely to a pair (i, j) satisfying:
l ≤ i ≤ j ≤ r
j < rightBound[i]
The binary search partitions starts into those whose valid region ends before r and those whose valid region extends beyond r. The contribution formula counts exactly the number of valid endings for each start position, so summing these contributions yields the precise number of valid substrings.
Python Solution
from typing import List
from bisect import bisect_right
class Solution:
def countKConstraintSubstrings(self, s: str, k: int, queries: List[List[int]]) -> List[int]:
n = len(s)
right_bound = [n] * n
left = 0
zeros = 0
ones = 0
for right, ch in enumerate(s):
if ch == '0':
zeros += 1
else:
ones += 1
while zeros > k and ones > k:
right_bound[left] = right
if s[left] == '0':
zeros -= 1
else:
ones -= 1
left += 1
prefix_right = [0] * (n + 1)
prefix_idx = [0] * (n + 1)
for i in range(n):
prefix_right[i + 1] = prefix_right[i] + right_bound[i]
prefix_idx[i + 1] = prefix_idx[i] + i
answers = []
for l, r in queries:
p = bisect_right(right_bound, r, l, r + 1)
part1 = (
prefix_right[p] - prefix_right[l]
- (prefix_idx[p] - prefix_idx[l])
)
cnt = r - p + 1
part2 = (
cnt * (r + 1)
- (prefix_idx[r + 1] - prefix_idx[p])
)
answers.append(part1 + part2)
return answers
Implementation Explanation
The sliding window computes right_bound[i], the first ending position that makes a substring starting at i invalid.
Because the left pointer only moves forward, the resulting right_bound array is naturally nondecreasing. This property enables binary search during query processing.
Two prefix arrays are constructed:
prefix_rightstores cumulative sums ofright_bound.prefix_idxstores cumulative sums of indices.
For each query, bisect_right finds the boundary between positions whose valid region ends before or at r and those whose valid region extends beyond r.
The first group contributes right_bound[i] - i.
The second group contributes (r + 1) - i.
Both sums are obtained from the prefix arrays in constant time after the binary search.
Go Solution
package main
import "sort"
func countKConstraintSubstrings(s string, k int, queries [][]int) []int64 {
n := len(s)
rightBound := make([]int, n)
for i := range rightBound {
rightBound[i] = n
}
left := 0
zeros := 0
ones := 0
for right := 0; right < n; right++ {
if s[right] == '0' {
zeros++
} else {
ones++
}
for zeros > k && ones > k {
rightBound[left] = right
if s[left] == '0' {
zeros--
} else {
ones--
}
left++
}
}
prefixRight := make([]int64, n+1)
prefixIdx := make([]int64, n+1)
for i := 0; i < n; i++ {
prefixRight[i+1] = prefixRight[i] + int64(rightBound[i])
prefixIdx[i+1] = prefixIdx[i] + int64(i)
}
ans := make([]int64, len(queries))
for qi, q := range queries {
l, r := q[0], q[1]
p := sort.Search(r-l+1, func(x int) bool {
return rightBound[l+x] > r
}) + l
part1 := (prefixRight[p] - prefixRight[l]) -
(prefixIdx[p] - prefixIdx[l])
cnt := int64(r - p + 1)
part2 := cnt*int64(r+1) -
(prefixIdx[r+1] - prefixIdx[p])
ans[qi] = part1 + part2
}
return ans
}
Go-Specific Notes
The answer can be as large as approximately n²/2, which exceeds the range of a 32-bit integer. Therefore all prefix sums and final answers use int64.
Go does not provide a direct equivalent of Python's bisect_right with bounds, so sort.Search is used to locate the first index whose rightBound value exceeds r.
Worked Examples
Example 1
Input:
s = "0001111"
k = 2
queries = [[0,6]]
Sliding Window Result
| i | rightBound[i] |
|---|---|
| 0 | 5 |
| 1 | 6 |
| 2 | 7 |
| 3 | 7 |
| 4 | 7 |
| 5 | 7 |
| 6 | 7 |
Valid counts starting at each position:
| i | rightBound[i] - i |
|---|---|
| 0 | 5 |
| 1 | 5 |
| 2 | 5 |
| 3 | 4 |
| 4 | 3 |
| 5 | 2 |
| 6 | 1 |
Total:
5 + 5 + 5 + 4 + 3 + 2 + 1 = 25
For query [0,6]:
p = first index with rightBound > 6 = 2
First section:
(5-0) + (6-1) = 10
Second section:
(7-2) + (7-3) + (7-4) + (7-5) + (7-6)
= 5 + 4 + 3 + 2 + 1
= 15
Answer:
10 + 15 = 25
Example 2
Input:
s = "010101"
k = 1
Computed array:
| i | rightBound[i] |
|---|---|
| 0 | 3 |
| 1 | 4 |
| 2 | 5 |
| 3 | 6 |
| 4 | 6 |
| 5 | 6 |
Query [0,5]
Binary search:
p = 3
First part:
(3-0)+(4-1)+(5-2)=9
Second part:
(6-3)+(6-4)+(6-5)=6
Answer:
15
Query [1,4]
Every valid substring has length at most 3.
Counting contributions:
3 + 3 + 2 + 1 = 9
Answer:
9
Query [2,3]
Substrings:
"0"
"1"
"01"
All valid.
Answer:
3
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + q log n) | Sliding window preprocessing is linear, each query performs one binary search |
| Space | O(n) | Stores rightBound and prefix sums |
The sliding window advances each pointer at most n times, giving O(n) preprocessing. Each query requires only a binary search on the monotonic rightBound array plus constant-time prefix-sum calculations, resulting in O(log n) per query.
Test Cases
from typing import List
sol = Solution()
assert sol.countKConstraintSubstrings(
"0001111", 2, [[0, 6]]
) == [25] # example 1
assert sol.countKConstraintSubstrings(
"010101", 1, [[0, 5], [1, 4], [2, 3]]
) == [15, 9, 3] # example 2
assert sol.countKConstraintSubstrings(
"0", 1, [[0, 0]]
) == [1] # single character
assert sol.countKConstraintSubstrings(
"11111", 1, [[0, 4]]
) == [15] # all substrings valid
assert sol.countKConstraintSubstrings(
"00000", 2, [[1, 3]]
) == [6] # all substrings inside subrange valid
assert sol.countKConstraintSubstrings(
"01", 1, [[0, 1]]
) == [3] # smallest mixed string
assert sol.countKConstraintSubstrings(
"0011", 1, [[0, 3]]
) == [9] # some invalid substrings appear
assert sol.countKConstraintSubstrings(
"01010", 5, [[0, 4]]
) == [15] # k larger than counts
assert sol.countKConstraintSubstrings(
"0101010101", 1, [[0, 9], [3, 7]]
) == [27, 12] # larger alternating pattern
assert sol.countKConstraintSubstrings(
"101010", 1, [[0, 0], [5, 5]]
) == [1, 1] # boundary queries
Test Summary
| Test | Why |
|---|---|
"0001111" |
Validates sample behavior with only two invalid substrings |
"010101" |
Validates alternating pattern and multiple queries |
"0" |
Minimum possible string |
"11111" |
Single-character type, every substring valid |
"00000" subrange |
Query entirely inside larger string |
"01" |
Smallest mixed binary string |
"0011" |
Contains invalid longer substrings |
Large k |
Every substring should be valid |
| Long alternating string | Stress test for binary search partition |
| Single-element queries | Validates range boundaries |
Edge Cases
Large k Makes Everything Valid
If k is at least the total number of zeros or ones in every substring, the constraint is always satisfied. In this situation the sliding window never encounters an invalid state, so every rightBound[i] remains n. The query formula naturally reduces to counting all substrings inside the range.
Single Character Query Ranges
A query such as [i, i] contains exactly one substring. Off-by-one mistakes commonly appear here. The implementation handles this correctly because the contribution formula becomes exactly one valid ending position.
Strings Containing Only One Character Type
For strings like "000000" or "111111", one character count is always zero. Since zero is always at most k, every substring is valid. The sliding window never shrinks, rightBound[i] remains n, and all substrings are counted correctly.
Boundary Where Validity First Breaks
The most subtle case occurs when a window transitions from valid to invalid because both counts become greater than k. The algorithm stores the first invalid position in rightBound[i], not the last valid position. This convention makes the validity condition simply:
j < rightBound[i]
which avoids off-by-one errors and enables the clean prefix-sum formulation used during query processing.