677. Map Sum Pairs
Problem Description
You need to design a data structure called MapSum
that supports two operations:
-
Insert Operation: Store a key-value pair where the key is a string and the value is an integer. If the key already exists in the map, replace the old value with the new value.
-
Sum Operation: Given a prefix string, return the sum of all values whose keys start with that prefix.
The MapSum
class should implement:
MapSum()
: Constructor that initializes an empty MapSum objectinsert(key, val)
: Adds or updates the key-value pair in the mapsum(prefix)
: Returns the sum of all values whose keys begin with the given prefix
For example, if you insert ("apple", 3)
and ("app", 2)
, then calling sum("ap")
would return 5
because both "apple" and "app" start with "ap", and 3 + 2 = 5
.
The solution uses a combination of a hash table and a Trie data structure. The hash table d
keeps track of the current values for each key, while the Trie stores cumulative sums at each node representing prefixes. When inserting a key, the algorithm calculates the difference x
between the new value and the existing value (if any), then updates all nodes along the path in the Trie by adding this difference. This ensures that each node maintains the correct sum for its prefix. The search operation traverses the Trie following the prefix path and returns the accumulated sum stored at the final node.
Intuition
The key insight is recognizing that we need to efficiently compute sums for all keys sharing a common prefix. A naive approach would be to iterate through all stored keys and check if each starts with the given prefix, but this would be inefficient.
This naturally leads us to think about Trie (prefix tree) data structure, which is specifically designed for prefix-based operations. In a standard Trie, each node represents a character in a path from root to leaf, and complete paths form words.
However, instead of just storing whether a word exists, we can augment each Trie node to store the sum of all values whose keys pass through that node. This way, when we reach the node representing a prefix, we immediately have the sum of all keys starting with that prefix.
The challenge arises when we need to handle updates - if a key already exists and we insert it again with a new value, we need to update not just one node but all nodes along the path from root to that key. Simply adding the new value would give incorrect results.
To solve this elegantly, we maintain a separate hash table d
that tracks the current value for each key. When inserting, we calculate the difference x = newValue - oldValue
(where oldValue
is 0 if the key doesn't exist). We then traverse the Trie path and add this difference to each node's sum. This incremental update strategy ensures that:
- If it's a new key, we add the full value to each node on the path
- If it's an update, we only add the difference, effectively removing the old value and adding the new one in a single pass
This design gives us O(n)
insertion time where n
is the key length, and O(m)
query time where m
is the prefix length, making both operations very efficient.
Learn more about Trie patterns.
Solution Approach
The solution uses a combination of a hash table and a Trie to efficiently handle both insertions and prefix sum queries.
Data Structures
-
Hash Table (
d
): A dictionary that stores the current value for each key. This allows us to track what value is already associated with a key when we need to update it. -
Trie: A prefix tree where each node contains:
children
: An array of size 26 (for lowercase letters a-z) storing child nodesval
: The cumulative sum of all key-value pairs that pass through this node
Implementation Details
Trie Node Structure:
- Each node has a
children
array initialized withNone
values for all 26 letters - The
val
field stores the sum of all values whose keys include this node in their path
Insert Operation in Trie:
def insert(self, w: str, x: int):
node = self
for c in w:
idx = ord(c) - ord('a') # Convert character to index (0-25)
if node.children[idx] is None:
node.children[idx] = Trie() # Create new node if needed
node = node.children[idx]
node.val += x # Add the difference to cumulative sum
The method traverses the Trie, creating nodes as needed, and adds the value x
to each node along the path.
MapSum Insert Operation:
def insert(self, key: str, val: int) -> None:
x = val - self.d[key] # Calculate difference (0 if key doesn't exist)
self.d[key] = val # Update hash table with new value
self.tree.insert(key, x) # Insert difference into [Trie](/problems/trie_intro)
The clever part is calculating x = val - self.d[key]
. Since defaultdict(int)
returns 0 for non-existent keys:
- For new keys:
x = val - 0 = val
(adds the full value) - For existing keys:
x = newVal - oldVal
(adds only the difference)
Search Operation:
def search(self, w: str) -> int:
node = self
for c in w:
idx = ord(c) - ord('a')
if node.children[idx] is None:
return 0 # Prefix doesn't exist
node = node.children[idx]
return node.val # Return cumulative sum at prefix node
The search traverses the Trie following the prefix path. If any character in the prefix doesn't exist, it returns 0. Otherwise, it returns the val
stored at the final node, which represents the sum of all keys with this prefix.
Time Complexity
- Insert:
O(n)
wheren
is the length of the key - Sum:
O(m)
wherem
is the length of the prefix
Space Complexity
O(ALPHABET_SIZE ร N ร L)
whereALPHABET_SIZE = 26
,N
is the number of unique keys, andL
is the average length of keys
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 concrete example to illustrate how the MapSum data structure works:
Step 1: Initialize MapSum
mapSum = MapSum() - Hash table d = {} - Trie root initialized with empty children and val = 0
Step 2: Insert ("apple", 3)
- Calculate difference: x = 3 - 0 = 3 (key doesn't exist, so old value is 0) - Update hash table: d = {"apple": 3} - Insert into Trie with value 3: root | a (val=3) | p (val=3) | p (val=3) | l (val=3) | e (val=3)
Each node along the path gets val += 3.
Step 3: Insert ("app", 2)
- Calculate difference: x = 2 - 0 = 2 (new key) - Update hash table: d = {"apple": 3, "app": 2} - Insert into Trie with value 2: root | a (val=5) // was 3, now 3+2=5 | p (val=5) // was 3, now 3+2=5 | p (val=5) // was 3, now 3+2=5 | l (val=3) // only "apple" passes through here | e (val=3)
Note: The first three nodes (a-p-p) are shared by both "apple" and "app", so their values are 5 (3+2).
Step 4: Query sum("ap")
- Traverse Trie: root โ a โ p
- Return value at node 'p': 5
- This correctly returns the sum of all keys starting with "ap" (apple=3 + app=2)
Step 5: Update by Insert ("apple", 5)
- Calculate difference: x = 5 - 3 = 2 (updating existing key from 3 to 5) - Update hash table: d = {"apple": 5, "app": 2} - Add difference 2 to Trie path for "apple": root | a (val=7) // was 5, now 5+2=7 | p (val=7) // was 5, now 5+2=7 | p (val=7) // was 5, now 5+2=7 | l (val=5) // was 3, now 3+2=5 | e (val=5) // was 3, now 3+2=5
Step 6: Query sum("ap") again
- Traverse Trie: root โ a โ p - Return value at node 'p': 7 - This correctly returns apple=5 + app=2 = 7
The key insight is that by storing cumulative sums at each node and using the difference calculation (new_value - old_value), we can efficiently handle both new insertions and updates while maintaining correct prefix sums throughout the Trie.
Solution Implementation
1from typing import List, Optional
2from collections import defaultdict
3
4
5class Trie:
6 """A Trie (prefix tree) data structure for efficient prefix-based operations."""
7
8 def __init__(self):
9 # Array to store 26 child nodes (one for each lowercase letter a-z)
10 self.children: List[Optional['Trie']] = [None] * 26
11 # Accumulated value at this node (sum of all values for words ending here or passing through)
12 self.val: int = 0
13
14 def insert(self, word: str, delta: int) -> None:
15 """
16 Insert a word into the trie and update values along the path.
17
18 Args:
19 word: The word to insert
20 delta: The value difference to add to each node along the path
21 """
22 node = self
23 for char in word:
24 # Convert character to index (0-25)
25 index = ord(char) - ord('a')
26 # Create new node if path doesn't exist
27 if node.children[index] is None:
28 node.children[index] = Trie()
29 # Move to child node
30 node = node.children[index]
31 # Update the accumulated value at this node
32 node.val += delta
33
34 def search(self, prefix: str) -> int:
35 """
36 Search for a prefix and return the sum of all values with this prefix.
37
38 Args:
39 prefix: The prefix to search for
40
41 Returns:
42 The sum of all values for words with the given prefix, or 0 if prefix doesn't exist
43 """
44 node = self
45 for char in prefix:
46 # Convert character to index (0-25)
47 index = ord(char) - ord('a')
48 # If path doesn't exist, prefix is not in trie
49 if node.children[index] is None:
50 return 0
51 # Move to child node
52 node = node.children[index]
53 # Return the accumulated value at the prefix node
54 return node.val
55
56
57class MapSum:
58 """
59 A data structure that maps strings to values and supports sum queries for prefixes.
60 If a key already exists, its value is overridden.
61 """
62
63 def __init__(self):
64 # Dictionary to store current value for each key
65 self.key_value_map = defaultdict(int)
66 # Trie to efficiently compute prefix sums
67 self.trie = Trie()
68
69 def insert(self, key: str, val: int) -> None:
70 """
71 Insert or update a key-value pair.
72
73 Args:
74 key: The string key to insert or update
75 val: The new value for the key
76 """
77 # Calculate the difference between new value and old value (0 if new key)
78 delta = val - self.key_value_map[key]
79 # Update the stored value for this key
80 self.key_value_map[key] = val
81 # Update the trie with the value difference
82 self.trie.insert(key, delta)
83
84 def sum(self, prefix: str) -> int:
85 """
86 Return the sum of all values for keys that start with the given prefix.
87
88 Args:
89 prefix: The prefix to search for
90
91 Returns:
92 The sum of values for all keys with the given prefix
93 """
94 return self.trie.search(prefix)
95
96
97# Your MapSum object will be instantiated and called as such:
98# obj = MapSum()
99# obj.insert(key,val)
100# param_2 = obj.sum(prefix)
101
1/**
2 * Trie node implementation for prefix sum calculation
3 */
4class Trie {
5 // Array to store 26 lowercase English letters
6 private Trie[] children = new Trie[26];
7 // Sum of all values for strings passing through this node
8 private int sum;
9
10 /**
11 * Inserts a word into the trie and updates sum values along the path
12 * @param word the word to insert
13 * @param delta the value to add to each node along the path
14 */
15 public void insert(String word, int delta) {
16 Trie currentNode = this;
17
18 // Traverse each character in the word
19 for (int i = 0; i < word.length(); i++) {
20 // Calculate array index for the character (0-25)
21 int index = word.charAt(i) - 'a';
22
23 // Create new node if path doesn't exist
24 if (currentNode.children[index] == null) {
25 currentNode.children[index] = new Trie();
26 }
27
28 // Move to child node
29 currentNode = currentNode.children[index];
30 // Update sum value at this node
31 currentNode.sum += delta;
32 }
33 }
34
35 /**
36 * Searches for a prefix and returns the sum of all values with this prefix
37 * @param prefix the prefix to search for
38 * @return sum of all values with the given prefix, or 0 if prefix doesn't exist
39 */
40 public int search(String prefix) {
41 Trie currentNode = this;
42
43 // Traverse each character in the prefix
44 for (int i = 0; i < prefix.length(); i++) {
45 // Calculate array index for the character
46 int index = prefix.charAt(i) - 'a';
47
48 // If path doesn't exist, prefix not found
49 if (currentNode.children[index] == null) {
50 return 0;
51 }
52
53 // Move to child node
54 currentNode = currentNode.children[index];
55 }
56
57 // Return the sum at the final node of the prefix
58 return currentNode.sum;
59 }
60}
61
62/**
63 * MapSum data structure that maps keys to values and calculates prefix sums
64 */
65class MapSum {
66 // HashMap to store current key-value pairs
67 private Map<String, Integer> keyValueMap = new HashMap<>();
68 // Trie for efficient prefix sum calculation
69 private Trie trie = new Trie();
70
71 /**
72 * Constructor for MapSum
73 */
74 public MapSum() {
75 }
76
77 /**
78 * Inserts or updates a key-value pair
79 * @param key the key to insert or update
80 * @param val the new value for the key
81 */
82 public void insert(String key, int val) {
83 // Calculate the difference between new value and old value (0 if new key)
84 int delta = val - keyValueMap.getOrDefault(key, 0);
85
86 // Update the key-value mapping
87 keyValueMap.put(key, val);
88
89 // Update the trie with the delta value
90 trie.insert(key, delta);
91 }
92
93 /**
94 * Returns the sum of all values whose keys start with the given prefix
95 * @param prefix the prefix to search for
96 * @return sum of all values with keys starting with prefix
97 */
98 public int sum(String prefix) {
99 return trie.search(prefix);
100 }
101}
102
103/**
104 * Your MapSum object will be instantiated and called as such:
105 * MapSum obj = new MapSum();
106 * obj.insert(key,val);
107 * int param_2 = obj.sum(prefix);
108 */
109
1class Trie {
2public:
3 // Constructor initializes children array with 26 null pointers (for 'a' to 'z')
4 Trie() : children(26, nullptr), sum(0) {
5 }
6
7 // Insert a word into the trie and update sum values along the path
8 void insert(string& word, int delta) {
9 Trie* currentNode = this;
10
11 for (char ch : word) {
12 // Convert character to index (0-25)
13 int index = ch - 'a';
14
15 // Create new node if path doesn't exist
16 if (!currentNode->children[index]) {
17 currentNode->children[index] = new Trie();
18 }
19
20 // Move to child node
21 currentNode = currentNode->children[index];
22
23 // Add delta to cumulative sum at this node
24 currentNode->sum += delta;
25 }
26 }
27
28 // Search for a prefix and return the sum of all values with this prefix
29 int search(string& prefix) {
30 Trie* currentNode = this;
31
32 for (char ch : prefix) {
33 // Convert character to index (0-25)
34 int index = ch - 'a';
35
36 // If path doesn't exist, no words with this prefix
37 if (!currentNode->children[index]) {
38 return 0;
39 }
40
41 // Move to child node
42 currentNode = currentNode->children[index];
43 }
44
45 // Return the cumulative sum at the prefix node
46 return currentNode->sum;
47 }
48
49private:
50 vector<Trie*> children; // Pointers to child nodes (26 letters)
51 int sum; // Cumulative sum of all values passing through this node
52};
53
54class MapSum {
55public:
56 // Constructor
57 MapSum() : trieRoot(new Trie()) {
58 }
59
60 // Insert or update a key-value pair
61 void insert(string key, int val) {
62 // Calculate the difference between new value and old value (0 if new key)
63 int delta = val - keyValueMap[key];
64
65 // Update the stored value for this key
66 keyValueMap[key] = val;
67
68 // Update the trie with the delta value
69 trieRoot->insert(key, delta);
70 }
71
72 // Return the sum of all values that have the given prefix
73 int sum(string prefix) {
74 return trieRoot->search(prefix);
75 }
76
77private:
78 unordered_map<string, int> keyValueMap; // Store key-value pairs for tracking updates
79 Trie* trieRoot; // Root of the trie structure
80};
81
82/**
83 * Your MapSum object will be instantiated and called as such:
84 * MapSum* obj = new MapSum();
85 * obj->insert(key,val);
86 * int param_2 = obj->sum(prefix);
87 */
88
1// Trie node structure for efficient prefix operations
2interface TrieNode {
3 children: (TrieNode | null)[]; // Array of 26 slots for lowercase letters a-z
4 value: number; // Sum of all values for words passing through this node
5}
6
7// Create a new trie node
8function createTrieNode(): TrieNode {
9 return {
10 children: new Array(26).fill(null),
11 value: 0
12 };
13}
14
15// Insert a word into the trie with its associated value
16function trieInsert(root: TrieNode, word: string, valueToAdd: number): void {
17 let currentNode = root;
18
19 for (const char of word) {
20 // Convert character to index (a=0, b=1, ..., z=25)
21 const index = char.charCodeAt(0) - 97;
22
23 // Create child node if it doesn't exist
24 if (!currentNode.children[index]) {
25 currentNode.children[index] = createTrieNode();
26 }
27
28 // Move to child node and update its value
29 currentNode = currentNode.children[index]!;
30 currentNode.value += valueToAdd;
31 }
32}
33
34// Search for a prefix and return the sum of all values with this prefix
35function trieSearch(root: TrieNode, prefix: string): number {
36 let currentNode = root;
37
38 for (const char of prefix) {
39 // Convert character to index
40 const index = char.charCodeAt(0) - 97;
41
42 // If path doesn't exist, no words with this prefix
43 if (!currentNode.children[index]) {
44 return 0;
45 }
46
47 currentNode = currentNode.children[index]!;
48 }
49
50 // Return the accumulated sum at this node
51 return currentNode.value;
52}
53
54// Global variables for MapSum functionality
55let keyValueMap: Map<string, number>; // Store current value for each key
56let trieRoot: TrieNode; // Root of the trie structure
57
58// Initialize MapSum data structures
59function MapSum(): void {
60 keyValueMap = new Map<string, number>();
61 trieRoot = createTrieNode();
62}
63
64// Insert or update a key-value pair
65function insert(key: string, val: number): void {
66 // Calculate the difference between new value and existing value (or 0 if new key)
67 const previousValue = keyValueMap.get(key) ?? 0;
68 const valueDifference = val - previousValue;
69
70 // Update the map with new value
71 keyValueMap.set(key, val);
72
73 // Update trie with the difference to maintain correct sums
74 trieInsert(trieRoot, key, valueDifference);
75}
76
77// Get the sum of all values whose keys start with the given prefix
78function sum(prefix: string): number {
79 return trieSearch(trieRoot, prefix);
80}
81
82/**
83 * Usage:
84 * MapSum();
85 * insert(key, val);
86 * let result = sum(prefix);
87 */
88
Time and Space Complexity
Time Complexity:
insert(key, val)
:O(m)
wherem
is the length of the key. The method traverses through each character of the key once to update the trie nodes.sum(prefix)
:O(p)
wherep
is the length of the prefix. The method traverses through each character of the prefix to find the corresponding node.
Space Complexity:
-
The overall space complexity is
O(n ร m ร C)
, where:n
is the number of unique keys storedm
is the maximum length of the keysC
is the size of the character set (26 for lowercase English letters)
-
The
defaultdict
storesn
key-value pairs, contributingO(n ร m)
space (as keys are strings of length up tom
) -
The Trie structure can have up to
n ร m
nodes in the worst case (when all keys have completely different characters), and each node contains an array of 26 pointers to children, resulting inO(n ร m ร 26)
space -
Therefore, the dominant factor is the Trie structure with
O(n ร m ร C)
whereC = 26
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Key Updates
One of the most common mistakes is failing to properly handle the case when a key already exists and needs to be updated with a new value.
Pitfall Example:
def insert(self, key: str, val: int) -> None:
# WRONG: Simply adding the new value without considering existing value
self.trie.insert(key, val) # This would add val on top of existing value
self.key_value_map[key] = val
Why it's wrong: If "apple" was previously inserted with value 3, and we insert "apple" again with value 5, the trie nodes would accumulate both values (3 + 5 = 8) instead of replacing 3 with 5.
Correct Solution:
def insert(self, key: str, val: int) -> None:
# Calculate the difference to add/subtract from trie nodes
delta = val - self.key_value_map[key] # defaultdict returns 0 for new keys
self.key_value_map[key] = val
self.trie.insert(key, delta) # Only add the difference
2. Not Using defaultdict Leading to KeyError
Another common mistake is using a regular dictionary instead of defaultdict(int)
.
Pitfall Example:
def __init__(self):
self.key_value_map = {} # Regular dict instead of defaultdict
self.trie = Trie()
def insert(self, key: str, val: int) -> None:
# This will raise KeyError if key doesn't exist
delta = val - self.key_value_map[key] # KeyError for new keys!
Correct Solution:
def __init__(self):
self.key_value_map = defaultdict(int) # Returns 0 for missing keys
# OR handle it explicitly:
# self.key_value_map = {}
def insert(self, key: str, val: int) -> None:
# If using regular dict, check existence first
delta = val - self.key_value_map.get(key, 0) # Use get() with default
self.key_value_map[key] = val
self.trie.insert(key, delta)
3. Accumulating Values at Wrong Trie Nodes
Some implementations mistakenly only update the value at the end node of a word, rather than updating all nodes along the path.
Pitfall Example:
def insert(self, word: str, delta: int) -> None:
node = self
for char in word:
index = ord(char) - ord('a')
if node.children[index] is None:
node.children[index] = Trie()
node = node.children[index]
# WRONG: Only updating the final node
node.val += delta # Should update ALL nodes along the path
Why it's wrong: If we have "apple" with value 3 and "app" with value 2, searching for prefix "ap" would return 0 instead of 5, because the intermediate nodes weren't updated.
Correct Solution:
def insert(self, word: str, delta: int) -> None:
node = self
for char in word:
index = ord(char) - ord('a')
if node.children[index] is None:
node.children[index] = Trie()
node = node.children[index]
node.val += delta # Update EVERY node along the path
4. Memory Inefficiency with Fixed-Size Children Array
While not incorrect, using a fixed array of 26 elements for each node can be memory-inefficient when the trie is sparse.
Alternative Solution Using Dictionary:
class Trie:
def __init__(self):
self.children = {} # Use dict instead of fixed array
self.val = 0
def insert(self, word: str, delta: int) -> None:
node = self
for char in word:
if char not in node.children:
node.children[char] = Trie()
node = node.children[char]
node.val += delta
This saves memory when most nodes don't have all 26 children, though it may have slightly worse cache locality.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
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!