2168. Unique Substrings With Equal Digit Frequency 🔒
Problem Description
You are given a string s
that consists only of digits (0-9). Your task is to find the number of unique substrings where every digit that appears in the substring appears the same number of times.
For example:
- In substring "112233", digits 1, 2, and 3 each appear exactly 2 times - this is valid
- In substring "1123", digit 1 appears 2 times while digits 2 and 3 appear 1 time each - this is invalid
- In substring "111", only digit 1 appears and it appears 3 times - this is valid
The solution uses a prefix sum approach to efficiently count digit frequencies in any substring. The presum
array tracks cumulative counts of each digit (0-9) up to each position. For each possible substring from index i
to j
, the check
function verifies if all present digits have the same frequency by:
- Calculating the count of each digit in the substring using prefix sums
- Collecting all non-zero counts into a set
- Returning true if the set has at most 1 unique value (meaning all digits appear the same number of times)
Finally, all valid substrings are stored in a set to ensure uniqueness, and the size of this set is returned as the answer.
Intuition
The key insight is that we need to check every possible substring to see if it satisfies our condition (all digits appear the same number of times). A brute force approach would generate each substring and count digit frequencies, but this involves repeated counting of the same characters.
To optimize this, we can observe that counting digit frequencies in a substring s[i:j+1]
is essentially finding the difference between cumulative counts at position j+1
and position i
. This is similar to how we use prefix sums to find range sums efficiently.
For each position in the string, we maintain a running count of how many times each digit (0-9) has appeared up to that point. This gives us a 2D prefix sum array where presum[i][d]
represents the count of digit d
in the substring s[0:i]
.
To check if a substring s[i:j+1]
has all digits appearing the same number of times, we:
- Calculate the frequency of each digit using
presum[j+1][k] - presum[i][k]
- Collect all non-zero frequencies (digits that actually appear in the substring)
- If all these frequencies are the same, they'll form a set with exactly one unique value
The beauty of this approach is that we can check any substring in O(10)
= O(1)
time (since we only have 10 possible digits), rather than O(length of substring)
.
Since the problem asks for unique substrings, not just the count of valid positions, we store the actual substring content in a set to automatically handle duplicates. For instance, if "11" appears multiple times in different positions, we only count it once.
Solution Approach
The implementation follows a prefix sum pattern combined with substring enumeration:
1. Building the Prefix Sum Array:
presum = [[0] * 10 for _ in range(n + 1)]
We create a 2D array where presum[i][j]
stores the count of digit j
in the substring s[0:i]
. The array has dimensions (n+1) × 10
to handle all positions and all possible digits (0-9).
2. Populating Prefix Sums:
for i, c in enumerate(s):
presum[i + 1][int(c)] += 1
for j in range(10):
presum[i + 1][j] += presum[i][j]
For each character in the string:
- Increment the count for the current digit at position
i+1
- Copy all previous counts from position
i
to positioni+1
This ensures presum[i+1]
contains cumulative counts up to and including position i
.
3. Validation Function:
def check(i, j):
v = set()
for k in range(10):
cnt = presum[j + 1][k] - presum[i][k]
if cnt > 0:
v.add(cnt)
if len(v) > 1:
return False
return True
For substring s[i:j+1]
:
- Calculate the frequency of each digit using the difference
presum[j+1][k] - presum[i][k]
- Add non-zero frequencies to a set
v
- If the set ever contains more than one value, different digits have different frequencies, so return
False
- If we finish checking all digits with at most one unique frequency value, return
True
4. Finding All Valid Unique Substrings:
vis = set(s[i : j + 1] for i in range(n) for j in range(i, n) if check(i, j))
Using a set comprehension:
- Enumerate all possible substrings with starting index
i
and ending indexj
- Include only those that pass the
check
function - The set automatically handles duplicate substrings
5. Return Result:
return len(vis)
The size of the set gives us the count of unique valid substrings.
Time Complexity: O(n² × 10)
= O(n²)
where n
is the length of the string. We check all O(n²)
substrings, and each check takes O(10)
= O(1)
time.
Space Complexity: O(n × 10 + n²)
= O(n²)
for the prefix sum array and potentially storing all substrings in the set.
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 the solution with the string s = "1123"
.
Step 1: Build the prefix sum array
We create a presum
array of size (5 × 10)
since n = 4
:
Initial state: all zeros
Position: 0 1 2 3 4 String: '1' '1' '2' '3'
Building prefix sums:
- Position 0: All counts are 0 (base case)
- Position 1 (after '1'): digit 1 count = 1
- Position 2 (after '11'): digit 1 count = 2
- Position 3 (after '112'): digit 1 count = 2, digit 2 count = 1
- Position 4 (after '1123'): digit 1 count = 2, digit 2 count = 1, digit 3 count = 1
The presum
array (showing only digits 0-3 for brevity):
digit: 0 1 2 3 position 0: [0, 0, 0, 0] position 1: [0, 1, 0, 0] position 2: [0, 2, 0, 0] position 3: [0, 2, 1, 0] position 4: [0, 2, 1, 1]
Step 2: Check all possible substrings
Now we enumerate all substrings and check if they're valid:
-
Substring "1" (i=0, j=0):
- Frequency of digit 1:
presum[1][1] - presum[0][1] = 1 - 0 = 1
- Non-zero frequencies: {1}
- Valid ✓ (only one unique frequency)
- Frequency of digit 1:
-
Substring "11" (i=0, j=1):
- Frequency of digit 1:
presum[2][1] - presum[0][1] = 2 - 0 = 2
- Non-zero frequencies: {2}
- Valid ✓
- Frequency of digit 1:
-
Substring "112" (i=0, j=2):
- Frequency of digit 1:
presum[3][1] - presum[0][1] = 2 - 0 = 2
- Frequency of digit 2:
presum[3][2] - presum[0][2] = 1 - 0 = 1
- Non-zero frequencies: {2, 1}
- Invalid ✗ (two different frequencies)
- Frequency of digit 1:
-
Substring "1123" (i=0, j=3):
- Frequency of digit 1:
2 - 0 = 2
- Frequency of digit 2:
1 - 0 = 1
- Frequency of digit 3:
1 - 0 = 1
- Non-zero frequencies: {2, 1}
- Invalid ✗
- Frequency of digit 1:
-
Substring "1" (i=1, j=1):
- Frequency of digit 1:
presum[2][1] - presum[1][1] = 2 - 1 = 1
- Non-zero frequencies: {1}
- Valid ✓ (but duplicate of substring from position 0)
- Frequency of digit 1:
-
Substring "12" (i=1, j=2):
- Frequency of digit 1:
2 - 1 = 1
- Frequency of digit 2:
1 - 0 = 1
- Non-zero frequencies: {1}
- Valid ✓ (all digits appear once)
- Frequency of digit 1:
-
Substring "123" (i=1, j=3):
- Frequency of digit 1:
2 - 1 = 1
- Frequency of digit 2:
1 - 0 = 1
- Frequency of digit 3:
1 - 0 = 1
- Non-zero frequencies: {1}
- Valid ✓ (all digits appear once)
- Frequency of digit 1:
-
Substring "2" (i=2, j=2):
- Frequency of digit 2:
presum[3][2] - presum[2][2] = 1 - 0 = 1
- Non-zero frequencies: {1}
- Valid ✓
- Frequency of digit 2:
-
Substring "23" (i=2, j=3):
- Frequency of digit 2:
1 - 0 = 1
- Frequency of digit 3:
1 - 0 = 1
- Non-zero frequencies: {1}
- Valid ✓
- Frequency of digit 2:
-
Substring "3" (i=3, j=3):
- Frequency of digit 3:
presum[4][3] - presum[3][3] = 1 - 0 = 1
- Non-zero frequencies: {1}
- Valid ✓
- Frequency of digit 3:
Step 3: Collect unique valid substrings
Valid substrings found: {"1", "11", "1", "12", "123", "2", "23", "3"}
After removing duplicates (the second "1"): {"1", "11", "12", "123", "2", "23", "3"}
Final Answer: 7 unique valid substrings
Solution Implementation
1class Solution:
2 def equalDigitFrequency(self, s: str) -> int:
3 """
4 Count the number of unique substrings where all digits appear with equal frequency.
5
6 Args:
7 s: Input string containing only digits
8
9 Returns:
10 Number of unique substrings with equal digit frequency
11 """
12
13 def has_equal_frequency(start_idx: int, end_idx: int) -> bool:
14 """
15 Check if substring s[start_idx:end_idx+1] has all digits with equal frequency.
16
17 Args:
18 start_idx: Starting index of substring (inclusive)
19 end_idx: Ending index of substring (inclusive)
20
21 Returns:
22 True if all present digits have equal frequency, False otherwise
23 """
24 frequency_set = set()
25
26 # Check frequency of each digit (0-9) in the substring
27 for digit in range(10):
28 # Calculate frequency using prefix sum
29 count = prefix_sum[end_idx + 1][digit] - prefix_sum[start_idx][digit]
30
31 if count > 0:
32 frequency_set.add(count)
33
34 # Early termination: more than one unique frequency means not equal
35 if len(frequency_set) > 1:
36 return False
37
38 return True
39
40 n = len(s)
41
42 # Build prefix sum array for digit frequencies
43 # prefix_sum[i][d] = count of digit d in s[0:i]
44 prefix_sum = [[0] * 10 for _ in range(n + 1)]
45
46 for i, char in enumerate(s):
47 digit = int(char)
48
49 # Copy previous frequencies
50 for d in range(10):
51 prefix_sum[i + 1][d] = prefix_sum[i][d]
52
53 # Increment current digit's frequency
54 prefix_sum[i + 1][digit] += 1
55
56 # Collect all valid substrings with equal digit frequency
57 unique_substrings = set()
58
59 for start in range(n):
60 for end in range(start, n):
61 if has_equal_frequency(start, end):
62 substring = s[start:end + 1]
63 unique_substrings.add(substring)
64
65 return len(unique_substrings)
66
1class Solution {
2 public int equalDigitFrequency(String s) {
3 int stringLength = s.length();
4
5 // Create a 2D prefix sum array to store cumulative count of each digit (0-9)
6 // prefixSum[i][d] represents count of digit 'd' from index 0 to i-1
7 int[][] prefixSum = new int[stringLength + 1][10];
8
9 // Build the prefix sum array
10 for (int i = 0; i < stringLength; ++i) {
11 // Increment count for current digit
12 int currentDigit = s.charAt(i) - '0';
13 ++prefixSum[i + 1][currentDigit];
14
15 // Copy all previous counts to current position
16 for (int digit = 0; digit < 10; ++digit) {
17 prefixSum[i + 1][digit] += prefixSum[i][digit];
18 }
19 }
20
21 // Use a set to store unique substrings that have equal digit frequency
22 Set<String> uniqueSubstrings = new HashSet<>();
23
24 // Check all possible substrings
25 for (int startIndex = 0; startIndex < stringLength; ++startIndex) {
26 for (int endIndex = startIndex; endIndex < stringLength; ++endIndex) {
27 // Check if substring from startIndex to endIndex has equal digit frequency
28 if (hasEqualDigitFrequency(startIndex, endIndex, prefixSum)) {
29 // Add the valid substring to set (automatically handles duplicates)
30 uniqueSubstrings.add(s.substring(startIndex, endIndex + 1));
31 }
32 }
33 }
34
35 // Return count of unique substrings with equal digit frequency
36 return uniqueSubstrings.size();
37 }
38
39 /**
40 * Checks if all digits in the substring from startIndex to endIndex
41 * have the same frequency (count of occurrences)
42 *
43 * @param startIndex starting index of substring (inclusive)
44 * @param endIndex ending index of substring (inclusive)
45 * @param prefixSum prefix sum array containing digit counts
46 * @return true if all present digits have equal frequency, false otherwise
47 */
48 private boolean hasEqualDigitFrequency(int startIndex, int endIndex, int[][] prefixSum) {
49 // Set to store all unique frequencies of digits that appear in substring
50 Set<Integer> frequencies = new HashSet<>();
51
52 // Check frequency of each digit (0-9) in the substring
53 for (int digit = 0; digit < 10; ++digit) {
54 // Calculate count of this digit in substring using prefix sum
55 int digitCount = prefixSum[endIndex + 1][digit] - prefixSum[startIndex][digit];
56
57 // If this digit appears in substring, add its frequency to set
58 if (digitCount > 0) {
59 frequencies.add(digitCount);
60 }
61
62 // If we have more than one unique frequency, digits don't have equal frequency
63 if (frequencies.size() > 1) {
64 return false;
65 }
66 }
67
68 // All present digits have the same frequency
69 return true;
70 }
71}
72
1class Solution {
2public:
3 int equalDigitFrequency(string s) {
4 int stringLength = s.length();
5
6 // Create a 2D prefix sum array to store cumulative count of each digit (0-9)
7 // prefixSum[i][d] represents count of digit 'd' from index 0 to i-1
8 vector<vector<int>> prefixSum(stringLength + 1, vector<int>(10, 0));
9
10 // Build the prefix sum array
11 for (int i = 0; i < stringLength; ++i) {
12 // Copy all previous counts to current position
13 for (int digit = 0; digit < 10; ++digit) {
14 prefixSum[i + 1][digit] = prefixSum[i][digit];
15 }
16
17 // Increment count for current digit
18 int currentDigit = s[i] - '0';
19 ++prefixSum[i + 1][currentDigit];
20 }
21
22 // Use an unordered_set to store unique substrings that have equal digit frequency
23 unordered_set<string> uniqueSubstrings;
24
25 // Check all possible substrings
26 for (int startIndex = 0; startIndex < stringLength; ++startIndex) {
27 for (int endIndex = startIndex; endIndex < stringLength; ++endIndex) {
28 // Check if substring from startIndex to endIndex has equal digit frequency
29 if (hasEqualDigitFrequency(startIndex, endIndex, prefixSum)) {
30 // Add the valid substring to set (automatically handles duplicates)
31 uniqueSubstrings.insert(s.substr(startIndex, endIndex - startIndex + 1));
32 }
33 }
34 }
35
36 // Return count of unique substrings with equal digit frequency
37 return uniqueSubstrings.size();
38 }
39
40private:
41 /**
42 * Checks if all digits in the substring from startIndex to endIndex
43 * have the same frequency (count of occurrences)
44 *
45 * @param startIndex starting index of substring (inclusive)
46 * @param endIndex ending index of substring (inclusive)
47 * @param prefixSum prefix sum array containing digit counts
48 * @return true if all present digits have equal frequency, false otherwise
49 */
50 bool hasEqualDigitFrequency(int startIndex, int endIndex, const vector<vector<int>>& prefixSum) {
51 // Set to store all unique frequencies of digits that appear in substring
52 unordered_set<int> frequencies;
53
54 // Check frequency of each digit (0-9) in the substring
55 for (int digit = 0; digit < 10; ++digit) {
56 // Calculate count of this digit in substring using prefix sum
57 int digitCount = prefixSum[endIndex + 1][digit] - prefixSum[startIndex][digit];
58
59 // If this digit appears in substring, add its frequency to set
60 if (digitCount > 0) {
61 frequencies.insert(digitCount);
62 }
63
64 // If we have more than one unique frequency, digits don't have equal frequency
65 if (frequencies.size() > 1) {
66 return false;
67 }
68 }
69
70 // All present digits have the same frequency
71 return true;
72 }
73};
74
1function equalDigitFrequency(s: string): number {
2 const stringLength = s.length;
3
4 // Create a 2D prefix sum array to store cumulative count of each digit (0-9)
5 // prefixSum[i][d] represents count of digit 'd' from index 0 to i-1
6 const prefixSum: number[][] = Array(stringLength + 1)
7 .fill(null)
8 .map(() => Array(10).fill(0));
9
10 // Build the prefix sum array
11 for (let i = 0; i < stringLength; i++) {
12 // Increment count for current digit
13 const currentDigit = parseInt(s.charAt(i));
14 prefixSum[i + 1][currentDigit]++;
15
16 // Copy all previous counts to current position
17 for (let digit = 0; digit < 10; digit++) {
18 prefixSum[i + 1][digit] += prefixSum[i][digit];
19 }
20 }
21
22 // Use a set to store unique substrings that have equal digit frequency
23 const uniqueSubstrings = new Set<string>();
24
25 // Check all possible substrings
26 for (let startIndex = 0; startIndex < stringLength; startIndex++) {
27 for (let endIndex = startIndex; endIndex < stringLength; endIndex++) {
28 // Check if substring from startIndex to endIndex has equal digit frequency
29 if (hasEqualDigitFrequency(startIndex, endIndex, prefixSum)) {
30 // Add the valid substring to set (automatically handles duplicates)
31 uniqueSubstrings.add(s.substring(startIndex, endIndex + 1));
32 }
33 }
34 }
35
36 // Return count of unique substrings with equal digit frequency
37 return uniqueSubstrings.size;
38}
39
40/**
41 * Checks if all digits in the substring from startIndex to endIndex
42 * have the same frequency (count of occurrences)
43 *
44 * @param startIndex - Starting index of substring (inclusive)
45 * @param endIndex - Ending index of substring (inclusive)
46 * @param prefixSum - Prefix sum array containing digit counts
47 * @returns true if all present digits have equal frequency, false otherwise
48 */
49function hasEqualDigitFrequency(
50 startIndex: number,
51 endIndex: number,
52 prefixSum: number[][]
53): boolean {
54 // Set to store all unique frequencies of digits that appear in substring
55 const frequencies = new Set<number>();
56
57 // Check frequency of each digit (0-9) in the substring
58 for (let digit = 0; digit < 10; digit++) {
59 // Calculate count of this digit in substring using prefix sum
60 const digitCount = prefixSum[endIndex + 1][digit] - prefixSum[startIndex][digit];
61
62 // If this digit appears in substring, add its frequency to set
63 if (digitCount > 0) {
64 frequencies.add(digitCount);
65 }
66
67 // If we have more than one unique frequency, digits don't have equal frequency
68 if (frequencies.size > 1) {
69 return false;
70 }
71 }
72
73 // All present digits have the same frequency
74 return true;
75}
76
Time and Space Complexity
Time Complexity: O(n³)
The time complexity breaks down as follows:
- Building the prefix sum array takes
O(n × 10) = O(n)
time, where we iterate through each character and update 10 digit counters. - The main nested loops iterate through all possible substrings:
O(n²)
pairs of(i, j)
wherei
ranges from0
ton-1
andj
ranges fromi
ton-1
. - For each substring pair
(i, j)
, thecheck
function is called, which iterates through 10 digits to verify if all non-zero digit frequencies are equal. This takesO(10) = O(1)
time. - For each valid substring that passes the check, we create the substring
s[i:j+1]
which takesO(j - i + 1)
time in the worst case, which can beO(n)
. - Therefore, the overall time complexity is
O(n²) × O(n) = O(n³)
.
Space Complexity: O(n² + n)
The space complexity consists of:
- The prefix sum array
presum
usesO((n + 1) × 10) = O(n)
space. - The set
vis
stores unique substrings. In the worst case where all substrings have equal digit frequency, there areO(n²)
possible substrings, and each substring can take up toO(n)
space. - However, since we're storing unique substrings in a set and the total number of unique substrings is bounded by
O(n²)
, the space forvis
isO(n²)
in total (considering amortized storage). - Therefore, the overall space complexity is
O(n² + n) = O(n²)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Inefficient Substring Frequency Calculation
A common mistake is recalculating digit frequencies from scratch for each substring instead of using prefix sums:
Incorrect/Inefficient Approach:
def check_substring(s, start, end):
freq = {}
# Recalculating frequencies for every substring - O(n) per check!
for i in range(start, end + 1):
digit = s[i]
freq[digit] = freq.get(digit, 0) + 1
frequencies = set(freq.values())
return len(frequencies) <= 1
Why it's problematic: This approach has O(n³) time complexity since we iterate through each substring to count frequencies, making it inefficient for large inputs.
Solution: Use prefix sums to calculate any substring's digit frequencies in O(1) time after O(n) preprocessing.
2. Forgetting to Handle Single Character Substrings
Some implementations might incorrectly assume that single characters don't count as valid substrings:
Incorrect Logic:
# Wrong: skipping single characters
for start in range(n):
for end in range(start + 1, n): # Missing single character case!
if has_equal_frequency(start, end):
unique_substrings.add(s[start:end + 1])
Solution: Ensure the inner loop starts from start
not start + 1
:
for start in range(n):
for end in range(start, n): # Includes single characters
if has_equal_frequency(start, end):
unique_substrings.add(s[start:end + 1])
3. Not Deduplicating Substrings Properly
Using a list instead of a set to store valid substrings leads to counting duplicates:
Incorrect:
valid_substrings = [] # List allows duplicates
for start in range(n):
for end in range(start, n):
if has_equal_frequency(start, end):
valid_substrings.append(s[start:end + 1])
return len(valid_substrings) # May count "111" multiple times
Solution: Use a set to automatically handle deduplication:
unique_substrings = set() # Set prevents duplicates
4. Off-by-One Errors in Prefix Sum Calculation
A subtle but critical error is misaligning indices when building or using the prefix sum array:
Incorrect:
# Wrong: Using wrong indices count = prefix_sum[end][digit] - prefix_sum[start - 1][digit] # Dangerous if start = 0
Correct:
# Correct: prefix_sum[i] contains counts for s[0:i] count = prefix_sum[end + 1][digit] - prefix_sum[start][digit]
5. Early Termination Logic Error
Incorrectly implementing the early termination in the frequency check:
Incorrect:
def has_equal_frequency(start, end):
frequency_set = set()
for digit in range(10):
count = prefix_sum[end + 1][digit] - prefix_sum[start][digit]
if count > 0:
frequency_set.add(count)
# Wrong: checking after the loop misses optimization opportunity
return len(frequency_set) <= 1
Better:
def has_equal_frequency(start, end):
frequency_set = set()
for digit in range(10):
count = prefix_sum[end + 1][digit] - prefix_sum[start][digit]
if count > 0:
frequency_set.add(count)
# Early termination when we know it's invalid
if len(frequency_set) > 1:
return False
return True
This early termination can significantly improve performance, especially for longer strings where invalid substrings are detected early.
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!