Algorithmics study and revision notes
2026-08-22
Approximate matching finds text regions close to a pattern under a stated error model. It supports spelling correction, biological sequence search, record linkage, and noisy measurements. “Similar” is not an algorithmic specification: the distance, threshold, occurrence boundaries, and output policy must all be defined.
Dynamic programming gives a general baseline. Faster methods exploit a small threshold, reject impossible candidates cheaply, pack states into words, or preprocess the text.
After studying this note, you should be able to:
You should know the edit-distance recurrence from the dynamic-programming note and exact search from the exact-matching note. The recurrence and required exact-search facts are restated here so this chapter can be read independently.
Let pattern and text be strings over alphabet .
One can report every qualifying pair (start,end), one
best start for each end, nonoverlapping matches, or maximal regions.
These are different output problems. The basic DP below gives the best
score for each endpoint and can recover one tied start by traceback.
Enumerating all tied starts can require additional output-sensitive
work.
Let be the minimum edit cost of matching pattern prefix to a suffix of text prefix . Thus column is anchored at text boundary , although final deletions need not consume a text character. For unit costs use
The crucial boundaries are
The free top row discards any text prefix before a candidate start. In pairwise global distance it would instead be .
approximate_endpoints(P, T, k):
for j = 0 .. n: D[0,j] = 0
for i = 1 .. m: D[i,0] = i
for j = 1 .. n:
for i = 1 .. m:
D[i,j] = min(D[i-1,j]+1,
D[i,j-1]+1,
D[i-1,j-1] + (P[i] != T[j]))
if D[m,j] <= k: report endpoint j
The invariant is exactly the state definition. Consider the final operation of an optimum alignment: deletion, insertion, or diagonal match/substitution gives the three candidates. Conversely, appending the stated operation to an optimal predecessor gives a feasible alignment. Induction over the grid proves the recurrence.
The full method takes time and space. Two columns suffice for scores in space. To recover a start and edit script, retain predecessors or recompute a region when an endpoint is found.
For pattern abc and text zabxc, the
substring-search table is:
| z | a | b | x | c | ||
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | |
| a | 1 | 1 | 0 | 1 | 1 | 1 |
| b | 2 | 2 | 1 | 0 | 1 | 2 |
| c | 3 | 3 | 2 | 1 | 1 | 1 |
With
,
endpoint 4 represents abx with one substitution; endpoint 5
represents abxc with insertion x. Tracing the
second path from
reaches row zero at column 1, so the occurrence starts at text position
2. The leading z costs nothing because the top row is
zero.
An edit rule may replace a whole string by another string , rather than consuming at most one character from each side. Store every finite-cost rule as
where the two strings are not both empty. Equivalently, every pair has a cost, with meaning that the operation is disallowed. An implementation stores only the finite catalogue.
This chapter uses a left-to-right non-overlapping alignment model: one rule consumes a block of the original pattern and a block of the chosen text substring, and its output is not edited again. Unrestricted repeated string rewriting is a different problem and is not computed by this acyclic DP.
Ordinary weighted edit distance is the special case containing
identity rules
,
deletions
,
insertions
,
and substitutions
.
Multi-character rules can represent historical spelling, pronunciation,
dialect, or transliteration, such as hw
f and a
aa. The direction matters: here rules transform the pattern
into a text substring, so the cost need not be symmetric.
Let be the minimum cost of aligning pattern prefix with text substring material ending at boundary . A rule is applicable at when is a suffix of and is a suffix of . Write and . Then
For global distance initialise and every other entry to . Insertions and deletions, if permitted, generate the remaining boundary values through their own rules. For approximate substring search instead initialise
Then reports an occurrence ending at boundary , and traceback to row zero recovers a start. Every transition strictly increases , even if one rule side is empty, so row-major evaluation is a valid topological order. Correctness follows by induction: every nonempty alignment has one final rule represented by the recurrence, and appending any applicable rule to a predecessor constructs a valid alignment.
Rescanning every and at every matrix cell is unnecessarily expensive. For each distinct nonempty , run KMP once over and record the boundaries where it ends. Do the same for each distinct nonempty over . An empty side matches every boundary. When the catalogue contains many distinct strings, one Aho–Corasick automaton per side replaces the repeated KMP scans with one multi-pattern scan.
It helps to separate two layers. Endpoint matching answers only where each rule side occurs; the DP then joins compatible pattern and text endpoints into a minimum-cost path. KMP or Aho–Corasick accelerates the first layer but does not choose edits or remove the need to process genuinely applicable rules.
rule_endpoints(S, side, rules):
occurrence_ends[epsilon] = [0, 1, ..., length(S)]
for each distinct nonempty string q among the rule sides:
occurrence_ends[q] = empty list
for start in kmp_all_occurrences(S, q):
occurrence_ends[q].append(start + length(q))
for each rule id r:
ends[r] = occurrence_ends[side(r)] // share equal-side lists
return ends
The lists for both sides determine exactly which cells admit each rule. Materialising these candidates makes the later DP bound explicit:
rule_candidates(P, T, rules):
Pend = rule_endpoints(P, alpha, rules)
Tend = rule_endpoints(T, beta, rules)
applicable[0 .. m, 0 .. n] = empty lists
for each rule id r:
for i in Pend[r]:
for j in Tend[r]:
applicable[i,j].append(r)
return applicable
Now evaluate the acyclic grid in row-major order:
generalised_edit(P, T, rules, substring_search, threshold):
applicable = rule_candidates(P, T, rules)
fill D[0 .. m, 0 .. n] with infinity
fill predecessor with NONE
if substring_search:
for j = 0 .. n: D[0,j] = 0
else:
D[0,0] = 0
for i = 0 .. m:
for j = 0 .. n:
for r in applicable[i,j]:
a = length(alpha[r]); b = length(beta[r])
if a + b = 0: continue
candidate = D[i-a,j-b] + weight[r]
if candidate < D[i,j]:
D[i,j] = candidate
predecessor[i,j] = r
if substring_search:
return all j with D[m,j] <= threshold
return D[m,n]
Suppose there are rules, distinct nonempty strings, distinct nonempty strings, and their total stored length is . Let be the number of occurrence endpoints reported for these distinct strings; rules with an identical side share one endpoint list. Repeated KMP preprocessing and assigning the shared lists to rules takes time. Aho–Corasick reduces the scanning part, giving under its alphabet-transition model.
Let
where and count endpoints of the two sides. The outer loop reads all rules and the nested endpoint loops construct exactly candidates. Initialising and filling the dense grid therefore takes time after matching.
The value, predecessor, and dense candidate-bucket grids use space. The candidate payload itself is , and a sparse map can avoid allocating empty buckets. If storing all candidates is undesirable, per-boundary rule bit sets can instead be intersected on demand in word operations.
In repetitive strings, many rules may really apply at the same cell. KMP and Aho–Corasick eliminate repeated character comparison, not that output-sensitive work. A two-row optimisation is not automatic because a rule may jump back several pattern positions.
Transform ahwrika into aafrika. In addition
to zero-cost identity rules and unit-cost single-character edits,
suppose the catalogue contains
The first rule gives
.
KMP marks hw as ending at pattern boundary 3 and
f as ending at target boundary 3, so the second rule
gives
The remaining r, i, k, and
a use identity rules, giving
.
Ordinary unit-cost Levenshtein distance can instead substitute
h by a and w by f,
with total cost 2; the block rules encode that these particular changes
should be considerably cheaper.
A unit-edit diagonal band is not automatically safe here. One cheap rule may consume strings of very different lengths and jump across many diagonals. Any safe band must be derived from the permitted rule lengths and a lower bound connecting their costs to net length change.
While verifying two strings from a fixed common start, any alignment using at most insertions and deletions satisfies : the difference in consumed prefix lengths changes by at most one per insertion or deletion. It is therefore safe to compute only that diagonal band. For strings of comparable length this takes time and or working space, depending on layout.
Substring search is different because the start offset is unknown. One fixed band around the table’s main diagonal is not safe: a valid occurrence far into the text lies around another diagonal. Safe choices include:
Capping values above at is useful but does not alone prove time. In repetitive input, many states may remain active. State the particular threshold algorithm and its assumptions rather than attributing every narrow-looking computation to “banding.”
A filter returns candidates cheaply. It may return false positives, but a correct filter must not discard a true match. A verifier then computes the real distance.
Assume , so can be partitioned into nonempty, nonoverlapping pieces. Under at most mismatches, at most pieces contain a changed position, so at least one piece occurs exactly at its expected offset. Search all pieces with KMP or Aho–Corasick, convert their hits into candidate starts, deduplicate, and verify the full window. When , this particular nonempty-piece filter provides no useful partition guarantee.
For edit distance, each edit can disrupt at most one disjoint piece, so an unchanged piece still occurs exactly, but preceding insertions and deletions shift its text position. If a piece begins at pattern offset and occurs at text position , a candidate start can lie within positions of . The verifier must also allow candidate lengths through . This positional slack is essential for a no-false-negative guarantee.
For ALGORITHM and
mismatches, use pieces ALG, ORI, and
THM. Every qualifying length-nine window preserves at least
one complete piece. An exact hit is only a candidate; Hamming
verification still decides whether the other positions contain at most
two errors.
Filtering helps when few candidates survive. On repetitive text or very short pieces, verification can dominate and the worst case can return to quadratic behaviour.
A q-gram is a length- substring, with . Pattern has overlapping q-grams counted by position. One substitution lies in at most of them. Therefore a window with at most mismatches has at least
position-aligned q-grams equal to those of . Rejecting a window below this threshold has no false negatives for the stated mismatch model.
An inverted q-gram index maps each gram to text or document positions. Candidate generation can count hits before verification. Multiset counting, position-aware counting, and document-level counting are not interchangeable. With insertions and deletions, positions shift and q-grams are created as well as destroyed; a safe edit-distance filter must use a theorem derived for its exact counting convention and must allow positional slack. Overlap is why one cannot count every disagreeing q-gram as an independent edit.
For substitutions only, extend Shift-And with one bit vector for each allowed error count . Bit says that prefix matches a suffix of the processed text using at most mismatches. Let mark positions of character , and initialise every to zero. Using old vectors from before the next text character :
The first term extends with a matching character; the second spends one mismatch. A match with at most errors ends when bit of is set. Update error layers from old copies or from high downward so a text character is not consumed twice.
If , this costs word operations and words. Multiple words extend longer patterns. Insertions and deletions require additional terms because one side may advance alone; Myers’ bit-vector algorithm encodes full edit-distance columns more compactly. Its speed is still a word-RAM result, not constant time independent of .
A pattern position may denote a character class, such as
[A-G], or a wildcard. Bit masks handle single-position
classes naturally by setting the corresponding bit in every permitted
character mask. An unbounded wildcard such as .* changes
the automaton structure and cannot be treated as one ordinary
position.
Nonuniform edit costs require the weighted DP model from the previous
note. Command-line fuzzy-search tools such as agrep may
expose mismatches, insertions, deletions, classes, or separate operation
costs. Before comparing speed, verify their distance semantics,
Unicode/byte model, line-boundary policy, and whether reported results
are exact under that policy.
| Method | Main time bound | Best use |
|---|---|---|
| Full substring DP | general baseline and traceback | |
| Generalised-rule DP | after endpoint matching | transliteration and spelling variants |
| Banded fixed-start verifier | a known candidate start | |
| piece filter | exact-search cost plus verification | small , selective pieces |
| q-gram index | candidate dependent | repeated queries or documents |
| Bit-parallel mismatches | word operations | short patterns, small |
abc against the occurrence
abxc.a
aa lead from
to
?bio::pattern_matching::myers
reports substring matches within unit edit distance and can recover
starts and edit paths over byte slices. find_all_end
returns an inclusive end index, while find_all returns a
half-open (start,end) range. The simple form is limited by
its 64- or 128-bit vector; the block form supports longer patterns with
threshold-dependent work.regex
allows bounded or weighted single-character insertions, deletions, and
substitutions inside a regular expression. Default,
ENHANCEMATCH, and BESTMATCH choose different
search policies, and none supplies a catalogue of multi-character block
rules. Unicode-string spans use Python string indices; bytes-pattern
spans use byte indices.{a,b} of length at
most 4. Assert that your unit-cost DP, RapidFuzz, and Edlib global mode
return the same distance, then test how each API signals that a cutoff
was exceeded.abc and text
zabxc, request locations and a path, and compare its chosen
best span with the qualifying endpoints in the worked table. Explain any
tie. Separately log the applicable cells of the a
aa and hw
f rules to see what endpoint preprocessing
contributes.