Implement a Longest-Match Tokenizer
AnthropicSoftware Engineer · OnsiteLast updated
Design and implement a greedy longest-match text tokenizer that validates vocabulary, supports configurable unknown-token coalescing, and emits token IDs alongside their exact substring matches.
Problem Statement
Implement a greedy, case-sensitive longest-match tokenizer (LongestMatchTokenizer). Given a vocabulary mapping token strings to unique integer IDs, the tokenizer scans input text from left to right, always matching the longest possible vocabulary token starting at the current position.
Your implementation must support the following features:
- Vocabulary Validation: During initialization, ensure that token strings are non-empty and mapped IDs are non-negative. Raise a
ValueErrorif any token is empty or if any ID is negative. - Longest-Match Tokenization: At each position
i, match the longest vocabulary token that equals a prefix oftext[i:]. - Unknown Handling (
coalesce_unk):- When
coalesce_unk=False, an unmatched character emits(unk_id, char)and advances the cursor by 1 code point. - When
coalesce_unk=True, consecutive characters that cannot start any vocabulary match must be grouped into a single unknown token:(unk_id, unk_span).
- When
- Unicode Safety: Handle arbitrary Unicode code points accurately without corrupting surrogate pairs or multi-byte characters.
Interface
class LongestMatchTokenizer:
def __init__(self, vocab: dict[str, int], unk_id: int = -1, coalesce_unk: bool = False):
"""
Initializes the tokenizer with a vocabulary dictionary.
Raises ValueError on invalid vocabulary entries.
"""
pass
def tokenize(self, text: str) -> list[tuple[int, str]]:
"""
Tokenizes the input text into a list of (token_id, text_span) tuples.
"""
pass
Examples
Example 1
vocab = {"app": 1, "apple": 2, "pie": 3, "let": 4}
tokenizer = LongestMatchTokenizer(vocab, unk_id=-1, coalesce_unk=False)
tokens = tokenizer.tokenize("applepie!")
# Output: [(2, "apple"), (3, "pie"), (-1, "!")]
# Explanation: At index 0, both "app" and "apple" match; "apple" is longer, so ID 2 is chosen.
# Then "pie" matches at index 5. Index 8 is '!', which is not in vocab, emitting (-1, "!").
Example 2
vocab = {"cat": 10, "cater": 11, "pillar": 12}
tokenizer = LongestMatchTokenizer(vocab, unk_id=0, coalesce_unk=True)
tokens = tokenizer.tokenize("cat xyz cater")
# Output: [(10, "cat"), (0, " xyz "), (11, "cater")]
# Explanation: "cat" matches first. The sequence " xyz " cannot form any match starting at
# indices 3, 4, 5, 6, or 7. Because coalesce_unk=True, it is merged into a single span.
Constraints
1 <= len(vocab) <= 100,000- Token lengths in
vocabare between1and100Unicode code points. 0 <= len(text) <= 500,000- Token IDs are integers in
[0, 10^9]. - Tokenization should run in
O(N * L_max)time, whereNislen(text)andL_maxis the maximum token length invocab.
Hints
Hint 1
Store the vocabulary in a Trie where each node indicates whether it terminates a valid token and holds that token's ID.
Hint 2
For greedy matching, traverse down the Trie from the current index as far as possible, recording the deepest terminal node encountered.
Hint 3
To coalesce unknown characters, accumulate consecutive indices that have no valid outgoing Trie match from their starting position, stopping as soon as a future index finds a non-empty match.
Follow-up questions
- How would you handle streaming input where a chunk boundary might split a multi-character token across network packets?
- How does the Trie-based lookup compare in memory footprint and CPU cache performance against a hash set with length-bucketed lookups?
- How would you extend this architecture to support Byte-Pair Encoding (BPE) merge rules or WordPiece subword prefixes (e.g., '##')?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.