Count Substrings That Satisfy K-Constraint II
HardPrompt
You are given a binary string s and an integer k.
You are also given a 2D integer array queries, where queries[i] = [li, ri].
A binary string satisfies the k-constraint if either of the following conditions holds:
- The number of
0's in the string is at mostk. - The number of
1's in the string is at mostk.
Return an integer array answer, where answer[i] is the number of substrings of s[li..ri] that satisfy the k-constraint.
Example 1:
Input: s = "0001111", k = 2, queries = [[0,6]]
Output: [26]
Explanation:
For the query [0, 6], all substrings of s[0..6] = "0001111" satisfy the k-constraint except for the substrings s[0..5] = "000111" and s[0..6] = "0001111".
Example 2:
Input: s = "010101", k = 1, queries = [[0,5],[1,4],[2,3]]
Output: [15,9,3]
Explanation:
The substrings of s with a length greater than 3 do not satisfy the k-constraint.
Constraints:
1 <= s.length <= 105s[i]is either'0'or'1'.1 <= k <= s.length1 <= queries.length <= 105queries[i] == [li, ri]0 <= li <= ri < s.length- All queries are distinct.
Approaches
3 approaches with complexity analysis and trade-offs.
This is the most straightforward approach. For each query, we iterate through all possible substrings within the given range [l, r]. For each of these substrings, we count the number of '0's and '1's to check if it satisfies the k-constraint. To make this slightly more efficient than re-counting for every substring, we can maintain running counts of zeros and ones as we extend the substring from a fixed starting point.
Algorithm
- For each query
[l, r]inqueries:- Initialize
valid_count = 0. - Iterate through all possible start indices
ifromltor.- Initialize
zeros = 0,ones = 0. - Iterate through all possible end indices
jfromitor.- Update
zerosandonesbased ons[j]. - If
zeros <= korones <= k, incrementvalid_count.
- Update
- Initialize
- Add
valid_countto the answer list.
- Initialize
Walkthrough
The brute-force method directly translates the problem statement into code. We are given Q queries. For each query [l, r], we need to examine every substring of s[l..r]. A substring is defined by its start and end indices. We can use nested loops to generate all such pairs of indices (i, j) where l <= i <= j <= r.
For each substring s[i..j], we then count the occurrences of '0's and '1's. If the count of '0's is at most k, or the count of '1's is at most k, we increment a counter for the current query. After checking all substrings for the query [l, r], the value of the counter is our answer for that query.
An optimization is to fix the starting point i and extend the endpoint j. As j increments, we can update the counts of '0's and '1's in O(1) time instead of recounting the entire substring s[i..j], which would take O(j-i+1) time. This improves the complexity for a single query from O(L^3) to O(L^2), where L is the length of the query range.
class Solution { public long[] countSubstrings(String s, int k, int[][] queries) { int q = queries.length; long[] ans = new long[q]; for (int i = 0; i < q; i++) { int l = queries[i][0]; int r = queries[i][1]; long count = 0; for (int start = l; start <= r; start++) { int zeros = 0; int ones = 0; for (int end = start; end <= r; end++) { if (s.charAt(end) == '0') { zeros++; } else { ones++; } if (zeros <= k || ones <= k) { count++; } } } ans[i] = count; } return ans; }}Complexity
Time
O(Q * N^2), where Q is the number of queries and N is the length of the string. For each query, the two nested loops run in O(L^2) where L is the length of the query range (r-l+1), which can be up to N.
Space
O(1) extra space, excluding the space for the output array.
Trade-offs
Pros
Simple to understand and implement.
Requires minimal space.
Cons
Extremely inefficient and will result in a 'Time Limit Exceeded' error for the given constraints.
This approach improves upon brute-force by using precomputation. Instead of counting valid substrings directly, we count the total number of substrings and subtract the number of invalid ones. A substring is invalid if it has more than k zeros AND more than k ones. The key is to efficiently find, for each starting position i, the first ending position j that makes the substring s[i..j] invalid. This information is precomputed and stored in an array. Then, for each query, we can use this precomputed data to count invalid substrings with a single loop over the query range.
Algorithm
- Precomputation:
- Compute
pos0,pos1(lists of indices for '0's and '1's) andrank0,rank1(prefix counts of '0's and '1's). This takesO(N). - For each
ifrom0toN-1, calculatefirst_invalid_end[i], the smallestj >= iwheres[i..j]is invalid. This can be done inO(1)for eachiusing the precomputed arrays. Total precomputation time isO(N).
- Compute
- Query Processing:
- For each query
[l, r]:- Calculate total substrings in
s[l..r]:total = len * (len + 1) / 2wherelen = r - l + 1. - Initialize
invalid_substrings = 0. - Iterate
ifromltor:- The first invalid ending index for a substring starting at
iisfirst_invalid_end[i]. - The number of invalid substrings starting at
iand ending within[l,r]ismax(0, r - first_invalid_end[i] + 1). - Add this to
invalid_substrings.
- The first invalid ending index for a substring starting at
- The answer for the query is
total_substrings - invalid_substrings.
- Calculate total substrings in
- For each query
Walkthrough
The main idea is to switch from counting valid substrings to counting invalid ones. The total number of substrings in a range [l, r] of length len = r - l + 1 is easily calculated as len * (len + 1) / 2.
An invalid substring s[i..j] must satisfy count0(s[i..j]) > k and count1(s[i..j]) > k. For a fixed start i, as we increase j, the counts of zeros and ones are non-decreasing. This means there's a minimum j where the substring s[i..j] becomes invalid. Let's call this first_invalid_end[i]. Any substring s[i..p] with p >= first_invalid_end[i] will also be invalid.
We can precompute first_invalid_end[i] for all i in O(N) time. This involves finding the index of the (k+1)-th '0' and (k+1)-th '1' after position i. This is done efficiently using precomputed lists of indices for '0's and '1's (pos0, pos1) and prefix counts (rank0, rank1).
Once first_invalid_end is computed, we process each query [l, r]. We iterate i from l to r. For each i, the number of invalid substrings starting at i and contained within s[l..r] is the number of valid end indices j in the range [first_invalid_end[i], r]. This count is max(0, r - first_invalid_end[i] + 1). Summing this up for all i in [l, r] gives the total number of invalid substrings for the query.
class Solution { public long[] countSubstrings(String s, int k, int[][] queries) { int n = s.length(); List<Integer> pos0 = new ArrayList<>(); List<Integer> pos1 = new ArrayList<>(); for (int i = 0; i < n; i++) { if (s.charAt(i) == '0') pos0.add(i); else pos1.add(i); } int[] rank0 = new int[n + 1]; int[] rank1 = new int[n + 1]; for (int i = 0; i < n; i++) { rank0[i+1] = rank0[i] + (s.charAt(i) == '0' ? 1 : 0); rank1[i+1] = rank1[i] + (s.charAt(i) == '1' ? 1 : 0); } int[] firstInvalidEnd = new int[n]; for (int i = 0; i < n; i++) { int targetRank0 = rank0[i] + k; int j0 = (targetRank0 < pos0.size()) ? pos0.get(targetRank0) : n; int targetRank1 = rank1[i] + k; int j1 = (targetRank1 < pos1.size()) ? pos1.get(targetRank1) : n; firstInvalidEnd[i] = Math.max(j0, j1); } int q = queries.length; long[] ans = new long[q]; for (int i = 0; i < q; i++) { int l = queries[i][0]; int r = queries[i][1]; long len = r - l + 1; long totalSubstrings = len * (len + 1) / 2; long invalidSubstrings = 0; for (int start = l; start <= r; start++) { int invalidStartIdx = firstInvalidEnd[start]; if (invalidStartIdx <= r) { invalidSubstrings += (r - invalidStartIdx + 1); } } ans[i] = totalSubstrings - invalidSubstrings; } return ans; }}Complexity
Time
O(N + Q*N). The precomputation takes O(N). Each of the Q queries involves a loop of length up to N, leading to a total query time of O(Q*N).
Space
O(N) for storing precomputed arrays like `pos0`, `pos1`, `rank0`, `rank1`, and `first_invalid_end`.
Trade-offs
Pros
Much faster than brute-force due to O(N) precomputation.
The logic of counting invalid substrings is a key insight.
Cons
The query processing part is still slow, taking O(N) per query in the worst case.
Will likely time out for large query ranges.
This approach builds on the previous one to achieve optimal performance. The linear scan per query is the bottleneck. We can optimize the calculation of the sum sum_{i=l to r} max(0, r - first_invalid_end[i] + 1) by processing queries offline. Instead of handling queries sequentially, we group them by their right endpoint r. We then iterate r from 0 to N-1, and at each step, we update a data structure (a Fenwick Tree) and answer all queries that end at the current r. This turns the 2D-like range query problem into a series of 1D range queries that can be solved efficiently.
Algorithm
- Precomputation:
- Same as Approach 2, compute
first_invalid_end[i]for alli. Letf_i = first_invalid_end[i].O(N). - Group all starting indices
iby theirf_ivalue into a list of lists,indicesByF.O(N).
- Same as Approach 2, compute
- Offline Processing:
- Group all queries by their right endpoint
rintoqueriesByR.O(Q). - Initialize two Fenwick Trees (BITs),
bitCountandbitSumF, of sizeN.O(N). - Initialize an answer array
ans.O(Q). - Loop
rfrom0toN-1:- For each index
iinindicesByF[r](i.e.,f_i == r):- "Activate" this index
iby updating the BITs:bitCount.add(i+1, 1)andbitSumF.add(i+1, f_i).
- "Activate" this index
- For each query
(l, q_idx)inqueriesByR[r]:- Query the BITs to get the count of active indices and the sum of their
fvalues in the range[l, r]:count = bitCount.query(l+1, r+1)andsumF = bitSumF.query(l+1, r+1). - Calculate invalid substrings:
invalid = (long)(r + 1) * count - sumF. - Calculate total substrings and find the final answer:
ans[q_idx] = total - invalid.
- Query the BITs to get the count of active indices and the sum of their
- For each index
- Group all queries by their right endpoint
Walkthrough
The number of invalid substrings for a query [l, r] is sum_{i=l to r, f_i <= r} (r + 1 - f_i), where f_i = first_invalid_end[i]. This sum can be rewritten as (r+1) * (count of i in [l,r] with f_i <= r) - (sum of f_i for i in [l,r] with f_i <= r).
This is a complex range sum that depends on both i and r. The offline processing technique elegantly handles this. We sort queries by r. As we iterate r from 0 to N-1, we maintain two Fenwick Trees (BITs). When our sweep line is at r, we "activate" all indices i for which f_i = r. Activating an index i means adding its contribution to the BITs: we add 1 to a bitCount at position i, and we add f_i to a bitSumF at position i.
Now, to answer a query [l, r] that ends at our current r, we simply query our BITs for the range [l, r]. bitCount.query(l, r) will give us the count of indices i in [l, r] such that f_i <= r (because only those have been activated so far). Similarly, bitSumF.query(l, r) gives the sum of their f_i values. With these two values, we can compute the number of invalid substrings in O(log N) time.
class FenwickTree { private long[] bit; private int size; public FenwickTree(int size) { this.size = size; this.bit = new long[size + 1]; } public void add(int index, long value) { for (; index <= size; index += index & -index) { bit[index] += value; } } public long getSum(int index) { long sum = 0; for (; index > 0; index -= index & -index) { sum += bit[index]; } return sum; } public long query(int l, int r) { if (l > r) return 0; return getSum(r) - getSum(l - 1); }} class Solution { public long[] countSubstrings(String s, int k, int[][] queries) { int n = s.length(); List<Integer> pos0 = new ArrayList<>(); List<Integer> pos1 = new ArrayList<>(); for (int i = 0; i < n; i++) { if (s.charAt(i) == '0') pos0.add(i); else pos1.add(i); } int[] rank0 = new int[n + 1]; int[] rank1 = new int[n + 1]; for (int i = 0; i < n; i++) { rank0[i+1] = rank0[i] + (s.charAt(i) == '0' ? 1 : 0); rank1[i+1] = rank1[i] + (s.charAt(i) == '1' ? 1 : 0); } int[] firstInvalidEnd = new int[n]; List<List<Integer>> indicesByF = new ArrayList<>(n + 1); for(int i = 0; i <= n; i++) indicesByF.add(new ArrayList<>()); for (int i = 0; i < n; i++) { int targetRank0 = rank0[i] + k; int j0 = (targetRank0 < pos0.size()) ? pos0.get(targetRank0) : n; int targetRank1 = rank1[i] + k; int j1 = (targetRank1 < pos1.size()) ? pos1.get(targetRank1) : n; firstInvalidEnd[i] = Math.max(j0, j1); if (firstInvalidEnd[i] <= n) { indicesByF.get(firstInvalidEnd[i]).add(i); } } int q = queries.length; List<List<int[]>> queriesByR = new ArrayList<>(n); for(int i = 0; i < n; i++) queriesByR.add(new ArrayList<>()); for (int i = 0; i < q; i++) { queriesByR.get(queries[i][1]).add(new int[]{queries[i][0], i}); } FenwickTree bitCount = new FenwickTree(n); FenwickTree bitSumF = new FenwickTree(n); long[] ans = new long[q]; for (int r = 0; r < n; r++) { if (r < indicesByF.size()) { for (int i : indicesByF.get(r)) { bitCount.add(i + 1, 1); bitSumF.add(i + 1, firstInvalidEnd[i]); } } for (int[] query : queriesByR.get(r)) { int l = query[0]; int qIdx = query[1]; long countActive = bitCount.query(l + 1, r + 1); long sumF = bitSumF.query(l + 1, r + 1); long invalidCount = (long)(r + 1) * countActive - sumF; long len = r - l + 1; long totalSubstrings = len * (len + 1) / 2; ans[qIdx] = totalSubstrings - invalidCount; } } return ans; }}Complexity
Time
O(N + (N+Q) log N). Precomputation is O(N). The main loop involves a total of N BIT updates (O(N log N)) and Q BIT queries (O(Q log N)) across all iterations.
Space
O(N + Q) for storing precomputed arrays, queries grouped by endpoint, and the Fenwick Trees.
Trade-offs
Pros
Highly efficient, passing the given constraints.
A standard and powerful technique for a class of offline range query problems.
Cons
More complex to understand and implement.
Requires knowledge of Fenwick Trees (or Segment Trees) and the offline processing technique.
Solutions
Solution
class Solution {public long[] countKConstraintSubstrings(String s, int k, int[][] queries) { int[] cnt = new int[2]; int n = s.length(); int[] d = new int[n]; Arrays.fill(d, n); long[] pre = new long[n + 1]; for (int i = 0, j = 0; j < n; ++j) { cnt[s.charAt(j) - '0']++; while (cnt[0] > k && cnt[1] > k) { d[i] = j; cnt[s.charAt(i++) - '0']--; } pre[j + 1] = pre[j] + j - i + 1; } int m = queries.length; long[] ans = new long[m]; for (int i = 0; i < m; ++i) { int l = queries[i][0], r = queries[i][1]; int p = Math.min(r + 1, d[l]); long a = (1L + p - l) * (p - l) / 2; long b = pre[r + 1] - pre[p]; ans[i] = a + b; } return ans; }}class Solution {public: vector<long long> countKConstraintSubstrings(string s, int k, vector<vector<int>> &queries) { int cnt[2]{}; int n = s.size(); vector<int> d(n, n); long long pre[n + 1]; pre[0] = 0; for (int i = 0, j = 0; j < n; ++j) { cnt[s[j] - '0']++; while (cnt[0] > k && cnt[1] > k) { d[i] = j; cnt[s[i++] - '0']--; } pre[j + 1] = pre[j] + j - i + 1; } vector<long long> ans; for (const auto &q : queries) { int l = q[0], r = q[1]; int p = min(r + 1, d[l]); long long a = (1LL + p - l) * (p - l) / 2; long long b = pre[r + 1] - pre[p]; ans.push_back(a + b); } return ans; }};class Solution: def countKConstraintSubstrings(self, s: str, k: int, queries: List[List[int]]) -> List[int]: cnt = [0, 0] i, n = 0, len(s) d = [n] * n pre = [0] * (n + 1) for j, x in enumerate(map(int, s)): cnt[x] += 1 while cnt[0] > k and cnt[1] > k: d[i] = j cnt[int(s[i])] -= 1 i += 1 pre[j + 1] = pre[j] + j - i + 1 ans = [] for l, r in queries: p = min(r + 1, d[l]) a = (1 + p - l) * (p - l) // 2 b = pre[r + 1] - pre[p] ans . append(a + b) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.