2489. Number of Substrings With Fixed Ratio 🔒
Problem Description
You are given a binary string s
consisting of only '0's and '1's, along with two coprime integers num1
and num2
.
A ratio substring is defined as any substring of s
where the ratio between the count of '0's and the count of '1's is exactly num1 : num2
. This means if a substring has x
zeros and y
ones, then x/y = num1/num2
.
For example, if num1 = 2
and num2 = 3
:
"01011"
is a ratio substring because it has 2 zeros and 3 ones (ratio 2:3)"1110000111"
is a ratio substring because it has 4 zeros and 6 ones (ratio 4:6 = 2:3)"11000"
is NOT a ratio substring because it has 3 zeros and 2 ones (ratio 3:2 ≠ 2:3)
Your task is to count the total number of non-empty substrings in s
that are ratio substrings.
Key points to remember:
- A substring is a contiguous sequence of characters from the string
- Two numbers are coprime if their greatest common divisor (GCD) equals 1
- The substring must be non-empty
- Since
num1
andnum2
are coprime, the rationum1:num2
is already in its simplest form
The solution uses a clever transformation: instead of checking each substring individually, it uses the fact that a substring from index i
to j
satisfies the ratio if zeros[i,j] / ones[i,j] = num1 / num2
, which can be rearranged to ones[i,j] * num1 - zeros[i,j] * num2 = 0
. Using prefix sums, this becomes (ones[j] - ones[i]) * num1 - (zeros[j] - zeros[i]) * num2 = 0
, which simplifies to checking when ones[j] * num1 - zeros[j] * num2 = ones[i] * num1 - zeros[i] * num2
.
Intuition
The naive approach would be to check every possible substring and count zeros and ones to verify if they satisfy the ratio num1:num2
. However, this would be inefficient with O(n²) substrings to check.
The key insight comes from transforming the ratio condition into something we can track efficiently. For a substring to have the correct ratio, we need:
count_of_zeros / count_of_ones = num1 / num2
Cross-multiplying gives us:
count_of_zeros * num2 = count_of_ones * num1
Rearranging:
count_of_ones * num1 - count_of_zeros * num2 = 0
Now, instead of thinking about individual substrings, we can think about prefix counts. If we track the cumulative count of ones and zeros as we traverse the string, a substring from position i
to position j
has:
- Ones:
ones[j] - ones[i]
- Zeros:
zeros[j] - zeros[i]
The ratio condition becomes:
(ones[j] - ones[i]) * num1 - (zeros[j] - zeros[i]) * num2 = 0
Expanding this:
ones[j] * num1 - zeros[j] * num2 = ones[i] * num1 - zeros[i] * num2
This reveals the crucial pattern: for any position j
, we need to find how many previous positions i
have the same value of ones * num1 - zeros * num2
. This is a classic pattern that can be solved using a hash map to count occurrences of each value.
As we iterate through the string:
- We maintain running counts of ones and zeros
- We calculate the value
x = ones * num1 - zeros * num2
at each position - We check how many times we've seen this value
x
before (these represent valid starting positions for ratio substrings ending at the current position) - We add the current value to our hash map for future positions
The hash map is initialized with {0: 1}
to handle substrings that start from the beginning of the string (when both prefix counts are 0).
Learn more about Math and Prefix Sum patterns.
Solution Approach
The solution implements a prefix sum approach combined with hash map counting to efficiently find all ratio substrings.
Data Structures Used:
- Two counters
n0
andn1
to track the cumulative count of '0's and '1's respectively - A hash map
cnt
(using Python's Counter) to store frequency of transformed values - An answer counter
ans
to accumulate the result
Algorithm Steps:
-
Initialize the hash map: Start with
cnt = {0: 1}
. This handles the case where a valid substring starts from index 0, as the "empty prefix" has a transformed value of 0. -
Iterate through each character in the string:
- Update the prefix counts:
- If current character is '0': increment
n0
- If current character is '1': increment
n1
- If current character is '0': increment
- Update the prefix counts:
-
Calculate the transformed value: For the current position, compute:
x = n1 * num1 - n0 * num2
This represents the value
ones * num1 - zeros * num2
at the current prefix. -
Count valid substrings ending at current position:
- Look up
cnt[x]
- this tells us how many previous positions have the same transformed value - Add this count to
ans
because each such position forms a valid ratio substring when paired with the current position
- Look up
-
Update the hash map: Increment
cnt[x]
by 1 to record that we've seen this transformed value one more time (at the current position).
Why this works:
- When two positions
i
andj
have the same transformed value (ones[i] * num1 - zeros[i] * num2 = ones[j] * num1 - zeros[j] * num2
), the substring between them has exactly the rationum1:num2
- By maintaining the count of each transformed value seen so far, we can immediately determine how many valid substrings end at each position
- The total time complexity is O(n) where n is the length of the string, as we make a single pass through the string with O(1) operations at each step
- Space complexity is O(n) in the worst case for the hash map
The elegance of this solution lies in transforming a ratio comparison problem into a difference problem, allowing us to use the common pattern of "finding subarrays with a specific sum" (here, sum of 0 after transformation).
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 = "0101"
, num1 = 1
, and num2 = 2
.
We want to find substrings where the ratio of zeros to ones is 1:2 (meaning for every 1 zero, there should be 2 ones).
Step-by-step execution:
Initial state:
n0 = 0
(count of zeros)n1 = 0
(count of ones)cnt = {0: 1}
(hash map initialized with 0)ans = 0
(answer counter)
Position 0: s[0] = '0'
- Update counts:
n0 = 1
,n1 = 0
- Calculate transformed value:
x = n1 * num1 - n0 * num2 = 0 * 1 - 1 * 2 = -2
- Check
cnt[-2]
: Not found, so 0 valid substrings end here - Update hash map:
cnt = {0: 1, -2: 1}
ans = 0
Position 1: s[1] = '1'
- Update counts:
n0 = 1
,n1 = 1
- Calculate transformed value:
x = 1 * 1 - 1 * 2 = -1
- Check
cnt[-1]
: Not found, so 0 valid substrings end here - Update hash map:
cnt = {0: 1, -2: 1, -1: 1}
ans = 0
Position 2: s[2] = '0'
- Update counts:
n0 = 2
,n1 = 1
- Calculate transformed value:
x = 1 * 1 - 2 * 2 = -3
- Check
cnt[-3]
: Not found, so 0 valid substrings end here - Update hash map:
cnt = {0: 1, -2: 1, -1: 1, -3: 1}
ans = 0
Position 3: s[3] = '1'
- Update counts:
n0 = 2
,n1 = 2
- Calculate transformed value:
x = 2 * 1 - 2 * 2 = -2
- Check
cnt[-2]
: Found with value 1! This means there's 1 position (after position 0) with the same transformed value- This corresponds to the substring from position 1 to 3: "101" which has 1 zero and 2 ones (ratio 1:2 ✓)
- Update
ans = 0 + 1 = 1
- Update hash map:
cnt = {0: 1, -2: 2, -1: 1, -3: 1}
Final answer: 1
Verification: Let's verify by checking all possible substrings:
- "0" → 1 zero, 0 ones (invalid - division by zero)
- "01" → 1 zero, 1 one (ratio 1:1 ≠ 1:2)
- "010" → 2 zeros, 1 one (ratio 2:1 ≠ 1:2)
- "0101" → 2 zeros, 2 ones (ratio 2:2 = 1:1 ≠ 1:2)
- "1" → 0 zeros, 1 one (ratio 0:1 ≠ 1:2)
- "10" → 1 zero, 1 one (ratio 1:1 ≠ 1:2)
- "101" → 1 zero, 2 ones (ratio 1:2 ✓)
- "0" → 1 zero, 0 ones (invalid)
- "01" → 1 zero, 1 one (ratio 1:1 ≠ 1:2)
- "1" → 0 zeros, 1 one (ratio 0:1 ≠ 1:2)
Indeed, only "101" satisfies the ratio requirement, confirming our answer of 1.
The key insight is that when we found x = -2
at position 3, it matched the value from position 0, indicating that the substring between these positions (exclusive of position 0, inclusive of position 3) maintains the required ratio.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def fixedRatio(self, s: str, num1: int, num2: int) -> int:
5 """
6 Count the number of subarrays where the ratio of 1s to 0s equals num1:num2.
7
8 The key insight: For a subarray to have ratio ones:zeros = num1:num2,
9 we need ones * num2 = zeros * num1, or equivalently:
10 ones * num1 - zeros * num2 = 0
11
12 We use prefix sums and track the difference (ones * num1 - zeros * num2).
13 When we see the same difference value again, it means the subarray between
14 those two positions has the desired ratio.
15
16 Args:
17 s: Binary string containing only '0' and '1'
18 num1: Target ratio numerator for 1s
19 num2: Target ratio numerator for 0s
20
21 Returns:
22 Number of subarrays with the fixed ratio
23 """
24 zeros_count = 0 # Running count of zeros
25 ones_count = 0 # Running count of ones
26 result = 0 # Total number of valid subarrays
27
28 # Track frequency of each difference value
29 # Initialize with 0:1 to handle subarrays starting from index 0
30 difference_frequency = Counter({0: 1})
31
32 # Iterate through each character in the string
33 for char in s:
34 # Update running counts
35 zeros_count += (char == '0')
36 ones_count += (char == '1')
37
38 # Calculate the weighted difference
39 # If this difference was seen before, those positions form valid subarrays
40 weighted_difference = ones_count * num1 - zeros_count * num2
41
42 # Add count of previous occurrences of this difference
43 # Each previous occurrence forms a valid subarray ending at current position
44 result += difference_frequency[weighted_difference]
45
46 # Update frequency map for current difference
47 difference_frequency[weighted_difference] += 1
48
49 return result
50
1class Solution {
2 public long fixedRatio(String s, int num1, int num2) {
3 // Count of 0s and 1s seen so far
4 long zeroCount = 0;
5 long oneCount = 0;
6
7 // Total number of valid subarrays found
8 long result = 0;
9
10 // HashMap to store frequency of each difference value
11 // Key: difference value (oneCount * num1 - zeroCount * num2)
12 // Value: frequency of this difference
13 Map<Long, Long> differenceFrequency = new HashMap<>();
14
15 // Initialize with 0 difference having frequency 1 (empty prefix)
16 differenceFrequency.put(0L, 1L);
17
18 // Iterate through each character in the string
19 for (char character : s.toCharArray()) {
20 // Update count of 0s or 1s based on current character
21 if (character == '0') {
22 zeroCount++;
23 } else {
24 oneCount++;
25 }
26
27 // Calculate the difference for current prefix
28 // For a valid subarray [i, j], we need:
29 // (ones in [i,j]) / (zeros in [i,j]) = num1 / num2
30 // This transforms to: ones * num2 = zeros * num1
31 // Or: ones * num1 - zeros * num2 = 0
32 long currentDifference = oneCount * num1 - zeroCount * num2;
33
34 // Add count of previous prefixes with same difference
35 // These form valid subarrays ending at current position
36 result += differenceFrequency.getOrDefault(currentDifference, 0L);
37
38 // Update frequency map with current difference
39 differenceFrequency.put(currentDifference,
40 differenceFrequency.getOrDefault(currentDifference, 0L) + 1);
41 }
42
43 return result;
44 }
45}
46
1using ll = long long;
2
3class Solution {
4public:
5 long long fixedRatio(string s, int num1, int num2) {
6 // Count of 0s and 1s encountered so far
7 ll count_zeros = 0;
8 ll count_ones = 0;
9
10 // Result counter for valid subarrays
11 ll result = 0;
12
13 // Map to store frequency of each difference value
14 // Key: difference value (count_ones * num1 - count_zeros * num2)
15 // Value: frequency of this difference
16 unordered_map<ll, ll> frequency_map;
17
18 // Initialize with empty prefix (before processing any character)
19 frequency_map[0] = 1;
20
21 // Iterate through each character in the string
22 for (char& current_char : s) {
23 // Update counts based on current character
24 count_zeros += (current_char == '0');
25 count_ones += (current_char == '1');
26
27 // Calculate the difference for current prefix
28 // Looking for subarrays where ones:zeros = num1:num2
29 // This translates to: count_ones * num2 = count_zeros * num1
30 // Rearranged as: count_ones * num1 - count_zeros * num2 = 0
31 ll difference = count_ones * num1 - count_zeros * num2;
32
33 // Add count of previous prefixes with same difference
34 // These form valid subarrays ending at current position
35 result += frequency_map[difference];
36
37 // Increment frequency of current difference
38 ++frequency_map[difference];
39 }
40
41 return result;
42 }
43};
44
1function fixedRatio(s: string, num1: number, num2: number): number {
2 // Count of 0s and 1s encountered so far
3 let countZeros: number = 0;
4 let countOnes: number = 0;
5
6 // Result counter for valid subarrays
7 let result: number = 0;
8
9 // Map to store frequency of each difference value
10 // Key: difference value (countOnes * num1 - countZeros * num2)
11 // Value: frequency of this difference
12 const frequencyMap: Map<number, number> = new Map();
13
14 // Initialize with empty prefix (before processing any character)
15 frequencyMap.set(0, 1);
16
17 // Iterate through each character in the string
18 for (const currentChar of s) {
19 // Update counts based on current character
20 if (currentChar === '0') {
21 countZeros++;
22 }
23 if (currentChar === '1') {
24 countOnes++;
25 }
26
27 // Calculate the difference for current prefix
28 // Looking for subarrays where ones:zeros = num1:num2
29 // This translates to: countOnes * num2 = countZeros * num1
30 // Rearranged as: countOnes * num1 - countZeros * num2 = 0
31 const difference: number = countOnes * num1 - countZeros * num2;
32
33 // Add count of previous prefixes with same difference
34 // These form valid subarrays ending at current position
35 result += frequencyMap.get(difference) || 0;
36
37 // Increment frequency of current difference
38 frequencyMap.set(difference, (frequencyMap.get(difference) || 0) + 1);
39 }
40
41 return result;
42}
43
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. The algorithm iterates through each character in the string exactly once. Within each iteration, all operations are constant time: incrementing counters (n0
and n1
), computing the value x = n1 * num1 - n0 * num2
, accessing and updating the Counter
dictionary, and adding to ans
. Dictionary operations (lookup and insertion) are O(1)
on average.
The space complexity is O(n)
, where n
is the length of the string s
. The Counter
dictionary cnt
stores at most n + 1
distinct values (one for the initial state and potentially one unique value for each character position). In the worst case, each prefix of the string could produce a different value of x = n1 * num1 - n0 * num2
, resulting in n + 1
entries in the dictionary. All other variables (n0
, n1
, ans
, x
) use constant space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Ratio Interpretation
A critical pitfall is misunderstanding which count corresponds to which parameter. The problem states the ratio is zeros:ones = num1:num2
, but developers often reverse this relationship.
Wrong interpretation:
# Incorrectly assuming num1 corresponds to ones and num2 to zeros weighted_difference = zeros_count * num1 - ones_count * num2
Correct interpretation:
# num1 corresponds to zeros, num2 corresponds to ones # So zeros/ones = num1/num2, which rearranges to: weighted_difference = zeros_count * num2 - ones_count * num1
2. Forgetting to Initialize the Hash Map with {0: 1}
The hash map must be initialized with {0: 1}
to handle substrings that start from the beginning of the string. Without this, valid substrings starting at index 0 won't be counted.
Incorrect:
difference_frequency = Counter() # Missing initial value
Correct:
difference_frequency = Counter({0: 1}) # Handles empty prefix
3. Order of Operations: Counting Before Updating
The order matters - you must first add the count of previous occurrences to the result, then update the frequency map. Reversing this order will incorrectly include the current position in its own count.
Wrong order:
# Updates frequency first (incorrect) difference_frequency[weighted_difference] += 1 result += difference_frequency[weighted_difference] # Now includes current position
Correct order:
# Count previous occurrences first result += difference_frequency[weighted_difference] # Then update for future positions difference_frequency[weighted_difference] += 1
4. Edge Case: All Zeros or All Ones
When the string contains only zeros or only ones, the ratio check becomes problematic. For instance, if s = "0000"
and we're looking for ratio 2:3
, no valid substring exists since there are no ones. The algorithm handles this correctly, but developers might add unnecessary special case handling that introduces bugs.
5. Integer Overflow in Other Languages
While Python handles arbitrary precision integers automatically, implementing this in languages like Java or C++ requires careful consideration of potential overflow when calculating ones_count * num1 - zeros_count * num2
for large strings and large ratio values.
Solution for languages with fixed integer sizes:
// Use long instead of int to prevent overflow long weightedDifference = (long)onesCount * num1 - (long)zerosCount * num2;
Consider the classic dynamic programming of longest increasing subsequence:
Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.
For example, the length of LIS for [50, 3, 10, 7, 40, 80]
is 4
and LIS is
[3, 7, 40, 80]
.
What is the recurrence relation?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!