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 |