3485. Longest Common Prefix of K Strings After Removal
Problem Description
You have an array of strings called words
and an integer k
. Your task is to find, for each string in the array, what happens when you remove that string and then look for the longest common prefix among any k
strings from the remaining strings.
More specifically:
- For each index
i
from0
towords.length - 1
, you need to:- Temporarily remove the string at index
i
from the array - From the remaining strings, select any
k
strings (they must be at different indices) - Find the longest common prefix among those
k
selected strings - Among all possible selections of
k
strings, find the maximum length of the common prefix
- Temporarily remove the string at index
The result should be an array answer
where answer[i]
represents the maximum length of the longest common prefix when the i
-th string is removed.
Special case: If removing the i
-th string leaves you with fewer than k
strings in the array, then answer[i]
should be 0
.
For example, if you have words = ["abc", "abd", "abcd", "xy"]
and k = 2
:
- When removing index 0 ("abc"), you can select "abd" and "abcd" which share prefix "ab" of length 2
- When removing index 1 ("abd"), you can select "abc" and "abcd" which share prefix "abc" of length 3
- And so on for each index
The solution uses a Trie data structure to efficiently store all strings and track how many strings pass through each node. It also uses a Segment Tree to quickly query the maximum depth at which at least k
strings share a common prefix, while temporarily adjusting counts when a string is "removed" from consideration.
Intuition
The key insight is that finding the longest common prefix among k
strings is fundamentally about finding the deepest point in a prefix tree (Trie) where at least k
strings pass through.
When we build a Trie with all the strings, each node represents a prefix, and we can count how many strings pass through each node. A node with count ≥ k
means that at least k
strings share that prefix. The deeper such a node is in the Trie, the longer the common prefix.
Now, when we "remove" the i
-th string, we're essentially asking: what's the longest prefix shared by at least k
strings when this particular string is not counted? This changes the counts at certain nodes in the Trie.
The challenge is that we need to do this efficiently for each string. We can't rebuild the Trie each time. Instead, we observe that:
- When we remove a string, only the nodes along its path in the Trie are affected (their counts decrease by 1)
- A node becomes "fragile" when its count is exactly
k
- removing one string that passes through it would make it invalid (count becomesk-1
)
For efficient querying of the maximum valid depth after temporary removals, we use a Segment Tree. The Segment Tree tracks, for each depth level, how many nodes at that depth have count ≥ k
. When we temporarily remove a string:
- We decrease the count for depths where the string passes through fragile nodes (count =
k
) - Query the maximum depth that still has at least one valid node
- Restore the counts back
This approach avoids rebuilding the Trie for each removal and uses the Segment Tree's O(log n)
update and query operations to efficiently find the answer for each string.
Learn more about Trie patterns.
Solution Approach
The solution uses a combination of Trie and Segment Tree data structures to efficiently solve the problem.
Step 1: Build the Trie
First, we construct a Trie to store all strings and track prefix information:
ArrayList<TrieNode> [trie](/problems/trie_intro) = new ArrayList<>(); trie.add(new TrieNode()); for (String word : words) { int cur = 0; for (char c : word.toCharArray()) { int idx = c - 'a'; if ([trie](/problems/trie_intro).get(cur).children[idx] == -1) { trie.get(cur).children[idx] = trie.size(); TrieNode node = new TrieNode(); node.depth = trie.get(cur).depth + 1; trie.add(node); } cur = trie.get(cur).children[idx]; trie.get(cur).count++; } }
Each TrieNode
stores:
count
: number of strings passing through this nodedepth
: the depth in the Trie (represents prefix length)children
: array of child node indices
Step 2: Find Maximum Valid Depth and Count Valid Nodes
We determine the maximum depth where at least k
strings share a prefix and count how many nodes at each depth are valid:
int maxDepth = 0; for (int i = 1; i < [trie](/problems/trie_intro).size(); ++i) { if (trie.get(i).count >= k) { maxDepth = Math.max(maxDepth, trie.get(i).depth); } } int[] globalCount = new int[maxDepth + 1]; for (int i = 1; i < [trie](/problems/trie_intro).size(); ++i) { TrieNode node = trie.get(i); if (node.count >= k && node.depth <= maxDepth) { globalCount[node.depth]++; } }
Step 3: Identify Fragile Nodes
For each string, we find the "fragile" nodes - nodes with exactly k
count that this string passes through:
List<List<Integer>> fragileList = new ArrayList<>(); for (int i = 0; i < n; ++i) { fragileList.add(new ArrayList<>()); } for (int i = 0; i < n; ++i) { int cur = 0; for (char c : words[i].toCharArray()) { int idx = c - 'a'; cur = [trie](/problems/trie_intro).get(cur).children[idx]; if (trie.get(cur).count == k) { fragileList.get(i).add(trie.get(cur).depth); } } }
Step 4: Use Segment Tree for Efficient Queries
The Segment Tree maintains the maximum depth that has at least one valid node. For each string removal:
SegmentTree segTree = new SegmentTree(segSize, globalCount); for (int i = 0; i < n; ++i) { // Temporarily decrease counts at fragile depths for (int d : fragileList.get(i)) { segTree.update(1, 1, segSize, d, globalCount[d] - 1); } // Query maximum valid depth int res = segTree.query(); ans[i] = res == -1 ? 0 : res; // Restore counts for (int d : fragileList.get(i)) { segTree.update(1, 1, segSize, d, globalCount[d]); } }
The Segment Tree's key operations:
build()
: Initializes the tree with global countsupdate()
: Updates the count at a specific depthquery()
: Returns the maximum depth with at least one valid node
The tree maintains that tree[idx]
stores the maximum valid depth in its range, where a depth is valid if it has at least one node with count ≥ k
.
Time Complexity
- Building Trie:
O(N × L)
whereN
is number of words andL
is average length - Building Segment Tree:
O(maxDepth)
- Processing each word:
O(L × log(maxDepth))
- Overall:
O(N × L × log(maxDepth))
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 a concrete example with words = ["abc", "ab", "abd", "abcd"]
and k = 2
.
Step 1: Build the Trie
We insert all words into the Trie, tracking count at each node:
root (count=0) | 'a' (count=4, depth=1) | 'b' (count=4, depth=2) / \ 'c' 'd' (count=2, depth=3) (count=2) | | 'c' 'd' (count=1, depth=4) (count=1)
Step 2: Find Valid Nodes and Maximum Depth
Since k = 2
, we need nodes with count ≥ 2:
- Depth 1: node 'a' has count=4 ≥ 2 ✓
- Depth 2: node 'b' has count=4 ≥ 2 ✓
- Depth 3: node 'c' has count=2 ≥ 2 ✓, node 'd' has count=2 ≥ 2 ✓
Maximum valid depth = 3
Global count array: [0, 1, 1, 2]
(representing counts at depths 0, 1, 2, 3)
Step 3: Identify Fragile Nodes for Each Word
A node is "fragile" if it has exactly count = k = 2.
-
Word "abc": passes through 'a'(4) → 'b'(4) → 'c'(2)
- Fragile nodes: 'c' at depth 3
- fragileList[0] = [3]
-
Word "ab": passes through 'a'(4) → 'b'(4)
- Fragile nodes: none
- fragileList[1] = []
-
Word "abd": passes through 'a'(4) → 'b'(4) → 'd'(2)
- Fragile nodes: 'd' at depth 3
- fragileList[2] = [3]
-
Word "abcd": passes through 'a'(4) → 'b'(4) → 'c'(2) → 'd'(1)
- Fragile nodes: 'c' at depth 3
- fragileList[3] = [3]
Step 4: Process Each Word Removal with Segment Tree
Initial Segment Tree with globalCount = [0, 1, 1, 2]:
For i=0 (remove "abc"):
- Update depth 3: globalCount[3] - 1 = 2 - 1 = 1
- Query: Maximum depth with count > 0 is still 3
- answer[0] = 3 (words "ab", "abd", "abcd" can form pairs with prefix "ab" of length 2, or "abd" and "abcd" with prefix "ab" of length 2)
- Restore depth 3 back to 2
For i=1 (remove "ab"):
- No fragile nodes to update
- Query: Maximum depth with count > 0 is 3
- answer[1] = 3 (words "abc", "abd", "abcd" - selecting "abd" and "abcd" gives prefix "abd" of length 3)
For i=2 (remove "abd"):
- Update depth 3: globalCount[3] - 1 = 2 - 1 = 1
- Query: Maximum depth with count > 0 is still 3
- answer[2] = 3 (words "abc", "ab", "abcd" - selecting "abc" and "abcd" gives prefix "abc" of length 3)
- Restore depth 3 back to 2
For i=3 (remove "abcd"):
- Update depth 3: globalCount[3] - 1 = 2 - 1 = 1
- Query: Maximum depth with count > 0 is still 3
- answer[3] = 3 (words "abc", "ab", "abd" - selecting "abc" and "abd" gives prefix "ab" of length 2, but we can also select "abc" and "ab" or "ab" and "abd" with prefix "ab" of length 2)
Wait, let me recalculate answer[3] more carefully:
- After removing "abcd", we have ["abc", "ab", "abd"]
- Best k=2 selection: "abc" and "ab" share prefix "ab" (length 2)
- Or "ab" and "abd" share prefix "ab" (length 2)
- answer[3] = 2
Final answer: [3, 3, 3, 2]
The key insight is that the Segment Tree efficiently tracks which depths remain valid after temporarily removing a string's contribution through its fragile nodes.
Solution Implementation
1class Solution:
2 def longestCommonPrefix(self, words: List[str], k: int) -> List[int]:
3 """
4 Find the longest common prefix for each word when that word is excluded,
5 where the prefix must appear in at least k of the remaining words.
6 """
7 n = len(words)
8 result = [0] * n
9
10 # Early return if not enough words for k-common prefix
11 if n - 1 < k:
12 return result
13
14 # Build trie from all words
15 # Each node stores: count (occurrences), depth (prefix length), children (dict of char->node_index)
16 trie = [{'count': 0, 'depth': 0, 'children': {}}] # Initialize with root node
17
18 for word in words:
19 current_node = 0
20 for ch in word:
21 # Create new node if path doesn't exist
22 if ch not in trie[current_node]['children']:
23 trie[current_node]['children'][ch] = len(trie)
24 trie.append({
25 'count': 0,
26 'depth': trie[current_node]['depth'] + 1,
27 'children': {}
28 })
29 current_node = trie[current_node]['children'][ch]
30 trie[current_node]['count'] += 1
31
32 # Find maximum depth among nodes with count >= k
33 max_depth = 0
34 for i in range(1, len(trie)):
35 if trie[i]['count'] >= k:
36 max_depth = max(max_depth, trie[i]['depth'])
37
38 # Count nodes at each depth that have count >= k
39 global_count = [0] * (max_depth + 1)
40 for i in range(1, len(trie)):
41 if trie[i]['count'] >= k and trie[i]['depth'] <= max_depth:
42 global_count[trie[i]['depth']] += 1
43
44 # Find fragile depths for each word (nodes with exactly k occurrences)
45 fragile_depths = [[] for _ in range(n)]
46 for i in range(n):
47 current_node = 0
48 for ch in words[i]:
49 current_node = trie[current_node]['children'][ch]
50 if trie[current_node]['count'] == k:
51 fragile_depths[i].append(trie[current_node]['depth'])
52
53 # Process each word using segment tree if there are valid depths
54 segment_size = max_depth
55 if segment_size >= 1:
56 segment_tree = SegmentTree(segment_size, global_count)
57
58 for i in range(n):
59 if n - 1 < k:
60 result[i] = 0
61 else:
62 # Temporarily decrease counts for fragile depths
63 for depth in fragile_depths[i]:
64 segment_tree.update(1, 1, segment_size, depth, global_count[depth] - 1)
65
66 # Query maximum valid depth
67 max_valid_depth = segment_tree.query()
68 result[i] = 0 if max_valid_depth == -1 else max_valid_depth
69
70 # Restore original counts
71 for depth in fragile_depths[i]:
72 segment_tree.update(1, 1, segment_size, depth, global_count[depth])
73
74 return result
75
76
77class SegmentTree:
78 """
79 Segment tree for efficient range maximum queries.
80 Used to find the maximum depth with valid count.
81 """
82
83 def __init__(self, n: int, global_count: List[int]):
84 """
85 Initialize segment tree with given size and count array.
86
87 Args:
88 n: Size of the segment tree
89 global_count: Reference to global count array
90 """
91 self.n = n
92 self.global_count = global_count
93 self.tree = [-1] * (4 * (n + 1))
94 self.build(1, 1, n)
95
96 def build(self, node_idx: int, left: int, right: int):
97 """
98 Build segment tree recursively.
99
100 Args:
101 node_idx: Current node index in the tree
102 left: Left boundary of the range
103 right: Right boundary of the range
104 """
105 if left == right:
106 # Leaf node: store depth if count > 0, otherwise -1
107 self.tree[node_idx] = left if self.global_count[left] > 0 else -1
108 return
109
110 mid = (left + right) // 2
111 self.build(node_idx * 2, left, mid)
112 self.build(node_idx * 2 + 1, mid + 1, right)
113 # Internal node stores maximum of children
114 self.tree[node_idx] = max(self.tree[node_idx * 2], self.tree[node_idx * 2 + 1])
115
116 def update(self, node_idx: int, left: int, right: int, position: int, new_value: int):
117 """
118 Update a single position in the segment tree.
119
120 Args:
121 node_idx: Current node index in the tree
122 left: Left boundary of the range
123 right: Right boundary of the range
124 position: Position to update
125 new_value: New value for the position
126 """
127 if left == right:
128 # Update leaf node based on new value
129 self.tree[node_idx] = left if new_value > 0 else -1
130 return
131
132 mid = (left + right) // 2
133 if position <= mid:
134 self.update(node_idx * 2, left, mid, position, new_value)
135 else:
136 self.update(node_idx * 2 + 1, mid + 1, right, position, new_value)
137 # Update internal node with maximum of children
138 self.tree[node_idx] = max(self.tree[node_idx * 2], self.tree[node_idx * 2 + 1])
139
140 def query(self) -> int:
141 """
142 Query the maximum value in the entire range.
143
144 Returns:
145 Maximum valid depth or -1 if none exists
146 """
147 return self.tree[1] # Root contains the maximum
148
1class Solution {
2 /**
3 * TrieNode class represents a node in the Trie data structure
4 */
5 static class TrieNode {
6 int count = 0; // Number of words passing through this node
7 int depth = 0; // Depth of this node in the trie (length of prefix)
8 int[] children = new int[26]; // Array to store indices of child nodes (for 26 letters)
9
10 TrieNode() {
11 // Initialize all children as -1 (no child exists)
12 for (int i = 0; i < 26; i++) {
13 children[i] = -1;
14 }
15 }
16 }
17
18 /**
19 * SegmentTree class for efficient range maximum queries
20 */
21 static class SegmentTree {
22 int n; // Size of the segment tree
23 int[] tree; // Array to store segment tree nodes
24 int[] globalCount; // Array to store global counts for each depth
25
26 SegmentTree(int n, int[] globalCount) {
27 this.n = n;
28 this.globalCount = globalCount;
29 this.tree = new int[4 * (n + 1)];
30
31 // Initialize tree with -1
32 for (int i = 0; i < tree.length; i++) {
33 tree[i] = -1;
34 }
35
36 // Build the segment tree
37 build(1, 1, n);
38 }
39
40 /**
41 * Build the segment tree recursively
42 */
43 void build(int nodeIndex, int left, int right) {
44 if (left == right) {
45 // Leaf node: set value based on globalCount
46 tree[nodeIndex] = globalCount[left] > 0 ? left : -1;
47 return;
48 }
49
50 int mid = (left + right) / 2;
51 // Build left subtree
52 build(nodeIndex * 2, left, mid);
53 // Build right subtree
54 build(nodeIndex * 2 + 1, mid + 1, right);
55 // Merge results from children
56 tree[nodeIndex] = Math.max(tree[nodeIndex * 2], tree[nodeIndex * 2 + 1]);
57 }
58
59 /**
60 * Update a single position in the segment tree
61 */
62 void update(int nodeIndex, int left, int right, int position, int newValue) {
63 if (left == right) {
64 // Update leaf node
65 tree[nodeIndex] = newValue > 0 ? left : -1;
66 return;
67 }
68
69 int mid = (left + right) / 2;
70 if (position <= mid) {
71 // Update left subtree
72 update(nodeIndex * 2, left, mid, position, newValue);
73 } else {
74 // Update right subtree
75 update(nodeIndex * 2 + 1, mid + 1, right, position, newValue);
76 }
77 // Update current node based on children
78 tree[nodeIndex] = Math.max(tree[nodeIndex * 2], tree[nodeIndex * 2 + 1]);
79 }
80
81 /**
82 * Query the maximum value in the entire range
83 */
84 int query() {
85 return tree[1]; // Root of the segment tree
86 }
87 }
88
89 /**
90 * Find the longest common prefix for each word when excluding it from the word list
91 * @param words Array of words to process
92 * @param k Minimum number of words that must share a common prefix
93 * @return Array where ans[i] is the length of longest common prefix when words[i] is excluded
94 */
95 public int[] longestCommonPrefix(String[] words, int k) {
96 int n = words.length;
97 int[] answer = new int[n];
98
99 // If total words minus one is less than k, no valid prefix exists
100 if (n - 1 < k) {
101 return answer;
102 }
103
104 // Initialize trie with root node
105 ArrayList<TrieNode> trie = new ArrayList<>();
106 trie.add(new TrieNode());
107
108 // Build the trie with all words
109 for (String word : words) {
110 int currentNodeIndex = 0;
111
112 for (char character : word.toCharArray()) {
113 int charIndex = character - 'a';
114
115 // Create new node if path doesn't exist
116 if (trie.get(currentNodeIndex).children[charIndex] == -1) {
117 trie.get(currentNodeIndex).children[charIndex] = trie.size();
118 TrieNode newNode = new TrieNode();
119 newNode.depth = trie.get(currentNodeIndex).depth + 1;
120 trie.add(newNode);
121 }
122
123 // Move to child node and increment count
124 currentNodeIndex = trie.get(currentNodeIndex).children[charIndex];
125 trie.get(currentNodeIndex).count++;
126 }
127 }
128
129 // Find maximum depth where at least k words share a prefix
130 int maxDepth = 0;
131 for (int i = 1; i < trie.size(); i++) {
132 if (trie.get(i).count >= k) {
133 maxDepth = Math.max(maxDepth, trie.get(i).depth);
134 }
135 }
136
137 // Count valid prefixes at each depth
138 int[] globalCount = new int[maxDepth + 1];
139 for (int i = 1; i < trie.size(); i++) {
140 TrieNode node = trie.get(i);
141 if (node.count >= k && node.depth <= maxDepth) {
142 globalCount[node.depth]++;
143 }
144 }
145
146 // Build list of fragile depths for each word
147 // A depth is fragile for a word if removing the word would make count < k
148 List<List<Integer>> fragileDepths = new ArrayList<>();
149 for (int i = 0; i < n; i++) {
150 fragileDepths.add(new ArrayList<>());
151 }
152
153 // Identify fragile depths for each word
154 for (int i = 0; i < n; i++) {
155 int currentNodeIndex = 0;
156
157 for (char character : words[i].toCharArray()) {
158 int charIndex = character - 'a';
159 currentNodeIndex = trie.get(currentNodeIndex).children[charIndex];
160
161 // This depth becomes fragile if count equals exactly k
162 if (trie.get(currentNodeIndex).count == k) {
163 fragileDepths.get(i).add(trie.get(currentNodeIndex).depth);
164 }
165 }
166 }
167
168 // Use segment tree to efficiently find maximum valid depth
169 int segmentTreeSize = maxDepth;
170 if (segmentTreeSize >= 1) {
171 SegmentTree segmentTree = new SegmentTree(segmentTreeSize, globalCount);
172
173 for (int i = 0; i < n; i++) {
174 if (n - 1 < k) {
175 answer[i] = 0;
176 } else {
177 // Temporarily decrease counts for fragile depths
178 for (int depth : fragileDepths.get(i)) {
179 segmentTree.update(1, 1, segmentTreeSize, depth, globalCount[depth] - 1);
180 }
181
182 // Query maximum valid depth
183 int result = segmentTree.query();
184 answer[i] = result == -1 ? 0 : result;
185
186 // Restore original counts
187 for (int depth : fragileDepths.get(i)) {
188 segmentTree.update(1, 1, segmentTreeSize, depth, globalCount[depth]);
189 }
190 }
191 }
192 }
193
194 return answer;
195 }
196}
197
1class Solution {
2public:
3 // Trie node structure to store prefix tree information
4 struct TrieNode {
5 int count = 0; // Number of words passing through this node
6 int depth = 0; // Depth of this node in the trie (length of prefix)
7 int children[26] = {0}; // Child node indices for each letter a-z
8 };
9
10 // Segment tree for efficient range maximum queries
11 class SegmentTree {
12 public:
13 int n; // Size of the segment tree
14 vector<int> tree; // Segment tree array
15 vector<int>& globalCount; // Reference to global count array
16
17 // Constructor initializes segment tree with given size and count array
18 SegmentTree(int n, vector<int>& globalCount)
19 : n(n), globalCount(globalCount) {
20 tree.assign(4 * (n + 1), -1);
21 build(1, 1, n);
22 }
23
24 // Build segment tree recursively
25 void build(int nodeIdx, int left, int right) {
26 if (left == right) {
27 // Leaf node: store depth if count > 0, otherwise -1
28 tree[nodeIdx] = globalCount[left] > 0 ? left : -1;
29 return;
30 }
31 int mid = (left + right) / 2;
32 build(nodeIdx * 2, left, mid);
33 build(nodeIdx * 2 + 1, mid + 1, right);
34 // Internal node stores maximum of children
35 tree[nodeIdx] = max(tree[nodeIdx * 2], tree[nodeIdx * 2 + 1]);
36 }
37
38 // Update a single position in the segment tree
39 void update(int nodeIdx, int left, int right, int position, int newValue) {
40 if (left == right) {
41 // Update leaf node based on new value
42 tree[nodeIdx] = newValue > 0 ? left : -1;
43 return;
44 }
45 int mid = (left + right) / 2;
46 if (position <= mid) {
47 update(nodeIdx * 2, left, mid, position, newValue);
48 } else {
49 update(nodeIdx * 2 + 1, mid + 1, right, position, newValue);
50 }
51 // Update internal node with maximum of children
52 tree[nodeIdx] = max(tree[nodeIdx * 2], tree[nodeIdx * 2 + 1]);
53 }
54
55 // Query the maximum value in the entire range
56 int query() {
57 return tree[1]; // Root contains the maximum
58 }
59 };
60
61 vector<int> longestCommonPrefix(vector<string>& words, int k) {
62 int n = words.size();
63 vector<int> result(n, 0);
64
65 // Early return if not enough words for k-common prefix
66 if (n - 1 < k) {
67 return result;
68 }
69
70 // Build trie from all words
71 vector<TrieNode> trie(1); // Initialize with root node
72 for (const string& word : words) {
73 int currentNode = 0;
74 for (char ch : word) {
75 int charIndex = ch - 'a';
76 // Create new node if path doesn't exist
77 if (trie[currentNode].children[charIndex] == 0) {
78 trie[currentNode].children[charIndex] = trie.size();
79 trie.push_back({0, trie[currentNode].depth + 1});
80 }
81 currentNode = trie[currentNode].children[charIndex];
82 trie[currentNode].count++;
83 }
84 }
85
86 // Find maximum depth among nodes with count >= k
87 int maxDepth = 0;
88 for (int i = 1; i < trie.size(); ++i) {
89 if (trie[i].count >= k) {
90 maxDepth = max(maxDepth, trie[i].depth);
91 }
92 }
93
94 // Count nodes at each depth that have count >= k
95 vector<int> globalCount(maxDepth + 1, 0);
96 for (int i = 1; i < trie.size(); ++i) {
97 if (trie[i].count >= k && trie[i].depth <= maxDepth) {
98 globalCount[trie[i].depth]++;
99 }
100 }
101
102 // Find fragile depths for each word (nodes with exactly k occurrences)
103 vector<vector<int>> fragileDepths(n);
104 for (int i = 0; i < n; ++i) {
105 int currentNode = 0;
106 for (char ch : words[i]) {
107 int charIndex = ch - 'a';
108 currentNode = trie[currentNode].children[charIndex];
109 if (trie[currentNode].count == k) {
110 fragileDepths[i].push_back(trie[currentNode].depth);
111 }
112 }
113 }
114
115 // Process each word using segment tree if there are valid depths
116 int segmentSize = maxDepth;
117 if (segmentSize >= 1) {
118 SegmentTree segmentTree(segmentSize, globalCount);
119
120 for (int i = 0; i < n; ++i) {
121 if (n - 1 < k) {
122 result[i] = 0;
123 } else {
124 // Temporarily decrease counts for fragile depths
125 for (int depth : fragileDepths[i]) {
126 segmentTree.update(1, 1, segmentSize, depth, globalCount[depth] - 1);
127 }
128
129 // Query maximum valid depth
130 int maxValidDepth = segmentTree.query();
131 result[i] = maxValidDepth == -1 ? 0 : maxValidDepth;
132
133 // Restore original counts
134 for (int depth : fragileDepths[i]) {
135 segmentTree.update(1, 1, segmentSize, depth, globalCount[depth]);
136 }
137 }
138 }
139 }
140
141 return result;
142 }
143};
144
1// Trie node structure to store prefix tree information
2interface TrieNode {
3 count: number; // Number of words passing through this node
4 depth: number; // Depth of this node in the trie (length of prefix)
5 children: number[]; // Child node indices for each letter a-z (26 letters)
6}
7
8// Segment tree for efficient range maximum queries
9class SegmentTree {
10 private n: number; // Size of the segment tree
11 private tree: number[]; // Segment tree array
12 private globalCount: number[]; // Reference to global count array
13
14 // Constructor initializes segment tree with given size and count array
15 constructor(n: number, globalCount: number[]) {
16 this.n = n;
17 this.globalCount = globalCount;
18 this.tree = new Array(4 * (n + 1)).fill(-1);
19 this.build(1, 1, n);
20 }
21
22 // Build segment tree recursively
23 private build(nodeIdx: number, left: number, right: number): void {
24 if (left === right) {
25 // Leaf node: store depth if count > 0, otherwise -1
26 this.tree[nodeIdx] = this.globalCount[left] > 0 ? left : -1;
27 return;
28 }
29 const mid = Math.floor((left + right) / 2);
30 this.build(nodeIdx * 2, left, mid);
31 this.build(nodeIdx * 2 + 1, mid + 1, right);
32 // Internal node stores maximum of children
33 this.tree[nodeIdx] = Math.max(this.tree[nodeIdx * 2], this.tree[nodeIdx * 2 + 1]);
34 }
35
36 // Update a single position in the segment tree
37 update(nodeIdx: number, left: number, right: number, position: number, newValue: number): void {
38 if (left === right) {
39 // Update leaf node based on new value
40 this.tree[nodeIdx] = newValue > 0 ? left : -1;
41 return;
42 }
43 const mid = Math.floor((left + right) / 2);
44 if (position <= mid) {
45 this.update(nodeIdx * 2, left, mid, position, newValue);
46 } else {
47 this.update(nodeIdx * 2 + 1, mid + 1, right, position, newValue);
48 }
49 // Update internal node with maximum of children
50 this.tree[nodeIdx] = Math.max(this.tree[nodeIdx * 2], this.tree[nodeIdx * 2 + 1]);
51 }
52
53 // Query the maximum value in the entire range
54 query(): number {
55 return this.tree[1]; // Root contains the maximum
56 }
57}
58
59function longestCommonPrefix(words: string[], k: number): number[] {
60 const n = words.length;
61 const result: number[] = new Array(n).fill(0);
62
63 // Early return if not enough words for k-common prefix
64 if (n - 1 < k) {
65 return result;
66 }
67
68 // Build trie from all words
69 const trie: TrieNode[] = [
70 { count: 0, depth: 0, children: new Array(26).fill(0) } // Initialize with root node
71 ];
72
73 for (const word of words) {
74 let currentNode = 0;
75 for (const ch of word) {
76 const charIndex = ch.charCodeAt(0) - 'a'.charCodeAt(0);
77 // Create new node if path doesn't exist
78 if (trie[currentNode].children[charIndex] === 0) {
79 trie[currentNode].children[charIndex] = trie.length;
80 trie.push({
81 count: 0,
82 depth: trie[currentNode].depth + 1,
83 children: new Array(26).fill(0)
84 });
85 }
86 currentNode = trie[currentNode].children[charIndex];
87 trie[currentNode].count++;
88 }
89 }
90
91 // Find maximum depth among nodes with count >= k
92 let maxDepth = 0;
93 for (let i = 1; i < trie.length; i++) {
94 if (trie[i].count >= k) {
95 maxDepth = Math.max(maxDepth, trie[i].depth);
96 }
97 }
98
99 // Count nodes at each depth that have count >= k
100 const globalCount: number[] = new Array(maxDepth + 1).fill(0);
101 for (let i = 1; i < trie.length; i++) {
102 if (trie[i].count >= k && trie[i].depth <= maxDepth) {
103 globalCount[trie[i].depth]++;
104 }
105 }
106
107 // Find fragile depths for each word (nodes with exactly k occurrences)
108 const fragileDepths: number[][] = Array.from({ length: n }, () => []);
109 for (let i = 0; i < n; i++) {
110 let currentNode = 0;
111 for (const ch of words[i]) {
112 const charIndex = ch.charCodeAt(0) - 'a'.charCodeAt(0);
113 currentNode = trie[currentNode].children[charIndex];
114 if (trie[currentNode].count === k) {
115 fragileDepths[i].push(trie[currentNode].depth);
116 }
117 }
118 }
119
120 // Process each word using segment tree if there are valid depths
121 const segmentSize = maxDepth;
122 if (segmentSize >= 1) {
123 const segmentTree = new SegmentTree(segmentSize, globalCount);
124
125 for (let i = 0; i < n; i++) {
126 if (n - 1 < k) {
127 result[i] = 0;
128 } else {
129 // Temporarily decrease counts for fragile depths
130 for (const depth of fragileDepths[i]) {
131 segmentTree.update(1, 1, segmentSize, depth, globalCount[depth] - 1);
132 }
133
134 // Query maximum valid depth
135 const maxValidDepth = segmentTree.query();
136 result[i] = maxValidDepth === -1 ? 0 : maxValidDepth;
137
138 // Restore original counts
139 for (const depth of fragileDepths[i]) {
140 segmentTree.update(1, 1, segmentSize, depth, globalCount[depth]);
141 }
142 }
143 }
144 }
145
146 return result;
147}
148
Time and Space Complexity
Time Complexity: O(n * m + n * f * log(m))
Breaking down the time complexity:
- Building the Trie:
O(n * m)
wheren
is the number of words andm
is the average length of each word - Traversing the Trie to calculate
maxDepth
andglobalCount
:O(T)
whereT
is the total number of nodes in the Trie, which is at mostO(n * m)
- Building the fragile list:
O(n * m)
as we traverse each word again - Building the Segment Tree:
O(maxDepth)
which is at mostO(m)
- For each word, updating and querying the Segment Tree:
O(n * f * log(maxDepth))
wheref
is the average number of fragile nodes per word, and each update/query takesO(log(maxDepth))
time
The dominant term is O(n * m)
for simple cases, but when there are many fragile nodes, the segment tree operations become significant, resulting in O(n * m + n * f * log(m))
.
Space Complexity: O(n * m)
Breaking down the space complexity:
- Trie storage:
O(n * m)
in the worst case where all words have unique prefixes - Each TrieNode contains an array of size 26, so the actual space is
O(26 * T)
whereT
is the number of Trie nodes - Segment Tree:
O(4 * maxDepth)
which isO(m)
globalCount
array:O(maxDepth)
which isO(m)
fragileList
:O(n * f)
wheref
is the average number of fragile nodes per word- Answer array:
O(n)
The dominant term is the Trie storage at O(n * m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Empty Fragile Depths
Pitfall: When a word doesn't pass through any nodes with exactly k
count (no fragile nodes), the code still processes the segment tree update/restore loop with an empty list. While this doesn't cause errors, it can lead to confusion about whether the answer should be the global maximum or 0.
Example Scenario:
words = ["ab", "abc", "abcd", "abcde"]
,k = 2
- The word "abcde" might not have any fragile nodes if all its prefix nodes have count > k
Solution: Explicitly handle the case when there are no fragile depths:
for i in range(n):
if n - 1 < k:
result[i] = 0
elif len(fragile_depths[i]) == 0:
# No fragile nodes means removing this word doesn't affect any k-threshold
# Simply query the current maximum
result[i] = segment_tree.query()
result[i] = 0 if result[i] == -1 else result[i]
else:
# Process fragile depths as before
for depth in fragile_depths[i]:
segment_tree.update(1, 1, segment_size, depth, global_count[depth] - 1)
max_valid_depth = segment_tree.query()
result[i] = 0 if max_valid_depth == -1 else max_valid_depth
for depth in fragile_depths[i]:
segment_tree.update(1, 1, segment_size, depth, global_count[depth])
2. Off-by-One Error in Segment Tree Indexing
Pitfall: The segment tree uses 1-based indexing for depths (1 to max_depth), but Python lists are 0-indexed. Mixing these can cause accessing invalid indices or missing depth 0.
Example Issue: If global_count
is 0-indexed but segment tree expects 1-indexed depths, global_count[0]
might be ignored.
Solution: Ensure consistent indexing throughout:
# Option 1: Make global_count 1-indexed by padding with dummy value at index 0
global_count = [0] * (max_depth + 1) # Current approach is correct
# Option 2: Adjust segment tree to handle 0-indexed arrays
class SegmentTree:
def __init__(self, n: int, global_count: List[int]):
self.n = n
self.global_count = global_count
self.tree = [-1] * (4 * (n + 1))
if n >= 1:
self.build(1, 0, n - 1) # Use 0-based range
3. Missing Edge Case: All Words Share No Common Prefix with k Others
Pitfall: If no depth has at least k
nodes (meaning no prefix is shared by at least k
words), max_depth
remains 0, and the segment tree might not be properly initialized.
Example: words = ["a", "b", "c", "d"]
, k = 2
- no two words share any prefix.
Solution: Add explicit check for this case:
# After calculating max_depth if max_depth == 0: # No valid prefixes exist with count >= k return [0] * n # Continue with segment tree construction only if max_depth > 0
4. Memory Optimization Overlooked for Large Alphabets
Pitfall: The original Java solution uses fixed-size arrays for children (children[26]
), but the Python version uses dictionaries. While more memory-efficient for sparse tries, dictionary operations are slightly slower.
Trade-off Consideration: For problems with full lowercase alphabet usage, consider using lists:
# Instead of: 'children': {}
# Consider: 'children': [-1] * 26 # for lowercase letters only
# Then access would be:
char_index = ord(ch) - ord('a')
if trie[current_node]['children'][char_index] == -1:
trie[current_node]['children'][char_index] = len(trie)
# Create new node
This trades memory for speed but should be chosen based on the specific constraints and character set size.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
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!