2950. Number of Divisible Substrings 🔒
Problem Description
You are given a phone keypad mapping where each letter of the English alphabet is assigned a number from 1 to 9 based on old phone keypads:
- Number 1: maps to 'a', 'b'
- Number 2: maps to 'c', 'd', 'e'
- Number 3: maps to 'f', 'g', 'h'
- Number 4: maps to 'i', 'j', 'k'
- Number 5: maps to 'l', 'm', 'n'
- Number 6: maps to 'o', 'p', 'q'
- Number 7: maps to 'r', 's', 't'
- Number 8: maps to 'u', 'v', 'w'
- Number 9: maps to 'x', 'y', 'z'
A string is considered divisible if the sum of the mapped values of its characters is divisible by the string's length.
Given a string s
, you need to find and return the total number of divisible substrings in s
.
A substring is defined as a contiguous sequence of one or more characters within the string.
For example, if we have a substring "abc":
- 'a' maps to 1
- 'b' maps to 1
- 'c' maps to 2
- Sum = 1 + 1 + 2 = 4
- Length = 3
- Since 4 is not divisible by 3, this substring is not divisible
The task is to count all possible substrings of the given string where the sum of mapped values is divisible by the substring's length.
Intuition
The key insight is that we need to check every possible substring to determine if it's divisible. Since we need to examine all substrings anyway, a brute force approach makes sense here.
First, we need a way to quickly convert each character to its corresponding number. We can preprocess the mapping by creating a dictionary where each letter maps directly to its number value (1-9). This allows us to look up any character's value in constant time.
For checking divisibility, we need two pieces of information for each substring:
- The sum of all mapped values in the substring
- The length of the substring
The natural approach is to use nested loops - the outer loop fixes the starting position i
of a substring, and the inner loop extends the ending position j
. As we extend the substring from position i
to j
, we can maintain a running sum of the mapped values. This is efficient because instead of recalculating the entire sum for each substring, we just add the value of the new character at position j
.
For a substring from index i
to j
, the length is j - i + 1
. We check if the current sum is divisible by this length using the modulo operator: sum % (j - i + 1) == 0
.
This approach ensures we examine all possible substrings exactly once, making it both complete and efficient given the constraints. Each substring is checked in O(1)
time after the initial character mapping, resulting in an O(n²)
overall complexity where n
is the length of the input string.
Learn more about Prefix Sum patterns.
Solution Approach
The implementation follows a straightforward enumeration strategy with preprocessing for the character-to-number mapping.
Step 1: Build the Character Mapping
First, we create a mapping dictionary mp
to store which number each letter corresponds to:
- We define an array
d
where each string contains the letters mapped to positions 1-9 - We iterate through this array with enumeration starting from 1
- For each group of letters, we assign them their corresponding number value
d = ["ab", "cde", "fgh", "ijk", "lmn", "opq", "rst", "uvw", "xyz"]
mp = {}
for i, s in enumerate(d, 1):
for c in s:
mp[c] = i
This creates a dictionary where mp['a'] = 1
, mp['b'] = 1
, mp['c'] = 2
, and so on.
Step 2: Enumerate All Substrings
We use nested loops to generate all possible substrings:
- The outer loop with index
i
represents the starting position of the substring - The inner loop with index
j
represents the ending position of the substring - For each starting position
i
, we examine all substrings that start ati
and end at positions fromi
ton-1
Step 3: Calculate Sum and Check Divisibility
For each starting position i
, we maintain a running sum s
:
- When we extend the substring to include character at position
j
, we addmp[word[j]]
to the sum - The length of the current substring is
j - i + 1
- We check if
s % (j - i + 1) == 0
to determine if the substring is divisible - If divisible, we increment our answer counter
ans = 0
n = len(word)
for i in range(n):
s = 0
for j in range(i, n):
s += mp[word[j]]
ans += s % (j - i + 1) == 0
return ans
The running sum optimization is crucial here - instead of recalculating the sum for each substring from scratch, we incrementally build it by adding one character's value at a time. This reduces the time complexity from O(n³)
to O(n²)
.
The expression ans += s % (j - i + 1) == 0
is a concise way to increment the counter - it adds 1 when the condition is true (divisible) and 0 when false.
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 = "ace"
.
Step 1: Build the character mapping
- 'a' → 1 (from group "ab")
- 'c' → 2 (from group "cde")
- 'e' → 2 (from group "cde")
Step 2: Enumerate all substrings and check divisibility
Starting position i = 0
(character 'a'):
-
j = 0
: substring "a"- Sum = 1 (value of 'a')
- Length = 0 - 0 + 1 = 1
- Check: 1 % 1 = 0 ✓ (divisible)
- Count = 1
-
j = 1
: substring "ac"- Sum = 1 + 2 = 3 (adding value of 'c')
- Length = 1 - 0 + 1 = 2
- Check: 3 % 2 = 1 ✗ (not divisible)
- Count = 1
-
j = 2
: substring "ace"- Sum = 3 + 2 = 5 (adding value of 'e')
- Length = 2 - 0 + 1 = 3
- Check: 5 % 3 = 2 ✗ (not divisible)
- Count = 1
Starting position i = 1
(character 'c'):
-
j = 1
: substring "c"- Sum = 2 (value of 'c')
- Length = 1 - 1 + 1 = 1
- Check: 2 % 1 = 0 ✓ (divisible)
- Count = 2
-
j = 2
: substring "ce"- Sum = 2 + 2 = 4 (adding value of 'e')
- Length = 2 - 1 + 1 = 2
- Check: 4 % 2 = 0 ✓ (divisible)
- Count = 3
Starting position i = 2
(character 'e'):
j = 2
: substring "e"- Sum = 2 (value of 'e')
- Length = 2 - 2 + 1 = 1
- Check: 2 % 1 = 0 ✓ (divisible)
- Count = 4
Final Answer: 4 divisible substrings
The divisible substrings are: "a", "c", "ce", and "e".
Solution Implementation
1class Solution:
2 def countDivisibleSubstrings(self, word: str) -> int:
3 # Define groups of characters with their corresponding values
4 # Each group represents characters with the same numeric value
5 character_groups = ["ab", "cde", "fgh", "ijk", "lmn", "opq", "rst", "uvw", "xyz"]
6
7 # Create a mapping from each character to its numeric value
8 char_to_value = {}
9 for value, group in enumerate(character_groups, start=1):
10 for char in group:
11 char_to_value[char] = value
12
13 # Initialize counter for divisible substrings
14 count = 0
15 n = len(word)
16
17 # Iterate through all possible starting positions
18 for start in range(n):
19 substring_sum = 0
20
21 # For each starting position, check all substrings starting from that position
22 for end in range(start, n):
23 # Add the value of the current character to the running sum
24 substring_sum += char_to_value[word[end]]
25
26 # Check if the sum is divisible by the length of the current substring
27 substring_length = end - start + 1
28 if substring_sum % substring_length == 0:
29 count += 1
30
31 return count
32
1class Solution {
2 public int countDivisibleSubstrings(String word) {
3 // Define digit groups where each group maps to a digit value (1-9)
4 // Group 1: a,b -> digit 1
5 // Group 2: c,d,e -> digit 2
6 // Group 3: f,g,h -> digit 3
7 // Group 4: i,j,k -> digit 4
8 // Group 5: l,m,n -> digit 5
9 // Group 6: o,p,q -> digit 6
10 // Group 7: r,s,t -> digit 7
11 // Group 8: u,v,w -> digit 8
12 // Group 9: x,y,z -> digit 9
13 String[] digitGroups = {"ab", "cde", "fgh", "ijk", "lmn", "opq", "rst", "uvw", "xyz"};
14
15 // Create mapping array: charToDigit[i] stores the digit value for character ('a' + i)
16 int[] charToDigit = new int[26];
17
18 // Build the character to digit mapping
19 for (int groupIndex = 0; groupIndex < digitGroups.length; groupIndex++) {
20 for (char character : digitGroups[groupIndex].toCharArray()) {
21 // Map each character to its corresponding digit value (1-based indexing)
22 charToDigit[character - 'a'] = groupIndex + 1;
23 }
24 }
25
26 // Initialize count of divisible substrings
27 int divisibleSubstringCount = 0;
28 int wordLength = word.length();
29
30 // Iterate through all possible starting positions
31 for (int startIndex = 0; startIndex < wordLength; startIndex++) {
32 int digitSum = 0;
33
34 // Extend substring from startIndex to each possible ending position
35 for (int endIndex = startIndex; endIndex < wordLength; endIndex++) {
36 // Add the digit value of current character to running sum
37 digitSum += charToDigit[word.charAt(endIndex) - 'a'];
38
39 // Calculate substring length
40 int substringLength = endIndex - startIndex + 1;
41
42 // Check if sum of digits is divisible by substring length
43 if (digitSum % substringLength == 0) {
44 divisibleSubstringCount++;
45 }
46 }
47 }
48
49 return divisibleSubstringCount;
50 }
51}
52
1class Solution {
2public:
3 int countDivisibleSubstrings(string word) {
4 // Define digit groups mapping letters to digits 1-9
5 // Group 1: a,b -> 1
6 // Group 2: c,d,e -> 2
7 // Group 3: f,g,h -> 3
8 // Group 4: i,j,k -> 4
9 // Group 5: l,m,n -> 5
10 // Group 6: o,p,q -> 6
11 // Group 7: r,s,t -> 7
12 // Group 8: u,v,w -> 8
13 // Group 9: x,y,z -> 9
14 string digitGroups[9] = {"ab", "cde", "fgh", "ijk", "lmn", "opq", "rst", "uvw", "xyz"};
15
16 // Create mapping array: letterToDigit[i] stores the digit value for letter at index i
17 int letterToDigit[26] = {0};
18
19 // Build the letter to digit mapping
20 for (int groupIndex = 0; groupIndex < 9; ++groupIndex) {
21 for (char& letter : digitGroups[groupIndex]) {
22 letterToDigit[letter - 'a'] = groupIndex + 1;
23 }
24 }
25
26 int result = 0;
27 int wordLength = word.size();
28
29 // Check all possible substrings
30 for (int startIndex = 0; startIndex < wordLength; ++startIndex) {
31 int digitSum = 0;
32
33 // Extend substring from startIndex to endIndex
34 for (int endIndex = startIndex; endIndex < wordLength; ++endIndex) {
35 // Add the digit value of current character to the sum
36 digitSum += letterToDigit[word[endIndex] - 'a'];
37
38 // Check if the sum is divisible by the substring length
39 int substringLength = endIndex - startIndex + 1;
40 if (digitSum % substringLength == 0) {
41 result++;
42 }
43 }
44 }
45
46 return result;
47 }
48};
49
1/**
2 * Counts the number of substrings where the sum of mapped values is divisible by the substring length
3 * @param word - The input string to process
4 * @returns The count of valid substrings
5 */
6function countDivisibleSubstrings(word: string): number {
7 // Define groups of characters, each group maps to a value (1-9)
8 const characterGroups: string[] = ['ab', 'cde', 'fgh', 'ijk', 'lmn', 'opq', 'rst', 'uvw', 'xyz'];
9
10 // Create a mapping array where index represents character (a=0, b=1, ..., z=25)
11 // and value represents the group number (1-9)
12 const characterToGroupMap: number[] = Array(26).fill(0);
13
14 // Populate the character to group mapping
15 for (let groupIndex = 0; groupIndex < characterGroups.length; groupIndex++) {
16 for (const character of characterGroups[groupIndex]) {
17 const alphabetIndex = character.charCodeAt(0) - 'a'.charCodeAt(0);
18 characterToGroupMap[alphabetIndex] = groupIndex + 1;
19 }
20 }
21
22 const wordLength = word.length;
23 let validSubstringCount = 0;
24
25 // Iterate through all possible starting positions
26 for (let startIndex = 0; startIndex < wordLength; startIndex++) {
27 let currentSum = 0;
28
29 // Extend the substring from the current starting position
30 for (let endIndex = startIndex; endIndex < wordLength; endIndex++) {
31 // Add the mapped value of the current character to the sum
32 const currentCharIndex = word.charCodeAt(endIndex) - 'a'.charCodeAt(0);
33 currentSum += characterToGroupMap[currentCharIndex];
34
35 // Check if the sum is divisible by the substring length
36 const substringLength = endIndex - startIndex + 1;
37 if (currentSum % substringLength === 0) {
38 validSubstringCount++;
39 }
40 }
41 }
42
43 return validSubstringCount;
44}
45
Time and Space Complexity
The time complexity is O(n^2)
, where n
is the length of the string word
. This is because we have two nested loops: the outer loop iterates through each starting position i
from 0
to n-1
, and the inner loop iterates from position i
to n-1
. The total number of iterations is n + (n-1) + (n-2) + ... + 1 = n(n+1)/2
, which simplifies to O(n^2)
. All operations inside the inner loop (dictionary lookup, addition, modulo operation, and comparison) take O(1)
time.
The space complexity is O(C)
, where C
is the size of the character set. In this problem, C = 26
since we're dealing with lowercase English letters. The space is primarily used by the dictionary mp
which stores a mapping from each character to its corresponding value. The dictionary has at most 26 entries (one for each lowercase letter). The list d
and other variables use constant space, so the overall space complexity remains O(C)
or O(26)
which is effectively O(1)
constant space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Division Confusion
Pitfall: Developers might mistakenly check for integer division instead of divisibility when determining if a substring qualifies.
Incorrect approach:
# Wrong: This checks if the division result is an integer (which it always is in Python 3)
if substring_sum / substring_length == int(substring_sum / substring_length):
count += 1
Correct approach:
# Right: Use modulo operator to check divisibility if substring_sum % substring_length == 0: count += 1
2. Recalculating Sum for Each Substring
Pitfall: Computing the sum from scratch for every substring leads to unnecessary redundant calculations and O(n³) time complexity.
Inefficient approach:
for start in range(n):
for end in range(start, n):
# Recalculating sum every time - inefficient!
substring_sum = 0
for k in range(start, end + 1):
substring_sum += char_to_value[word[k]]
if substring_sum % (end - start + 1) == 0:
count += 1
Optimized approach:
for start in range(n):
substring_sum = 0 # Initialize sum once per starting position
for end in range(start, n):
# Incrementally build the sum
substring_sum += char_to_value[word[end]]
if substring_sum % (end - start + 1) == 0:
count += 1
3. Off-by-One Errors in Length Calculation
Pitfall: Incorrectly calculating the substring length, especially forgetting that array indices are 0-based.
Wrong calculation:
substring_length = end - start # Missing the +1
Correct calculation:
substring_length = end - start + 1 # Proper length calculation
4. Hardcoding Character Mappings
Pitfall: Manually creating the character-to-value mapping with individual assignments, making the code verbose and error-prone.
Error-prone approach:
char_to_value = { 'a': 1, 'b': 1, 'c': 2, 'd': 2, 'e': 2, # ... 26 manual entries - easy to make typos }
Better approach: Use the systematic pattern to generate the mapping programmatically, as shown in the solution.
5. Missing Edge Cases
Pitfall: Not considering single-character substrings or empty string inputs.
Solution: The nested loop structure naturally handles single-character substrings (when start == end
), and empty strings will simply result in 0 since the outer loop won't execute. However, it's good practice to explicitly verify these cases work correctly.
Which of the following is a min heap?
Recommended Readings
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
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
Want a Structured Path to Master System Design Too? Don’t Miss This!