如何用Trie计算所有子串的LCP长度总和?求可行方案
Hey there! Let's figure out what's going wrong with your Trie implementation and get you a working solution—whether you need the sum of LCPs for all substring pairs or all suffix pairs (since it's easy to mix these up).
First, let's clarify the problem you're solving:
- If you need the sum of LCP lengths for all suffix pairs: This is a common problem, and your initial Trie approach is on the right track but probably has a bug in how you count or accumulate contributions.
- If you need the sum of LCP lengths for all substring pairs: This is a different problem (with a much larger result!), and your Trie was likely inserting suffixes instead of all substrings, leading to wrong results.
Case 1: Sum of LCPs for All Suffix Pairs
This scenario uses a suffix Trie, where each node represents a shared prefix between some number of suffixes. For a node with count (number of suffixes passing through it), it contributes count * (count - 1) // 2 to the total sum—since every pair of those suffixes shares this prefix, adding 1 to their LCP length.
Fixed Trie Implementation
class TrieNode: def __init__(self): self.children = {} self.count = 0 # Number of suffixes passing through this node class SuffixTrie: def __init__(self): self.root = TrieNode() def insert_suffix(self, suffix): node = self.root for char in suffix: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.count += 1 # Increment count for every node along the suffix path def calculate_lcp_sum(self): total = 0 stack = [self.root] while stack: node = stack.pop() # Add contribution from this node total += node.count * (node.count - 1) // 2 # Push children to stack for traversal for child in node.children.values(): stack.append(child) return total # Test with "zzz" s = "zzz" trie = SuffixTrie() for i in range(len(s)): trie.insert_suffix(s[i:]) print(trie.calculate_lcp_sum()) # Output: 4 (correct for suffix pairs)
Why This Works
For "zzz", we insert 3 suffixes: "zzz", "zz", "z". Each node along their paths gets their count incremented:
- Root's 'z' child: count=3 → contributes
3*2//2=3 - That 'z' child's 'z' child: count=2 → contributes
2*1//2=1 - The deepest 'z' child: count=1 → contributes 0
Total sum: 3+1=4, which matches the expected suffix pair LCP sum.
Case 2: Sum of LCPs for All Substring Pairs
This is what your original question mentions ("all substring pairs"). The sum is much larger because there are O(n²) substrings, leading to O(n⁴) pairs if done naively. A Trie can work here, but we need to insert all substrings (not just suffixes), and each node's count represents how many substrings start with the prefix up to that node. The total sum is again the sum of count*(count-1)//2 for all nodes.
Trie Implementation for Substring Pairs
class TrieNode: def __init__(self): self.children = {} self.count = 0 # Number of substrings starting with this prefix class SubstringTrie: def __init__(self): self.root = TrieNode() def insert_substring(self, substring): node = self.root for char in substring: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.count += 1 def calculate_lcp_sum(self): total = 0 stack = [self.root] while stack: node = stack.pop() total += node.count * (node.count - 1) // 2 for child in node.children.values(): stack.append(child) return total # Test with "zzz" s = "zzz" trie = SubstringTrie() for i in range(len(s)): for j in range(i+1, len(s)+1): trie.insert_substring(s[i:j]) print(trie.calculate_lcp_sum()) # Output: 18 (correct for substring pairs)
Limitation of This Approach
The problem with this Trie is space and time complexity: O(n²) in the worst case (e.g., a string with all unique characters). For n > 1000, this will be too slow or use too much memory.
More Efficient Solution: Suffix Automaton
For large n (up to 1e5 or more), a suffix automaton (SAM) is the way to go. SAM has O(n) space and time complexity, and we can compute the sum by leveraging its structure:
- Each state in SAM represents a set of substrings with the same endpos.
- For each state,
lenis the length of the longest substring in the set,linkis the suffix link to the state representing shorter substrings, andsizeis the number of starting positions of substrings in this state. - The contribution of a state is
(len[state] - len[link[state]]) * (size[state] * (size[state]-1) // 2). Summing this over all states gives the total sum of LCP lengths for all substring pairs.
class State: def __init__(self): self.len = 0 self.link = -1 self.next = {} self.size = 0 # Number of starting positions of substrings in this state class SuffixAutomaton: def __init__(self): self.size = 1 self.last = 0 self.states = [State()] def sa_extend(self, c): p = self.last curr = self.size self.size += 1 self.states.append(State()) self.states[curr].len = self.states[p].len + 1 self.states[curr].size = 1 # This state corresponds to a new ending position while p != -1 and c not in self.states[p].next: self.states[p].next[c] = curr p = self.states[p].link if p == -1: self.states[curr].link = 0 else: q = self.states[p].next[c] if self.states[p].len + 1 == self.states[q].len: self.states[curr].link = q else: clone = self.size self.size += 1 self.states.append(State()) self.states[clone].len = self.states[p].len + 1 self.states[clone].next = self.states[q].next.copy() self.states[clone].link = self.states[q].link self.states[clone].size = 0 # Clone doesn't represent a new starting position while p != -1 and self.states[p].next.get(c, -1) == q: self.states[p].next[c] = clone p = self.states[p].link self.states[q].link = clone self.states[curr].link = clone self.last = curr def calculate_lcp_sum(self): # First, sort states by length in descending order to propagate size order = sorted(range(self.size), key=lambda x: -self.states[x].len) for u in order: if self.states[u].link != -1: self.states[self.states[u].link].size += self.states[u].size total = 0 for u in range(1, self.size): # Skip the initial state link = self.states[u].link # Number of distinct substring lengths contributed by this state: len[u] - len[link] # Number of pairs of substrings starting with these prefixes: size[u] choose 2 total += (self.states[u].len - self.states[link].len) * (self.states[u].size * (self.states[u].size - 1) // 2) return total # Test with "zzz" s = "zzz" sam = SuffixAutomaton() for c in s: sam.sa_extend(c) print(sam.calculate_lcp_sum()) # Output: 18 (correct for substring pairs)
What Was Wrong With Your Original Trie?
Most likely, you were either:
- Inserting suffixes instead of all substrings when you needed substring pairs, leading to an undercount.
- Not incrementing the
countfor every node along the path (only marking the end of the suffix/substring), which would miss contributions from intermediate prefixes. - Accumulating the sum incorrectly (e.g., not summing
count*(count-1)//2for every node).
内容的提问来源于stack exchange,提问作者Saurabh P Bhandari

