Remake trie.py using tokens instead of substrings
dkl9

dkl9 commited on 2025-201 00:09:44
Showing 1 changed files, with 53 additions and 72 deletions.

... ...
@@ -1,5 +1,7 @@
1
-import os
1
+from collections import defaultdict
2
+import re
2 3
 import sys
4
+from typing import Generator
3 5
 
4 6
 def minima(options, key):
5 7
     min_val = None
... ...
@@ -13,90 +15,69 @@ def minima(options, key):
13 15
             result.add(o)
14 16
     return result
15 17
 
16
-class RadixTree:
17
-    def __init__(self, label: str = None, parent: "RadixTree" = None):
18
-        self.label = label or ""
19
-        self.children: set[RadixTree] = set()
20
-        self.parent = parent
21
-        self.weight = 1 if label is not None else 0
22
-        self.depth = parent.depth + 1 if parent else 0
23
-        self.usage = 0
18
+def cost(token: str, child: "Trie") -> tuple:
19
+    weight = child.deep_weight()
20
+    return child.deep_usage() / weight, -weight, len(token), token
24 21
 
25
-    def __str__(self):
26
-        s = f"{self.label or "∅"}"
22
+class Trie:
23
+    def __init__(self, weight: int = 0):
24
+        self.children: defaultdict[str, Trie] = defaultdict(Trie)
25
+        self.weight: int = weight
26
+        self.usage: int = 0
27
+
28
+    def __str__(self) -> str:
29
+        s = f"[{self.usage}/{self.weight}]"
27 30
         if self.children:
28
-            s = f"({s} {" ".join(str(x) for x in self.children)})"
31
+            s += f"({" ".join('"' + k + '"' + str(v) for (k, v) in self.children.items())})"
29 32
         return s
30 33
 
31
-    def add(self, word: str):
32
-        common = os.path.commonprefix([self.label, word])
33
-        suffix = word[len(common):]
34
-        if common == self.label:
35
-            for child in self.children:
36
-                if child.label:
37
-                    if child.add(suffix):
38
-                        break
34
+    def add(self, word: list[str], weight: int = 1):
35
+        if word:
36
+            self.children[word[0]].add(word[1:], weight)
39 37
         else:
40
-                if not self.children:
41
-                    self.children.add(RadixTree("", self))
42
-                self.children.add(RadixTree(suffix, self))
43
-        elif common:
44
-            remainder = self.label[len(common):]
45
-            split = RadixTree(remainder, self)
46
-            split.weight = self.weight
47
-            split.usage = self.usage
48
-            split.children = self.children
49
-            for child in split.children:
50
-                child.parent = split
51
-            self.label = common
52
-            self.children = {RadixTree(suffix, self), split}
53
-        if common or not self.label:
54
-            self.weight += 1
55
-            return True
38
+            self.weight += weight
56 39
 
57
-    def full(self) -> str:
58
-        s = self.label
59
-        if self.parent:
60
-            s = self.parent.full() + s
61
-        return s
40
+    def deep_weight(self) -> int:
41
+        return self.weight + sum(c.deep_weight() for c in self.children.values())
62 42
 
63
-    def del_empty(self):
64
-        for child in list(self.children):
65
-            if not child.label:
66
-                if child.children:
67
-                    child.del_empty()
68
-                else:
69
-                    self.children.remove(child)
43
+    def deep_usage(self) -> int:
44
+        return self.usage + sum(c.deep_usage() for c in self.children.values())
70 45
 
71
-    def leaves(self) -> set["RadixTree"]:
72
-        if self.children:
73
-            return {l for c in self.children for l in c.leaves()}
46
+    def best(self) -> tuple[list[str], "Trie"]:
47
+        if self.weight > self.usage:
48
+            return [], self
49
+        elif self.children:
50
+            token, child = min(
51
+                self.children.items(),
52
+                key=lambda kc: cost(*kc)
53
+            )
54
+            suffix, leaf = child.best()
55
+            if not leaf:
56
+                return None, None
57
+            return [token] + suffix, leaf
74 58
         else:
75
-            return {self}
59
+            return None, None
76 60
 
77 61
     def use(self):
78 62
         assert self.usage < self.weight
79 63
         self.usage += 1
80
-        if self.parent:
81
-            self.parent.use()
82 64
 
83
-    def best(self) -> "RadixTree":
84
-        if self.children:
85
-            branches = minima(self.children, lambda c: (c.usage / c.weight, -(c.weight // c.depth)))
86
-            return min((b.best() for b in branches), key=lambda b: (len(b.full()), b.full()))
87
-        else:
88
-            return self
65
+    def traverse(self) -> Generator[tuple[list[str], "Trie"], None, None]:
66
+        while True:
67
+            tokens, leaf = self.best()
68
+            if not leaf:
69
+                assert self.deep_usage() == self.deep_weight()
70
+                break
71
+            yield tokens
72
+            leaf.use()
89 73
 
90
-t = RadixTree()
91
-words = {w.strip() for w in sys.stdin}
74
+re_flags = re.MULTILINE | re.DOTALL
75
+token_pattern = "[-_ \t]*[^-_\\s]+"
76
+entry_pattern = re.compile(f"({token_pattern})+", re_flags)
77
+token_pattern = re.compile(token_pattern, re_flags)
78
+words = (re.findall(token_pattern, m.group(0)) for m in re.finditer(entry_pattern, sys.stdin.read()))
79
+t = Trie()
92 80
 for w in words:
93
-    t.add(w.strip())
94
-if "" not in words:
95
-    t.del_empty()
96
-leaves = {l.full() for l in t.leaves()}
97
-assert leaves <= words
98
-assert words <= leaves
99
-while t.usage < t.weight:
100
-    b = t.best()
101
-    b.use()
102
-    print(b.full())
81
+    t.add(w)
82
+for w in t.traverse():
83
+    print("".join(w))
103 84