This document analyzes the subsumption logic used in the Levenshtein automaton implementation, comparing the Rust and C++ approaches and identifying optimization opportunities.
Subsumption is a critical optimization for Levenshtein automata. A position p1 at (i, e) subsumes position p2 at (j, f) if all candidates reachable from p2 are also reachable from p1. This allows us to prune redundant positions from states, reducing the state space and improving performance.
p1 subsumes p2 if:
e ≤ f (fewer or equal errors)|i - j| ≤ (f - e) (index difference bounded by error difference)More complex due to special positions tracking transposition states:
p1 and p2 are special: i == jp1 special, p2 not: i == jp2 special (not p1): adjusted_diff ≤ (f - e) where:
adjusted_diff = (j < i) ? (i - j - 1) : (j - i + 1)|i - j| ≤ (f - e)p1 special but not p2: cannot subsume (return false)|i - j| ≤ (f - e)Process:
next_state without checking subsumptionunsubsume(next_state, query_length) to remove redundant positionsUnsubsume Implementation (unsubsume.cpp:10-38):
void UnsubsumeFn::operator()(State *state, std::size_t query_length) {
StateIterator outer_iter = state->begin();
StateIterator iter_end = state->end();
while (outer_iter != iter_end) {
Position *outer = *outer_iter;
std::size_t outer_errors = outer->num_errors();
// Skip positions with more errors (optimization)
StateIterator inner_iter(state, outer, &outer_iter);
++inner_iter;
while (inner_iter != iter_end) {
Position *inner = *inner_iter;
if (outer_errors < inner->num_errors()) {
break;
}
++inner_iter;
}
// Remove subsumed positions
while (inner_iter != iter_end) {
Position *inner = *inner_iter;
if (subsumes(outer, inner, query_length)) {
inner_iter.remove();
}
++inner_iter;
}
++outer_iter;
}
}
Complexity: O(n²) where n is the number of positions in the state
Process (state.rs:54-71):
pub fn insert(&mut self, position: Position, algorithm: Algorithm) {
// Check if this position is subsumed by an existing one
for existing in &self.positions {
if existing.subsumes(&position, algorithm) {
return; // Early exit - O(n) in best case
}
}
// Remove any positions that this new position subsumes
self.positions.retain(|p| !position.subsumes(p, algorithm)); // O(n)
// Insert in sorted position
let insert_pos = self
.positions
.binary_search(&position)
.unwrap_or_else(|pos| pos); // O(log n)
self.positions.insert(insert_pos, position); // O(n)
}
Complexity: O(n) per insertion for typical cases, O(kn) total for k insertions
Advantages:
Disadvantages:
Best Case: O(n²) - when no positions subsume each other Worst Case: O(n²) - when all positions subsume each other Average Case: O(n²)
Advantages:
Disadvantages:
Best Case: O(k) - when all positions are subsumed (early exit) Worst Case: O(kn) - when no positions subsume each other Average Case: O(kn) where k < n typically due to subsumption
From profiling data (pool.rs tests):
Implication: For small n (typical case), the difference between O(n²) and O(kn) is minimal. However, the constant factors and early termination matter more.
Common patterns observed:
(0, 0) in initial state subsumes many positions with higher errorsRust advantage: Early exit optimization in insert() is particularly effective for high subsumption scenarios.
Early Termination in retain():
retain() checks all positionsSorted Insertion with Subsumption:
Batch Hints:
SIMD Subsumption Checks:
Position Metadata:
Sort Before Unsubsume:
Incremental Unsubsumption:
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |