3261. Count Substrings That Satisfy K-Constraint II
Problem Description
You are given a binary string s
consisting of only '0's and '1's, and an integer k
.
A binary string satisfies the k-constraint if at least one of these conditions is true:
- The string contains at most
k
zeros (0's) - The string contains at most
k
ones (1's)
You are also given a 2D integer array queries
, where each queries[i] = [li, ri]
represents a query asking about a specific substring s[li..ri]
(inclusive).
For each query, you need to count how many substrings within s[li..ri]
satisfy the k-constraint.
Your task is to return an integer array answer
, where answer[i]
is the number of substrings of s[li..ri]
that satisfy the k-constraint.
Example:
If s = "0011"
, k = 1
, and queries = [[0, 3]]
:
- We need to find all substrings of
s[0..3]
= "0011" that satisfy the k-constraint - Valid substrings include: "0" (at index 0), "0" (at index 1), "1" (at index 2), "1" (at index 3), "00", "01", "11"
- Each of these has either at most 1 zero or at most 1 one
- Invalid substrings: "001", "011", "0011" (these have both more than 1 zero AND more than 1 one)
Intuition
The key insight is that we need to efficiently count valid substrings for multiple queries over different ranges. A naive approach of checking every substring for every query would be too slow.
Let's think about what makes a substring valid: it needs to have either at most k
zeros OR at most k
ones. This means a substring becomes invalid only when it has BOTH more than k
zeros AND more than k
ones.
For any starting position i
, there exists a critical position where substrings starting at i
transition from valid to invalid as we extend to the right. Once we hit a position j
where the substring s[i..j]
has more than k
zeros AND more than k
ones, all longer substrings starting at i
will also be invalid.
This observation leads us to use a sliding window approach. As we expand the window to the right, we track when the window becomes invalid (having more than k
of both 0s and 1s). When this happens, we need to shrink from the left until it's valid again.
For each starting position i
, we can precompute the first position d[i]
where the substring becomes invalid. This means all substrings s[i..j]
where j < d[i]
are valid.
To handle queries efficiently, we also need to precompute the count of valid substrings ending at each position using a prefix sum array. For position j
, the number of valid substrings ending at j
equals the number of valid starting positions, which is j - i + 1
where i
is the leftmost valid starting position.
When answering a query [l, r]
, we split it into two parts:
- Substrings starting at
l
that remain valid throughout (froml
tomin(r+1, d[l])-1
) - Substrings that start after
l
and end within the query range
The first part forms a continuous range where all substrings are valid, giving us (1 + p - l) * (p - l) / 2
substrings. The second part can be calculated using our prefix sum array as pre[r+1] - pre[p]
.
Learn more about Binary Search, Prefix Sum and Sliding Window patterns.
Solution Approach
The solution uses a sliding window technique combined with prefix sums to efficiently handle multiple queries.
Step 1: Initialize Data Structures
cnt = [0, 0]
: Track count of 0s and 1s in current windowi = 0
: Left boundary of sliding windowd = [n] * n
: Array whered[i]
stores the first position to the right ofi
that doesn't satisfy k-constraint (initialized ton
)pre = [0] * (n + 1)
: Prefix sum array wherepre[j+1]
stores total count of valid substrings ending at or before positionj
Step 2: Build the Sliding Window and Precompute Values
For each position j
from 0 to n-1:
- Add character at position
j
to the window:cnt[int(s[j])] += 1
- Check if window violates k-constraint:
cnt[0] > k and cnt[1] > k
- If violated, shrink window from left:
- Record
d[i] = j
(positionj
is first invalid position for starting indexi
) - Remove leftmost character:
cnt[int(s[i])] -= 1
- Move left boundary:
i += 1
- Repeat until window satisfies k-constraint
- Record
- Count valid substrings ending at
j
:j - i + 1
(all starting positions fromi
toj
form valid substrings) - Update prefix sum:
pre[j + 1] = pre[j] + j - i + 1
Step 3: Answer Queries
For each query [l, r]
:
-
Find the boundary:
p = min(r + 1, d[l])
p
is the first position where substrings starting atl
become invalid- If
d[l] > r + 1
, all substrings in range are valid
-
Calculate valid substrings in two parts:
- Part A: Substrings starting at
l
and ending beforep
- These form all possible substrings from position
l
top-1
- Count:
a = (1 + p - l) * (p - l) // 2
- This is the formula for sum of integers from 1 to
(p - l)
- These form all possible substrings from position
- Part B: Valid substrings with right boundary in range
[p, r]
- Use prefix sum difference:
b = pre[r + 1] - pre[p]
- This gives count of all valid substrings ending between positions
p
andr
- Use prefix sum difference:
- Part A: Substrings starting at
-
Total count for query:
a + b
Time Complexity: O(n + q)
where n
is string length and q
is number of queries
- Preprocessing takes
O(n)
with single pass - Each query answered in
O(1)
using precomputed values
Space Complexity: O(n)
for storing arrays d
and pre
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with s = "0011"
, k = 1
, and queries = [[0, 3]]
.
Step 1: Initialize
n = 4
,cnt = [0, 0]
(tracks count of 0s and 1s)i = 0
(left boundary of sliding window)d = [4, 4, 4, 4]
(initially all set to n=4)pre = [0, 0, 0, 0, 0]
(prefix sum array with n+1 elements)
Step 2: Build Sliding Window
Process j = 0 (s[0] = '0'):
- Add '0' to window:
cnt = [1, 0]
- Check constraint:
cnt[0] = 1 ≤ k
andcnt[1] = 0 ≤ k
✓ (valid) - Valid substrings ending at 0:
0 - 0 + 1 = 1
(substring "0") - Update:
pre[1] = pre[0] + 1 = 1
Process j = 1 (s[1] = '0'):
- Add '0' to window:
cnt = [2, 0]
- Check constraint:
cnt[0] = 2 > k
butcnt[1] = 0 ≤ k
✓ (still valid, needs only ONE condition) - Valid substrings ending at 1:
1 - 0 + 1 = 2
(substrings "0", "00") - Update:
pre[2] = pre[1] + 2 = 3
Process j = 2 (s[2] = '1'):
- Add '1' to window:
cnt = [2, 1]
- Check constraint:
cnt[0] = 2 > k
andcnt[1] = 1 ≤ k
✓ (still valid) - Valid substrings ending at 2:
2 - 0 + 1 = 3
(substrings "1", "01", "001") - Update:
pre[3] = pre[2] + 3 = 6
Process j = 3 (s[3] = '1'):
- Add '1' to window:
cnt = [2, 2]
- Check constraint:
cnt[0] = 2 > k
ANDcnt[1] = 2 > k
✗ (INVALID!) - Window violates k-constraint, must shrink:
- Record
d[0] = 3
(position 3 is first invalid for starting index 0) - Remove s[0] = '0':
cnt = [1, 2]
,i = 1
- Still invalid:
cnt[0] = 1 ≤ k
butcnt[1] = 2 > k
✓ (now valid!)
- Record
- Valid substrings ending at 3:
3 - 1 + 1 = 3
(substrings "1", "11", "011") - Update:
pre[4] = pre[3] + 3 = 9
Final arrays:
d = [3, 4, 4, 4]
(d[0]=3 means substrings starting at 0 become invalid at position 3)pre = [0, 1, 3, 6, 9]
Step 3: Answer Query [0, 3]
For query [l=0, r=3]
:
-
Find boundary:
p = min(r + 1, d[l]) = min(4, 3) = 3
-
Calculate Part A (substrings starting at 0, ending before 3):
- These are substrings from position 0 to 2
- Count:
a = (1 + 3 - 0) * (3 - 0) // 2 = 4 * 3 // 2 = 6
- These are: "0" (length 1), "00" (length 2), "001" (length 3)
-
Calculate Part B (valid substrings ending in range [3, 3]):
b = pre[4] - pre[3] = 9 - 6 = 3
- These are the valid substrings ending at position 3: "1", "11", "011"
-
Total:
a + b = 6 + 3 = 9
The answer correctly identifies 9 valid substrings:
- Starting at 0: "0", "00", "001" (3 substrings)
- Starting at 1: "0", "01", "011" (3 substrings)
- Starting at 2: "1", "11" (2 substrings)
- Starting at 3: "1" (1 substring)
Note that "0011" is invalid because it has 2 zeros AND 2 ones (both > k=1).
Solution Implementation
1class Solution:
2 def countKConstraintSubstrings(
3 self, s: str, k: int, queries: List[List[int]]
4 ) -> List[int]:
5 # Count of 0s and 1s in current window
6 count = [0, 0]
7 left = 0
8 n = len(s)
9
10 # For each index i, store the rightmost index j such that
11 # all substrings starting at i and ending before j satisfy the k-constraint
12 right_boundary = [n] * n
13
14 # Prefix sum array to store cumulative count of valid substrings ending at each position
15 prefix_sum = [0] * (n + 1)
16
17 # Sliding window to find valid substrings
18 for right, char in enumerate(map(int, s)):
19 # Add current character to window
20 count[char] += 1
21
22 # Shrink window from left while constraint is violated
23 # (both 0s and 1s exceed k)
24 while count[0] > k and count[1] > k:
25 right_boundary[left] = right
26 count[int(s[left])] -= 1
27 left += 1
28
29 # Count of valid substrings ending at position 'right'
30 # All substrings from indices [left, right] to right are valid
31 prefix_sum[right + 1] = prefix_sum[right] + (right - left + 1)
32
33 result = []
34 for query_left, query_right in queries:
35 # Find the boundary point: minimum of query_right+1 and right_boundary[query_left]
36 # This represents where substrings starting at query_left become invalid
37 boundary_point = min(query_right + 1, right_boundary[query_left])
38
39 # Count substrings starting at query_left and ending before boundary_point
40 # These are all valid substrings: sum from 1 to (boundary_point - query_left)
41 substrings_before_boundary = (1 + boundary_point - query_left) * (boundary_point - query_left) // 2
42
43 # Count remaining valid substrings in range [boundary_point, query_right]
44 # Use prefix sum to get this count efficiently
45 substrings_after_boundary = prefix_sum[query_right + 1] - prefix_sum[boundary_point]
46
47 result.append(substrings_before_boundary + substrings_after_boundary)
48
49 return result
50
1class Solution {
2 public long[] countKConstraintSubstrings(String s, int k, int[][] queries) {
3 // Array to count occurrences of '0' and '1' in current window
4 int[] charCount = new int[2];
5 int n = s.length();
6
7 // rightBoundary[i] stores the rightmost index where substring starting at i is valid
8 // Initialize all positions to n (end of string) as default
9 int[] rightBoundary = new int[n];
10 Arrays.fill(rightBoundary, n);
11
12 // prefixSum[i] stores the cumulative count of valid substrings ending before index i
13 long[] prefixSum = new long[n + 1];
14
15 // Use sliding window to find valid substrings and build prefix sum array
16 int left = 0;
17 for (int right = 0; right < n; right++) {
18 // Expand window by including character at right pointer
19 charCount[s.charAt(right) - '0']++;
20
21 // Shrink window from left while constraint is violated
22 // (both '0' and '1' counts exceed k)
23 while (charCount[0] > k && charCount[1] > k) {
24 // Mark the right boundary for substring starting at left
25 rightBoundary[left] = right;
26 // Remove leftmost character from window and move left pointer
27 charCount[s.charAt(left) - '0']--;
28 left++;
29 }
30
31 // Add count of all valid substrings ending at current right position
32 // Number of valid substrings = right - left + 1
33 prefixSum[right + 1] = prefixSum[right] + (right - left + 1);
34 }
35
36 // Process each query to calculate the result
37 int queryCount = queries.length;
38 long[] result = new long[queryCount];
39
40 for (int i = 0; i < queryCount; i++) {
41 int queryLeft = queries[i][0];
42 int queryRight = queries[i][1];
43
44 // Find the pivot point: minimum of query's right boundary + 1
45 // and the right boundary of substring starting at queryLeft
46 int pivot = Math.min(queryRight + 1, rightBoundary[queryLeft]);
47
48 // Part 1: Count substrings starting from queryLeft to pivot-1
49 // These are substrings that start at queryLeft and extend various lengths
50 // Using arithmetic sequence sum formula: n*(n+1)/2
51 long leftPartCount = (1L + pivot - queryLeft) * (pivot - queryLeft) / 2;
52
53 // Part 2: Count substrings that are fully within [pivot, queryRight]
54 // Use prefix sum to get this count efficiently
55 long rightPartCount = prefixSum[queryRight + 1] - prefixSum[pivot];
56
57 // Total count for this query
58 result[i] = leftPartCount + rightPartCount;
59 }
60
61 return result;
62 }
63}
64
1class Solution {
2public:
3 vector<long long> countKConstraintSubstrings(string s, int k, vector<vector<int>>& queries) {
4 // Track count of '0's and '1's in current window
5 int charCount[2] = {0, 0};
6 int n = s.size();
7
8 // rightBoundary[i] stores the rightmost index where substring starting at i is valid
9 // Initialize with n (beyond last index) assuming all substrings from i to end are valid
10 vector<int> rightBoundary(n, n);
11
12 // prefixSum[i] stores total count of valid substrings ending at index i-1
13 vector<long long> prefixSum(n + 1, 0);
14
15 // Two-pointer approach to find valid substrings
16 int left = 0;
17 for (int right = 0; right < n; ++right) {
18 // Expand window by including character at right pointer
19 charCount[s[right] - '0']++;
20
21 // Shrink window from left while constraint is violated
22 // (both 0s and 1s count exceed k)
23 while (charCount[0] > k && charCount[1] > k) {
24 rightBoundary[left] = right; // Mark where substring from left becomes invalid
25 charCount[s[left] - '0']--;
26 left++;
27 }
28
29 // All substrings ending at 'right' and starting from 'left' to 'right' are valid
30 // Count of such substrings = right - left + 1
31 prefixSum[right + 1] = prefixSum[right] + (right - left + 1);
32 }
33
34 vector<long long> result;
35
36 // Process each query
37 for (const auto& query : queries) {
38 int queryLeft = query[0];
39 int queryRight = query[1];
40
41 // Find the boundary point within query range
42 // All substrings starting from queryLeft to boundaryPoint-1 can extend to queryRight
43 int boundaryPoint = min(queryRight + 1, rightBoundary[queryLeft]);
44
45 // Part 1: Count substrings starting from queryLeft that are entirely within valid range
46 // These are substrings from queryLeft to queryLeft, queryLeft to queryLeft+1, ..., queryLeft to boundaryPoint-1
47 // Using arithmetic progression sum formula: n*(n+1)/2
48 long long validFromQueryLeft = (1LL + boundaryPoint - queryLeft) * (boundaryPoint - queryLeft) / 2;
49
50 // Part 2: Count all valid substrings in range [boundaryPoint, queryRight]
51 // Use precomputed prefix sums
52 long long validInRemainingRange = prefixSum[queryRight + 1] - prefixSum[boundaryPoint];
53
54 result.push_back(validFromQueryLeft + validInRemainingRange);
55 }
56
57 return result;
58 }
59};
60
1/**
2 * Counts the number of valid substrings for each query where a substring is valid
3 * if it contains at most k zeros or at most k ones
4 * @param s - Binary string containing only '0' and '1'
5 * @param k - Maximum allowed count of zeros or ones in a valid substring
6 * @param queries - Array of query ranges [left, right]
7 * @returns Array of counts for each query
8 */
9function countKConstraintSubstrings(s: string, k: number, queries: number[][]): number[] {
10 // Track count of zeros and ones in current window
11 const zeroOneCount: [number, number] = [0, 0];
12 const stringLength: number = s.length;
13
14 // For each position i, store the rightmost index j where substring s[i..j] is valid
15 const rightmostValidIndex: number[] = Array(stringLength).fill(stringLength);
16
17 // Prefix sum array to store cumulative count of valid substrings ending at each position
18 const prefixSum: number[] = Array(stringLength + 1).fill(0);
19
20 // Use sliding window to find all valid substrings
21 let leftPointer: number = 0;
22 for (let rightPointer: number = 0; rightPointer < stringLength; ++rightPointer) {
23 // Add current character to the window (convert char to 0 or 1 using unary plus)
24 zeroOneCount[+s[rightPointer]]++;
25
26 // Shrink window from left while constraint is violated
27 // (both zeros and ones count exceed k)
28 while (Math.min(zeroOneCount[0], zeroOneCount[1]) > k) {
29 // Mark the rightmost valid position for current left pointer
30 rightmostValidIndex[leftPointer] = rightPointer;
31 // Remove leftmost character from window
32 zeroOneCount[+s[leftPointer]]--;
33 leftPointer++;
34 }
35
36 // Add count of valid substrings ending at rightPointer to prefix sum
37 // Number of valid substrings = rightPointer - leftPointer + 1
38 prefixSum[rightPointer + 1] = prefixSum[rightPointer] + rightPointer - leftPointer + 1;
39 }
40
41 const results: number[] = [];
42
43 // Process each query
44 for (const [queryLeft, queryRight] of queries) {
45 // Find the boundary point: minimum of queryRight+1 and rightmost valid index from queryLeft
46 const boundaryPoint: number = Math.min(queryRight + 1, rightmostValidIndex[queryLeft]);
47
48 // Calculate substrings starting from queryLeft up to boundary using arithmetic sequence formula
49 // These are substrings where starting point is fixed at positions from queryLeft to boundaryPoint-1
50 const arithmeticSum: number = ((1 + boundaryPoint - queryLeft) * (boundaryPoint - queryLeft)) / 2;
51
52 // Add remaining valid substrings from boundary to queryRight using prefix sum
53 const remainingSum: number = prefixSum[queryRight + 1] - prefixSum[boundaryPoint];
54
55 results.push(arithmeticSum + remainingSum);
56 }
57
58 return results;
59}
60
Time and Space Complexity
Time Complexity: O(n + m)
The algorithm consists of two main phases:
-
Preprocessing phase: The first loop iterates through the string
s
once with a two-pointer technique. Although there's a nested while loop, each character is visited at most twice (once by pointerj
and once by pointeri
). This gives usO(n)
time. -
Query processing phase: The second loop processes each query in constant time
O(1)
by using the precomputed arraysd
andpre
. Withm
queries, this phase takesO(m)
time.
Therefore, the total time complexity is O(n + m)
.
Space Complexity: O(n)
The algorithm uses several auxiliary data structures:
cnt
: array of size 2, which isO(1)
d
: array of sizen
, which isO(n)
pre
: array of sizen + 1
, which isO(n)
ans
: array to store results of sizem
, which isO(m)
Since the output array ans
is typically not counted in space complexity analysis (as it's required for the output), the dominant space usage comes from arrays d
and pre
, giving us O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Boundary Calculation for Queries
A common mistake is misunderstanding what right_boundary[i]
represents and how to use it in query processing. Developers might incorrectly assume all substrings starting at query_left
and ending anywhere in [query_left, query_right]
are valid if right_boundary[query_left] > query_right
.
Wrong approach:
# Incorrect: Assuming all substrings are valid if boundary exceeds query range if right_boundary[query_left] > query_right: result.append((query_right - query_left + 1) * (query_right - query_left + 2) // 2)
Correct approach:
# Must still split into two parts even when boundary exceeds range
boundary_point = min(query_right + 1, right_boundary[query_left])
substrings_before_boundary = (1 + boundary_point - query_left) * (boundary_point - query_left) // 2
substrings_after_boundary = prefix_sum[query_right + 1] - prefix_sum[boundary_point]
2. Off-by-One Errors in Prefix Sum Array
The prefix sum array uses 1-based indexing (prefix_sum[j+1]
stores sum up to index j
), which can lead to indexing errors.
Wrong approach:
# Incorrect indexing prefix_sum[right] = prefix_sum[right - 1] + (right - left + 1) # Wrong! substrings_after_boundary = prefix_sum[query_right] - prefix_sum[boundary_point - 1] # Wrong!
Correct approach:
# Proper 1-based indexing for prefix sum prefix_sum[right + 1] = prefix_sum[right] + (right - left + 1) substrings_after_boundary = prefix_sum[query_right + 1] - prefix_sum[boundary_point]
3. Misunderstanding the K-Constraint Condition
The k-constraint is satisfied if either condition is met (at most k zeros OR at most k ones), not both. The violation occurs only when both counts exceed k.
Wrong approach:
# Incorrect: Treating it as AND condition
while count[0] > k or count[1] > k: # Wrong! Too restrictive
right_boundary[left] = right
count[int(s[left])] -= 1
left += 1
Correct approach:
# Correct: Window violates constraint only when BOTH exceed k
while count[0] > k and count[1] > k:
right_boundary[left] = right
count[int(s[left])] -= 1
left += 1
4. Integer Overflow in Substring Count Formula
When calculating the number of substrings, the formula (1 + p - l) * (p - l) // 2
can cause integer overflow for large values, though this is more relevant in languages like C++ than Python.
Prevention in Python:
# Python handles large integers automatically, but for other languages: # Use appropriate data types (long long in C++) or modular arithmetic if required substrings_before_boundary = (1 + boundary_point - query_left) * (boundary_point - query_left) // 2
5. Not Initializing right_boundary Correctly
Failing to initialize right_boundary
array to n
can cause incorrect results for positions where the window never violates the constraint.
Wrong approach:
right_boundary = [0] * n # Wrong! Should be initialized to n
Correct approach:
right_boundary = [n] * n # Correct: Default to string end if never violated
What's the relationship between a tree and a graph?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Want a Structured Path to Master System Design Too? Don’t Miss This!