Facebook Pixel

677. Map Sum Pairs

MediumDesignTrieHash TableString
Leetcode Link

Problem Description

You need to design a data structure called MapSum that supports two operations:

  1. 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.

  2. 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 object
  • insert(key, val): Adds or updates the key-value pair in the map
  • sum(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.

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 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

  1. 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.

  2. Trie: A prefix tree where each node contains:

    • children: An array of size 26 (for lowercase letters a-z) storing child nodes
    • val: 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 with None 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) where n is the length of the key
  • Sum: O(m) where m is the length of the prefix

Space Complexity

  • O(ALPHABET_SIZE ร— N ร— L) where ALPHABET_SIZE = 26, N is the number of unique keys, and L 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 Evaluator

Example 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) where m 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) where p 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 stored
    • m is the maximum length of the keys
    • C is the size of the character set (26 for lowercase English letters)
  • The defaultdict stores n key-value pairs, contributing O(n ร— m) space (as keys are strings of length up to m)

  • 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 in O(n ร— m ร— 26) space

  • Therefore, the dominant factor is the Trie structure with O(n ร— m ร— C) where C = 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More