3045. Count Prefix and Suffix Pairs II
Problem Description
You are given a 0-indexed array of strings called words.
The problem asks you to find pairs of strings where one string is both a prefix AND a suffix of another string.
Specifically, you need to count how many pairs of indices (i, j) exist where:
i < j(the first index must be smaller than the second)words[i]is both a prefix and a suffix ofwords[j]
For example:
"aba"is both a prefix and suffix of"ababa"(starts with "aba" and ends with "aba") → this would count as a valid pair"abc"is only a prefix of"abcd"but not a suffix → this would NOT count
The solution uses a Trie data structure with a clever approach: instead of storing single characters, it stores character pairs. For each string s, it creates pairs by combining the character at position i from the start with the character at position i from the end: (s[i], s[m-i-1]).
When a string is both a prefix and suffix of another string, their character pairs will match in this special pairing scheme. The algorithm:
- For each word, creates these character pairs
- Traverses the trie following these pairs
- At each node, adds the count of strings that have been inserted at that position (these represent valid prefix-suffix matches)
- After traversing, increments the count at the final node
The time complexity is O(n * m) where n is the number of words and m is the average length of words.
Intuition
The key insight is recognizing that when a string is both a prefix and suffix of another string, there's a special symmetry we can exploit.
Let's think about what it means for string A to be both a prefix and suffix of string B:
Amust match the beginning ofBAmust also match the end ofB
For example, if A = "aba" and B = "ababa":
- The first 3 characters of
Bare"aba"(prefix match) - The last 3 characters of
Bare"aba"(suffix match)
Now here's the clever observation: if we look at the characters of A from both ends simultaneously, they must match the corresponding positions in B from both ends. Specifically:
- The 1st character of
Amust equal the 1st character ofBAND - The last character of
Amust equal the last character ofB - The 2nd character of
Amust equal the 2nd character ofBAND - The 2nd-to-last character of
Amust equal the 2nd-to-last character ofB - And so on...
This gives us the idea to represent each string as a sequence of character pairs: (first_char, last_char), (second_char, second_to_last_char), etc.
By storing these pairs in a trie structure, we can efficiently check if one string's pair sequence is a prefix of another string's pair sequence. When string A's pair sequence is a prefix of string B's pair sequence, it means A is both a prefix and suffix of B.
The beauty of this approach is that we only need to traverse each string once, creating these pairs and checking against previously inserted strings in the trie. Each time we find a match during traversal, we know we've found a valid prefix-suffix pair.
Learn more about Trie patterns.
Solution Implementation
1class TrieNode: 2 """ 3 A node in the Trie data structure. 4 Uses __slots__ for memory optimization. 5 """ 6 __slots__ = ["children", "count"] 7 8 def __init__(self): 9 # Dictionary to store child nodes, keyed by (prefix_char, suffix_char) tuples 10 self.children = {} 11 # Count of words that end at this node 12 self.count = 0 13 14 15class Solution: 16 def countPrefixSuffixPairs(self, words: List[str]) -> int: 17 """ 18 Count the number of pairs (i, j) where i < j and words[i] is both 19 a prefix and suffix of words[j]. 20 21 The algorithm uses a Trie where each node represents a character pair 22 (prefix_char, suffix_char). By traversing with paired characters from 23 the start and end of each word simultaneously, we can efficiently check 24 if previous words are both prefixes and suffixes. 25 26 Args: 27 words: List of strings to analyze 28 29 Returns: 30 Number of valid prefix-suffix pairs 31 """ 32 result = 0 33 root = TrieNode() 34 35 # Process each word in order 36 for word in words: 37 current_node = root 38 39 # Create character pairs: (first_char, last_char), (second_char, second_last_char), etc. 40 # This allows checking prefix and suffix conditions simultaneously 41 for char_pair in zip(word, reversed(word)): 42 # Create new node if this character pair hasn't been seen at this position 43 if char_pair not in current_node.children: 44 current_node.children[char_pair] = TrieNode() 45 46 # Move to the child node corresponding to this character pair 47 current_node = current_node.children[char_pair] 48 49 # Add count of all previous words that match up to this position 50 # These words are both prefixes and suffixes of the current word 51 result += current_node.count 52 53 # Mark that this word ends at the current node 54 current_node.count += 1 55 56 return result 571class Node { 2 // Map to store child nodes, key is the hash of prefix and suffix characters 3 Map<Integer, Node> children = new HashMap<>(); 4 // Count of strings that end at this node 5 int count; 6} 7 8class Solution { 9 public long countPrefixSuffixPairs(String[] words) { 10 long answer = 0; 11 Node trieRoot = new Node(); 12 13 // Process each word in the array 14 for (String word : words) { 15 Node currentNode = trieRoot; 16 int wordLength = word.length(); 17 18 // Build trie path by pairing characters from start and end 19 for (int i = 0; i < wordLength; i++) { 20 // Create a unique key by combining: 21 // - Character at position i from the start (prefix) 22 // - Character at position i from the end (suffix) 23 // Multiply first char by 32 to avoid collisions 24 int pairKey = word.charAt(i) * 32 + word.charAt(wordLength - i - 1); 25 26 // Create new node if path doesn't exist 27 currentNode.children.putIfAbsent(pairKey, new Node()); 28 29 // Move to the child node 30 currentNode = currentNode.children.get(pairKey); 31 32 // Add count of previously seen strings with same prefix-suffix pattern 33 answer += currentNode.count; 34 } 35 36 // Increment count at the end node for this word 37 currentNode.count++; 38 } 39 40 return answer; 41 } 42} 431class TrieNode { 2public: 3 // Map to store child nodes, key is the combined hash of prefix and suffix characters 4 unordered_map<int, TrieNode*> children; 5 // Count of strings that end at this node 6 int count; 7 8 TrieNode() : count(0) {} 9}; 10 11class Solution { 12public: 13 long long countPrefixSuffixPairs(vector<string>& words) { 14 long long result = 0; 15 TrieNode* root = new TrieNode(); 16 17 // Process each word in the array 18 for (const string& word : words) { 19 TrieNode* currentNode = root; 20 int wordLength = word.length(); 21 22 // Build trie path for current word 23 // Simultaneously check prefix and suffix by combining characters 24 for (int i = 0; i < wordLength; ++i) { 25 // Create a unique key by combining: 26 // - prefix character at position i (word[i]) 27 // - suffix character at corresponding position from end (word[wordLength - i - 1]) 28 // Multiply by 32 to ensure unique hash values for different character pairs 29 int combinedKey = word[i] * 32 + word[wordLength - i - 1]; 30 31 // Create new node if path doesn't exist 32 if (currentNode->children.find(combinedKey) == currentNode->children.end()) { 33 currentNode->children[combinedKey] = new TrieNode(); 34 } 35 36 // Move to the child node 37 currentNode = currentNode->children[combinedKey]; 38 39 // Add count of all words that share this prefix-suffix pattern 40 result += currentNode->count; 41 } 42 43 // Increment count at the end node for this word 44 ++currentNode->count; 45 } 46 47 return result; 48 } 49}; 501// Global trie node structure using Map to store children 2interface TrieNode { 3 children: Map<number, TrieNode>; 4 cnt: number; 5} 6 7// Creates a new trie node 8function createNode(): TrieNode { 9 return { 10 children: new Map<number, TrieNode>(), 11 cnt: 0 12 }; 13} 14 15/** 16 * Counts the number of prefix-suffix pairs in the given array of words. 17 * Uses a trie structure where each node represents a combination of prefix and suffix characters. 18 * 19 * @param words - Array of strings to process 20 * @returns The total count of prefix-suffix pairs 21 */ 22function countPrefixSuffixPairs(words: string[]): number { 23 let totalCount: number = 0; 24 const rootNode: TrieNode = createNode(); 25 26 // Process each word in the array 27 for (const word of words) { 28 let currentNode: TrieNode = rootNode; 29 const wordLength: number = word.length; 30 31 // Build the trie path for current word 32 for (let index: number = 0; index < wordLength; index++) { 33 // Create a unique key combining the character at position i from start 34 // and the character at position i from end 35 const prefixChar: number = word.charCodeAt(index); 36 const suffixChar: number = word.charCodeAt(wordLength - index - 1); 37 const combinedKey: number = prefixChar * 32 + suffixChar; 38 39 // Create new node if path doesn't exist 40 if (!currentNode.children.has(combinedKey)) { 41 currentNode.children.set(combinedKey, createNode()); 42 } 43 44 // Move to the next node in the trie 45 currentNode = currentNode.children.get(combinedKey)!; 46 47 // Add count of words that have reached this node before 48 totalCount += currentNode.cnt; 49 } 50 51 // Increment count for the current word's complete path 52 currentNode.cnt++; 53 } 54 55 return totalCount; 56} 57Solution Approach
The solution uses a Trie data structure to efficiently store and search for character pair patterns.
Data Structure Setup:
- We define a
Nodeclass with two attributes:children: a dictionary to store child nodes, where keys are character pairs(char_from_start, char_from_end)cnt: a counter to track how many strings end at this node
Algorithm Steps:
-
Initialize an empty trie and a counter
ans = 0 -
Process each word in the array:
- Start from the root of the trie
- Create character pairs using
zip(s, reversed(s)):- This pairs up
s[0]withs[n-1],s[1]withs[n-2], and so on - For example,
"aba"becomes pairs:('a','a'),('b','b'),('a','a')
- This pairs up
-
Traverse and build the trie for each character pair:
- If the pair doesn't exist as a child, create a new node
- Move to the child node corresponding to this pair
- Add the current node's
cntto our answer (this represents all previously inserted strings that match up to this point)
-
Mark the end of the current string:
- Increment
node.cntby 1 at the final position - This indicates that a complete string ends here
- Increment
Why this works:
- When we traverse with a new string and encounter nodes with
cnt > 0, it means there are shorter strings that are both prefixes and suffixes of the current string - The pairing mechanism
(s[i], s[m-i-1])ensures that we're checking both prefix and suffix conditions simultaneously - By accumulating the counts during traversal, we automatically count all valid
(i, j)pairs wherei < j
Time Complexity: O(n * m) where n is the number of words and m is the average length of each word.
Space Complexity: O(n * m) for storing the trie nodes.
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 words = ["a", "aba", "ababa", "aa"].
Step 1: Process "a"
- Create pairs:
zip("a", "a")→[('a', 'a')] - Start at root, create child for
('a', 'a') - No existing nodes, so
ans = 0 - Mark end: set
cnt = 1at this node
Trie state:
root → ('a','a')[cnt=1]
Step 2: Process "aba"
- Create pairs:
zip("aba", "aba")→[('a','a'), ('b','b'), ('a','a')] - Traverse from root:
- Move to
('a','a')child (exists), add itscnt=1to answer →ans = 1 - Create new child
('b','b')withcnt=0 - Create new child
('a','a')withcnt=0
- Move to
- Mark end: set
cnt = 1at final node
Trie state:
root → ('a','a')[cnt=1] → ('b','b')[cnt=0] → ('a','a')[cnt=1]
Found: "a" is both prefix and suffix of "aba" (pair (0,1))
Step 3: Process "ababa"
- Create pairs:
zip("ababa", "ababa")→[('a','a'), ('b','b'), ('a','a'), ('b','b'), ('a','a')] - Traverse from root:
- Move to
('a','a'), addcnt=1→ans = 2 - Move to
('b','b'), addcnt=0→ans = 2 - Move to
('a','a'), addcnt=1→ans = 3 - Create new child
('b','b')withcnt=0 - Create new child
('a','a')withcnt=0
- Move to
- Mark end: set
cnt = 1at final node
Found: "a" and "aba" are both prefix and suffix of "ababa" (pairs (0,2) and (1,2))
Step 4: Process "aa"
- Create pairs:
zip("aa", "aa")→[('a','a'), ('a','a')] - Traverse from root:
- Move to
('a','a'), addcnt=1→ans = 4 - Try to move to another
('a','a')child, but it doesn't exist from this node - Create new child
('a','a')withcnt=0
- Move to
- Mark end: set
cnt = 1at final node
Found: "a" is both prefix and suffix of "aa" (pair (0,3))
Final answer: 4 (pairs: (0,1), (0,2), (1,2), (0,3))
Time and Space Complexity
The time complexity is O(n × m), where n is the number of words in the array and m is the maximum length among all strings.
For each word in words (n iterations), the algorithm iterates through each character of that word to create pairs of (s[i], s[-(i+1)]) using zip(s, reversed(s)). This pairing operation takes O(m) time for a word of length m. Within each character iteration, the trie operations (checking existence, creating nodes, and incrementing counters) are O(1). Therefore, the overall time complexity is O(n × m).
The space complexity is O(n × m). In the worst case, when all words have completely different prefix-suffix pairs, the trie needs to store a unique path for each word. Each word can contribute up to m nodes (one for each character position), and with n words, the maximum number of nodes in the trie is O(n × m). Each node stores a dictionary of children and a counter, but the space for these is already accounted for in the node count. The __slots__ optimization helps reduce memory overhead per node instance, but doesn't change the asymptotic complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Character Pairing Logic
Pitfall: Developers often misunderstand why we pair characters from the start and end of the string. They might think we need to check prefix and suffix separately, leading to incorrect implementations like:
# INCORRECT: Checking prefix and suffix separately def countPairs_wrong(words): result = 0 for j in range(len(words)): for i in range(j): if words[j].startswith(words[i]) and words[j].endswith(words[i]): result += 1 return result
Why it's wrong: While this brute force approach works, it defeats the purpose of using a Trie and has O(n²m) complexity.
Solution: Understand that the character pairing (s[i], s[len(s)-1-i]) simultaneously validates both conditions. When a string is both a prefix and suffix, these pairs will match perfectly.
2. Incorrect Order of Operations
Pitfall: Adding the count AFTER incrementing it, or incrementing before traversal:
# INCORRECT: Wrong order for char_pair in zip(word, reversed(word)): if char_pair not in current_node.children: current_node.children[char_pair] = TrieNode() current_node = current_node.children[char_pair] # Incrementing before accumulating - WRONG! current_node.count += 1 result += current_node.count # This includes the current word itself
Solution: Always accumulate the count BEFORE incrementing. The correct order is:
- Traverse and accumulate existing counts
- Only increment count after full traversal
3. Handling Edge Cases Incorrectly
Pitfall: Not considering that a word cannot be a prefix-suffix pair with itself:
# INCORRECT: Might count self-pairs if not careful if word in previous_words: # Wrong approach result += 1
Solution: The algorithm naturally handles this by processing words in order and only counting previously inserted words (those with smaller indices).
4. Memory Inefficiency with Character Pairs
Pitfall: Storing character pairs inefficiently or creating unnecessary objects:
# INEFFICIENT: Creating new strings for pairs char_pair = word[i] + word[-(i+1)] # Creates new string objects
Solution: Use tuples for character pairs as they are immutable and hashable:
char_pair = (word[i], word[-(i+1)]) # Efficient tuple # Or better yet, use zip with reversed: for char_pair in zip(word, reversed(word)):
5. Forgetting to Initialize the Trie Root
Pitfall: Reusing the same root for multiple test cases or forgetting to initialize:
# INCORRECT: Using class variable (shared across instances) class Solution: root = TrieNode() # This is shared! def countPrefixSuffixPairs(self, words): # Will have stale data from previous calls
Solution: Always create a fresh Trie root for each function call:
def countPrefixSuffixPairs(self, words): root = TrieNode() # Fresh root for each call
You are given an array of intervals where intervals[i] = [start_i, end_i] represent the start and end of the ith interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!