class TrieNode:
def __init__(self):
self.children = {}
self.value = 0
class MapSum:
def __init__(self):
self.root = TrieNode()
self.map = {}
def insert(self, key: str, val: int) -> None:
delta = val - self.map.get(key, 0)
self.map[key] = val
node = self.root
for c in key:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.value += delta
def sum(self, prefix: str) -> int:
node = self.root
for c in prefix:
if c not in node.children:
return 0
node = node.children[c]
return node.value
class TrieNode {
public:
unordered_map<char, TrieNode*> children;
int value = 0;
};
class MapSum {
TrieNode* root;
unordered_map<string, int> map;
public:
MapSum() {
root = new TrieNode();
}
void insert(string key, int val) {
int delta = val - map[key];
map[key] = val;
TrieNode* node = root;
for (char c : key) {
if (!node->children.count(c))
node->children[c] = new TrieNode();
node = node->children[c];
node->value += delta;
}
}
int sum(string prefix) {
TrieNode* node = root;
for (char c : prefix) {
if (!node->children.count(c))
return 0;
node = node->children[c];
}
return node->value;
}
};
class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
int value = 0;
}
class MapSum {
TrieNode root;
Map<String, Integer> map;
public MapSum() {
root = new TrieNode();
map = new HashMap<>();
}
public void insert(String key, int val) {
int delta = val - map.getOrDefault(key, 0);
map.put(key, val);
TrieNode node = root;
for (char c : key.toCharArray()) {
node.children.putIfAbsent(c, new TrieNode());
node = node.children.get(c);
node.value += delta;
}
}
public int sum(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
if (!node.children.containsKey(c))
return 0;
node = node.children.get(c);
}
return node.value;
}
}
class TrieNode {
constructor() {
this.children = {};
this.value = 0;
}
}
class MapSum {
constructor() {
this.root = new TrieNode();
this.map = {};
}
insert(key, val) {
let delta = val - (this.map[key] || 0);
this.map[key] = val;
let node = this.root;
for (let c of key) {
if (!node.children[c]) {
node.children[c] = new TrieNode();
}
node = node.children[c];
node.value += delta;
}
}
sum(prefix) {
let node = this.root;
for (let c of prefix) {
if (!node.children[c]) return 0;
node = node.children[c];
}
return node.value;
}
}
The Map Sum Pairs problem requires you to design a data structure that supports two operations:
insert(key, val)
: Insert the string key
with integer value val
into the data structure. If the key already exists, update its value to val
.
sum(prefix)
: Return the sum of all the values of keys that start with the given prefix
.
The key constraints are:
key
is a non-empty string of lowercase letters.val
is an integer.sum
operation must efficiently return the sum of all values for keys with the given prefix.
At first glance, the problem seems similar to working with a regular map or dictionary. You could store each key
with its val
and, for a sum(prefix)
query, iterate through all keys and add up values for those starting with the prefix.
However, this brute-force approach is inefficient, especially if there are many keys or frequent sum queries. We need an optimized way to quickly find and sum all values of keys sharing a common prefix.
This leads us to consider data structures designed for prefix-based operations. A trie (prefix tree) is a natural fit, as it allows us to traverse only the relevant parts of the key space for a given prefix.
To solve the problem efficiently, we use a combination of a trie (prefix tree) and a hash map:
value
field, which is the sum of all val
s for keys that pass through that node. This allows us to quickly compute the sum of all keys with a given prefix by traversing the trie to the end of the prefix and returning the stored sum.
Step-by-step algorithm:
delta
) between the new value and the existing value for the key (if any).delta
to its value
field.value
at the last node of the prefix.
This approach ensures that both insert
and sum
operations are efficient and scale well with the number of keys and queries.
Let's walk through an example step by step:
insert("apple", 3)
:value
field.{"apple": 3}
.
sum("ap")
:value = 3
(from "apple").insert("app", 2)
:value = 5
.{"apple": 3, "app": 2}
.
sum("ap")
:value = 5
(3 from "apple" + 2 from "app").insert("apple", 2)
:{"apple": 2, "app": 2}
.
sum("ap")
:value = 4
(2 from "apple" + 2 from "app").insert
: O(1)sum
: O(N * L), where N is the number of keys and L is the average key length (need to check each key for the prefix)insert
: O(L), where L is the length of the key (walk the trie and update each node)sum
: O(P), where P is the length of the prefix (just walk the prefix in the trie)
The trie-based approach is much more efficient for frequent sum
queries, especially as the number of keys grows.
The Map Sum Pairs problem is elegantly solved by combining a trie (for fast prefix-based aggregation) with a hash map (for tracking current key values and computing updates). This hybrid structure allows both insert
and sum
operations to be performed efficiently, avoiding the pitfalls of brute-force scanning. The key insight is to store running sums at each trie node and update them with the correct delta when key values change, ensuring both correctness and performance.