2416. Sum of Prefix Scores of Strings
Problem Description
You are given an array words
containing n
non-empty strings. Your task is to calculate the prefix scores for each word in the array.
The score of a string term
is defined as the count of how many strings in words
have term
as their prefix. A string term
is a prefix of another string if the other string starts with term
.
For example, given words = ["a", "ab", "abc", "cab"]
:
- The score of
"ab"
is2
because it's a prefix of both"ab"
and"abc"
- The score of
"a"
is3
because it's a prefix of"a"
,"ab"
, and"abc"
- The score of
"ca"
is1
because it's only a prefix of"cab"
For each word in the array, you need to calculate the sum of scores for all its non-empty prefixes. Remember that a string is considered a prefix of itself.
For instance, if we have the word "abc"
:
- Its prefixes are:
"a"
,"ab"
,"abc"
- You would calculate: score(
"a"
) + score("ab"
) + score("abc"
)
The output should be an array answer
of size n
, where answer[i]
represents the sum of scores of all non-empty prefixes of words[i]
.
Intuition
The key insight is recognizing that we need to count how many times each prefix appears across all words. If we were to use a brute force approach, for each word, we'd generate all its prefixes and then scan through all words to count matches. This would be inefficient with a time complexity of O(nΒ² Γ mΒ²) where n is the number of words and m is the average length.
The pattern here suggests using a Trie (prefix tree) because:
- We're dealing with prefixes, which is exactly what Tries are designed for
- We need to count occurrences of each prefix efficiently
- Multiple words may share common prefixes, and we can avoid redundant computations
Think about how we process the word "abc"
- we need to know how many words start with "a"
, how many start with "ab"
, and how many start with "abc"
. If we build a Trie from all words first, each node in the path from root to a letter represents a prefix, and we can store a counter at each node.
When we insert a word like "abc"
into the Trie:
- We traverse through
'a'
β'ab'
β'abc'
- At each node along this path, we increment a counter
- This counter tells us exactly how many words have passed through this node (i.e., how many words have this prefix)
Later, when we search for the sum of prefix scores for "abc"
:
- We traverse the same path
'a'
β'ab'
β'abc'
- At each node, we add its counter value to our sum
- The sum of all these counters gives us the total prefix score
This approach reduces our time complexity to O(n Γ m) for building the Trie and O(n Γ m) for querying, making it much more efficient than the brute force approach.
Learn more about Trie patterns.
Solution Approach
The solution uses a Trie data structure to efficiently store and count all prefixes. Let's walk through the implementation step by step:
Trie Node Structure
The [Trie](/problems/trie_intro)
class has two key attributes:
children
: An array of size 26 (for lowercase English letters a-z), wherechildren[i]
points to the child node for character'a' + i
cnt
: A counter that tracks how many words pass through this node (i.e., how many words have the prefix represented by the path from root to this node)
Insert Method
The insert
method adds a word to the Trie:
- Start from the root node
- For each character
c
in the word:- Calculate the index:
idx = ord(c) - ord("a")
(converts character to 0-25 range) - If no child exists at this index, create a new Trie node
- Move to the child node
- Increment the counter
cnt
at this node by 1
- Calculate the index:
For example, inserting "abc"
:
- At node for
'a'
: incrementcnt
(counts words with prefix"a"
) - At node for
'b'
(after'a'
): incrementcnt
(counts words with prefix"ab"
) - At node for
'c'
(after'ab'
): incrementcnt
(counts words with prefix"abc"
)
Search Method
The search
method calculates the sum of prefix scores for a word:
- Start from the root node with
ans = 0
- For each character
c
in the word:- Calculate the index:
idx = ord(c) - ord("a")
- If no child exists at this index, return the current sum (no more prefixes exist)
- Move to the child node
- Add the node's
cnt
value toans
- Calculate the index:
- Return the total sum
Main Solution
The sumPrefixScores
method orchestrates the process:
- Create an empty Trie
- Insert all words into the Trie (this builds up the counter values at each node)
- For each word, call
search
to get the sum of its prefix scores - Return the list of sums
Time and Space Complexity
-
Time Complexity: O(n Γ m), where n is the number of words and m is the average length of words
- Inserting all words: O(n Γ m)
- Searching all words: O(n Γ m)
-
Space Complexity: O(n Γ m) in the worst case, for storing all unique prefixes in the Trie
The __slots__
optimization in Python is used to reduce memory overhead by preventing the creation of __dict__
for each instance, making the Trie more memory-efficient.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with words = ["ab", "abc", "a"]
to illustrate how the Trie solution works.
Step 1: Build the Trie by inserting all words
Insert "ab":
Root β a (cnt=1) β b (cnt=1)
Insert "abc":
Root β a (cnt=2) β b (cnt=2) β c (cnt=1)
- When inserting "abc", we traverse through 'a' (increment cnt from 1 to 2), then 'b' (increment cnt from 1 to 2), then create and visit 'c' (cnt=1)
Insert "a":
Root β a (cnt=3) β b (cnt=2) β c (cnt=1)
- When inserting "a", we only visit node 'a' and increment its cnt from 2 to 3
Step 2: Calculate prefix scores for each word
For word "ab":
- Visit 'a': add cnt=3 to sum (3 words have prefix "a": "ab", "abc", "a")
- Visit 'b': add cnt=2 to sum (2 words have prefix "ab": "ab", "abc")
- Total: 3 + 2 = 5
For word "abc":
- Visit 'a': add cnt=3 to sum
- Visit 'b': add cnt=2 to sum
- Visit 'c': add cnt=1 to sum (1 word has prefix "abc": "abc")
- Total: 3 + 2 + 1 = 6
For word "a":
- Visit 'a': add cnt=3 to sum
- Total: 3
Final Answer: [5, 6, 3]
The key insight is that each node's counter tells us exactly how many words in our array have the prefix represented by the path from root to that node. By summing these counters along the path of each word, we get the total prefix score efficiently.
Solution Implementation
1from typing import List, Optional
2
3
4class Trie:
5 """
6 A Trie (prefix tree) data structure for efficient prefix operations.
7 Each node stores the count of words passing through it.
8 """
9 __slots__ = ("children", "count")
10
11 def __init__(self):
12 """Initialize a Trie node with 26 children (for lowercase letters) and count of 0."""
13 self.children: List[Optional['Trie']] = [None] * 26 # Array for 26 lowercase letters
14 self.count: int = 0 # Number of words passing through this node
15
16 def insert(self, word: str) -> None:
17 """
18 Insert a word into the Trie and increment count for each node in the path.
19
20 Args:
21 word: The word to insert into the Trie
22 """
23 current_node = self
24
25 for character in word:
26 # Calculate the index for this character (0-25 for 'a'-'z')
27 char_index = ord(character) - ord('a')
28
29 # Create a new node if it doesn't exist
30 if current_node.children[char_index] is None:
31 current_node.children[char_index] = Trie()
32
33 # Move to the child node
34 current_node = current_node.children[char_index]
35 # Increment the count of words passing through this node
36 current_node.count += 1
37
38 def search(self, word: str) -> int:
39 """
40 Calculate the sum of prefix scores for a word.
41 The prefix score is the sum of counts for all prefixes of the word.
42
43 Args:
44 word: The word to calculate prefix scores for
45
46 Returns:
47 The sum of all prefix scores
48 """
49 current_node = self
50 total_score = 0
51
52 for character in word:
53 # Calculate the index for this character
54 char_index = ord(character) - ord('a')
55
56 # If the path doesn't exist, return current score
57 if current_node.children[char_index] is None:
58 return total_score
59
60 # Move to the child node
61 current_node = current_node.children[char_index]
62 # Add the count of words with this prefix to the total score
63 total_score += current_node.count
64
65 return total_score
66
67
68class Solution:
69 """
70 Solution class for calculating prefix scores of words.
71 For each word, the prefix score is the sum of counts of all its prefixes
72 across all words in the list.
73 """
74
75 def sumPrefixScores(self, words: List[str]) -> List[int]:
76 """
77 Calculate the sum of prefix scores for each word in the list.
78
79 Args:
80 words: List of words to process
81
82 Returns:
83 List of prefix scores corresponding to each word
84 """
85 # Build the Trie with all words
86 trie = Trie()
87 for word in words:
88 trie.insert(word)
89
90 # Calculate and return prefix scores for each word
91 return [trie.search(word) for word in words]
92
1/**
2 * Trie (Prefix Tree) data structure for efficient prefix counting
3 */
4class Trie {
5 // Array to store 26 child nodes (one for each lowercase letter a-z)
6 private Trie[] children = new Trie[26];
7 // Counter to track how many words pass through this node
8 private int count;
9
10 /**
11 * Inserts a word into the trie and increments counters along the path
12 * @param word The word to insert into the trie
13 */
14 public void insert(String word) {
15 Trie currentNode = this;
16
17 // Traverse each character in the word
18 for (char character : word.toCharArray()) {
19 // Convert character to index (0-25) by subtracting 'a'
20 int index = character - 'a';
21
22 // Create new node if path doesn't exist
23 if (currentNode.children[index] == null) {
24 currentNode.children[index] = new Trie();
25 }
26
27 // Move to the child node
28 currentNode = currentNode.children[index];
29 // Increment the count for this prefix
30 currentNode.count++;
31 }
32 }
33
34 /**
35 * Searches for a word in the trie and calculates the sum of prefix scores
36 * @param word The word to search for
37 * @return The sum of counts for all prefixes of the word
38 */
39 public int search(String word) {
40 Trie currentNode = this;
41 int totalScore = 0;
42
43 // Traverse each character in the word
44 for (char character : word.toCharArray()) {
45 // Convert character to index (0-25) by subtracting 'a'
46 int index = character - 'a';
47
48 // If path doesn't exist, return accumulated score
49 if (currentNode.children[index] == null) {
50 return totalScore;
51 }
52
53 // Move to the child node
54 currentNode = currentNode.children[index];
55 // Add the count of words with this prefix to total score
56 totalScore += currentNode.count;
57 }
58
59 return totalScore;
60 }
61}
62
63/**
64 * Solution class for calculating sum of prefix scores for each word
65 */
66class Solution {
67 /**
68 * Calculates the sum of prefix scores for each word in the array
69 * The prefix score is the number of words that have the same prefix
70 * @param words Array of words to process
71 * @return Array where each element is the sum of prefix scores for the corresponding word
72 */
73 public int[] sumPrefixScores(String[] words) {
74 // Initialize the trie data structure
75 Trie trie = new Trie();
76
77 // Insert all words into the trie
78 for (String word : words) {
79 trie.insert(word);
80 }
81
82 // Create result array to store prefix scores
83 int[] result = new int[words.length];
84
85 // Calculate the sum of prefix scores for each word
86 for (int i = 0; i < words.length; i++) {
87 result[i] = trie.search(words[i]);
88 }
89
90 return result;
91 }
92}
93
1class Trie {
2private:
3 // Array to store 26 children nodes (one for each lowercase letter)
4 Trie* children[26]{};
5 // Counter to track how many words pass through this node
6 int count = 0;
7
8public:
9 /**
10 * Inserts a word into the trie and increments counters along the path
11 * @param word The word to insert into the trie
12 */
13 void insert(string& word) {
14 Trie* currentNode = this;
15
16 // Traverse each character in the word
17 for (char ch : word) {
18 int index = ch - 'a'; // Convert character to array index (0-25)
19
20 // Create new node if path doesn't exist
21 if (!currentNode->children[index]) {
22 currentNode->children[index] = new Trie();
23 }
24
25 // Move to child node and increment its counter
26 currentNode = currentNode->children[index];
27 ++currentNode->count;
28 }
29 }
30
31 /**
32 * Searches for a word in the trie and returns the sum of prefix scores
33 * @param word The word to search for
34 * @return The sum of counts for all prefixes of the word
35 */
36 int search(string& word) {
37 Trie* currentNode = this;
38 int totalScore = 0;
39
40 // Traverse each character and accumulate prefix counts
41 for (char ch : word) {
42 int index = ch - 'a'; // Convert character to array index
43
44 // If path doesn't exist, return accumulated score
45 if (!currentNode->children[index]) {
46 return totalScore;
47 }
48
49 // Move to child node and add its count to total
50 currentNode = currentNode->children[index];
51 totalScore += currentNode->count;
52 }
53
54 return totalScore;
55 }
56};
57
58class Solution {
59public:
60 /**
61 * Calculates the sum of prefix scores for each word in the array
62 * The prefix score is the number of words that have each prefix
63 * @param words Vector of words to process
64 * @return Vector containing the sum of prefix scores for each word
65 */
66 vector<int> sumPrefixScores(vector<string>& words) {
67 // Build trie with all words
68 Trie* trie = new Trie();
69 for (auto& word : words) {
70 trie->insert(word);
71 }
72
73 // Calculate prefix score sum for each word
74 vector<int> result;
75 for (auto& word : words) {
76 result.push_back(trie->search(word));
77 }
78
79 return result;
80 }
81};
82
1// Define the TrieNode structure with children array and count
2interface TrieNode {
3 children: (TrieNode | null)[];
4 count: number;
5}
6
7// Create a new TrieNode
8function createTrieNode(): TrieNode {
9 return {
10 children: new Array(26).fill(null),
11 count: 0
12 };
13}
14
15// Insert a word into the trie and increment count for each prefix
16function insertWord(root: TrieNode, word: string): void {
17 let currentNode = root;
18
19 for (const char of word) {
20 // Calculate the index for the character (0-25 for a-z)
21 const charIndex = char.charCodeAt(0) - 'a'.charCodeAt(0);
22
23 // Create a new node if it doesn't exist
24 if (!currentNode.children[charIndex]) {
25 currentNode.children[charIndex] = createTrieNode();
26 }
27
28 // Move to the child node and increment its count
29 currentNode = currentNode.children[charIndex]!;
30 currentNode.count++;
31 }
32}
33
34// Search for a word and calculate the sum of prefix scores
35function searchWord(root: TrieNode, word: string): number {
36 let currentNode = root;
37 let prefixScoreSum = 0;
38
39 for (const char of word) {
40 // Calculate the index for the character (0-25 for a-z)
41 const charIndex = char.charCodeAt(0) - 'a'.charCodeAt(0);
42
43 // If the path doesn't exist, return the accumulated sum
44 if (!currentNode.children[charIndex]) {
45 return prefixScoreSum;
46 }
47
48 // Move to the child node and add its count to the sum
49 currentNode = currentNode.children[charIndex]!;
50 prefixScoreSum += currentNode.count;
51 }
52
53 return prefixScoreSum;
54}
55
56// Calculate the sum of prefix scores for each word in the array
57function sumPrefixScores(words: string[]): number[] {
58 // Initialize the trie root
59 const trieRoot = createTrieNode();
60
61 // Insert all words into the trie
62 for (const word of words) {
63 insertWord(trieRoot, word);
64 }
65
66 // Calculate and return the prefix score sum for each word
67 return words.map(word => searchWord(trieRoot, word));
68}
69
Time and Space Complexity
Time Complexity: O(L)
where L
is the total length of all strings in the input array.
- The
insert
method iterates through each character of a word, performing constant-time operations for each character. For a single word of lengthm
, this takesO(m)
time. - Inserting all words takes
O(L)
time in total, whereL = sum of lengths of all words
. - The
search
method also iterates through each character of a word, performing constant-time operations for each character. For a single word of lengthm
, this takesO(m)
time. - Searching all words takes
O(L)
time in total. - Overall time complexity:
O(L) + O(L) = O(L)
.
Space Complexity: O(L)
where L
is the total length of all strings in the input array.
- Each character in the input can potentially create a new Trie node.
- In the worst case, when all words have completely different prefixes, we create a new node for each character across all words.
- Each Trie node stores an array of 26 pointers (for 26 lowercase letters) and a counter variable.
- The maximum number of nodes created is bounded by the total number of characters
L
. - Therefore, the space complexity is
O(L Γ 26) = O(L)
(since 26 is a constant).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Count the Word Itself as Its Own Prefix
A critical mistake is not recognizing that every word is a prefix of itself. Some implementations might stop counting before the last character or forget to include the full word in the prefix score calculation.
Incorrect Implementation Example:
def search(self, word: str) -> int:
current_node = self
total_score = 0
# Wrong: stops before the last character
for i in range(len(word) - 1): # Missing the last character!
char_index = ord(word[i]) - ord('a')
if current_node.children[char_index] is None:
return total_score
current_node = current_node.children[char_index]
total_score += current_node.count
return total_score
Solution: Ensure the loop iterates through ALL characters of the word, including the last one.
2. Not Handling Empty Strings or Edge Cases
While the problem states "non-empty strings," defensive programming suggests checking for edge cases.
Potential Issue:
# If words could be empty or None words = ["", "a", "ab"] # Empty string could cause issues
Solution: Add validation if needed:
def sumPrefixScores(self, words: List[str]) -> List[int]:
# Filter out empty strings if they might exist
words = [w for w in words if w]
trie = Trie()
for word in words:
trie.insert(word)
return [trie.search(word) for word in words]
3. Memory Inefficiency - Creating All 26 Children Immediately
The current implementation creates an array of 26 None values for every node, even if most won't be used. This can waste significant memory for sparse tries.
Memory-Efficient Alternative:
class Trie:
def __init__(self):
self.children = {} # Use dictionary instead of array
self.count = 0
def insert(self, word: str) -> None:
current_node = self
for character in word:
if character not in current_node.children:
current_node.children[character] = Trie()
current_node = current_node.children[character]
current_node.count += 1
def search(self, word: str) -> int:
current_node = self
total_score = 0
for character in word:
if character not in current_node.children:
return total_score
current_node = current_node.children[character]
total_score += current_node.count
return total_score
4. Incorrect Count Placement
A common mistake is incrementing the count at the wrong node level or forgetting to increment it during insertion.
Incorrect Example:
def insert(self, word: str) -> None:
current_node = self
for character in word:
char_index = ord(character) - ord('a')
if current_node.children[char_index] is None:
current_node.children[char_index] = Trie()
# Wrong: incrementing before moving to child
current_node.count += 1 # This counts at the parent level!
current_node = current_node.children[char_index]
Solution: Always increment the count AFTER moving to the child node, as shown in the correct implementation.
5. Character Range Assumptions
The code assumes all characters are lowercase letters ('a' to 'z'). If the input contains uppercase letters, numbers, or special characters, the program will crash or produce incorrect results.
Problem Example:
words = ["ABC", "123", "a-b"] # Will cause IndexError or incorrect behavior
Solution: Either validate input or handle a broader character range:
def insert(self, word: str) -> None:
current_node = self
for character in word.lower(): # Convert to lowercase
if not character.isalpha(): # Skip non-alphabetic characters
continue
char_index = ord(character) - ord('a')
# ... rest of the code
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
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!