2416. Sum of Prefix Scores of Strings

HardTrieArrayStringCounting
Leetcode Link

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" is 2 because it's a prefix of both "ab" and "abc"
  • The score of "a" is 3 because it's a prefix of "a", "ab", and "abc"
  • The score of "ca" is 1 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].

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. We're dealing with prefixes, which is exactly what Tries are designed for
  2. We need to count occurrences of each prefix efficiently
  3. 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 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

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), where children[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:

  1. Start from the root node
  2. 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

For example, inserting "abc":

  • At node for 'a': increment cnt (counts words with prefix "a")
  • At node for 'b' (after 'a'): increment cnt (counts words with prefix "ab")
  • At node for 'c' (after 'ab'): increment cnt (counts words with prefix "abc")

Search Method

The search method calculates the sum of prefix scores for a word:

  1. Start from the root node with ans = 0
  2. 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 to ans
  3. Return the total sum

Main Solution

The sumPrefixScores method orchestrates the process:

  1. Create an empty Trie
  2. Insert all words into the Trie (this builds up the counter values at each node)
  3. For each word, call search to get the sum of its prefix scores
  4. 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 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example 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":

Roota (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.

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 length m, this takes O(m) time.
  • Inserting all words takes O(L) time in total, where L = 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 length m, this takes O(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
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which type of traversal does breadth first search do?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More