stride-align 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/NOTICE +255 -0
- data/README.md +107 -0
- data/data/bmpm_data/gen_approx_any.txt +131 -0
- data/data/bmpm_data/gen_approx_arabic.txt +26 -0
- data/data/bmpm_data/gen_approx_common.txt +233 -0
- data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_approx_czech.txt +18 -0
- data/data/bmpm_data/gen_approx_dutch.txt +18 -0
- data/data/bmpm_data/gen_approx_english.txt +47 -0
- data/data/bmpm_data/gen_approx_french.txt +25 -0
- data/data/bmpm_data/gen_approx_german.txt +73 -0
- data/data/bmpm_data/gen_approx_greek.txt +18 -0
- data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
- data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
- data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
- data/data/bmpm_data/gen_approx_italian.txt +18 -0
- data/data/bmpm_data/gen_approx_polish.txt +84 -0
- data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
- data/data/bmpm_data/gen_approx_romanian.txt +18 -0
- data/data/bmpm_data/gen_approx_russian.txt +48 -0
- data/data/bmpm_data/gen_approx_spanish.txt +21 -0
- data/data/bmpm_data/gen_approx_turkish.txt +18 -0
- data/data/bmpm_data/gen_exact_any.txt +40 -0
- data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
- data/data/bmpm_data/gen_exact_arabic.txt +18 -0
- data/data/bmpm_data/gen_exact_common.txt +32 -0
- data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_exact_czech.txt +18 -0
- data/data/bmpm_data/gen_exact_dutch.txt +18 -0
- data/data/bmpm_data/gen_exact_english.txt +18 -0
- data/data/bmpm_data/gen_exact_french.txt +18 -0
- data/data/bmpm_data/gen_exact_german.txt +18 -0
- data/data/bmpm_data/gen_exact_greek.txt +18 -0
- data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
- data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
- data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
- data/data/bmpm_data/gen_exact_italian.txt +18 -0
- data/data/bmpm_data/gen_exact_polish.txt +23 -0
- data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
- data/data/bmpm_data/gen_exact_romanian.txt +18 -0
- data/data/bmpm_data/gen_exact_russian.txt +19 -0
- data/data/bmpm_data/gen_exact_spanish.txt +19 -0
- data/data/bmpm_data/gen_exact_turkish.txt +18 -0
- data/data/bmpm_data/gen_hebrew_common.txt +113 -0
- data/data/bmpm_data/gen_lang.txt +295 -0
- data/data/bmpm_data/gen_languages.txt +36 -0
- data/data/bmpm_data/gen_rules_any.txt +367 -0
- data/data/bmpm_data/gen_rules_arabic.txt +76 -0
- data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
- data/data/bmpm_data/gen_rules_czech.txt +67 -0
- data/data/bmpm_data/gen_rules_dutch.txt +78 -0
- data/data/bmpm_data/gen_rules_english.txt +113 -0
- data/data/bmpm_data/gen_rules_french.txt +114 -0
- data/data/bmpm_data/gen_rules_german.txt +129 -0
- data/data/bmpm_data/gen_rules_greek.txt +97 -0
- data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
- data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
- data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
- data/data/bmpm_data/gen_rules_italian.txt +77 -0
- data/data/bmpm_data/gen_rules_polish.txt +185 -0
- data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
- data/data/bmpm_data/gen_rules_romanian.txt +64 -0
- data/data/bmpm_data/gen_rules_russian.txt +142 -0
- data/data/bmpm_data/gen_rules_spanish.txt +85 -0
- data/data/bmpm_data/gen_rules_turkish.txt +50 -0
- data/data/keyboard_data/qwerty.npy +0 -0
- data/data/matrix_data/BLOSUM100 +31 -0
- data/data/matrix_data/BLOSUM30 +31 -0
- data/data/matrix_data/BLOSUM35 +31 -0
- data/data/matrix_data/BLOSUM40 +31 -0
- data/data/matrix_data/BLOSUM45 +25 -0
- data/data/matrix_data/BLOSUM50 +25 -0
- data/data/matrix_data/BLOSUM55 +31 -0
- data/data/matrix_data/BLOSUM60 +31 -0
- data/data/matrix_data/BLOSUM62 +25 -0
- data/data/matrix_data/BLOSUM65 +31 -0
- data/data/matrix_data/BLOSUM70 +31 -0
- data/data/matrix_data/BLOSUM75 +31 -0
- data/data/matrix_data/BLOSUM80 +25 -0
- data/data/matrix_data/BLOSUM85 +31 -0
- data/data/matrix_data/BLOSUM90 +25 -0
- data/data/matrix_data/NUC.4.4 +25 -0
- data/data/matrix_data/PAM10 +34 -0
- data/data/matrix_data/PAM100 +34 -0
- data/data/matrix_data/PAM110 +34 -0
- data/data/matrix_data/PAM120 +34 -0
- data/data/matrix_data/PAM130 +34 -0
- data/data/matrix_data/PAM140 +34 -0
- data/data/matrix_data/PAM150 +34 -0
- data/data/matrix_data/PAM160 +34 -0
- data/data/matrix_data/PAM170 +34 -0
- data/data/matrix_data/PAM180 +34 -0
- data/data/matrix_data/PAM190 +34 -0
- data/data/matrix_data/PAM20 +34 -0
- data/data/matrix_data/PAM200 +34 -0
- data/data/matrix_data/PAM210 +34 -0
- data/data/matrix_data/PAM220 +34 -0
- data/data/matrix_data/PAM230 +34 -0
- data/data/matrix_data/PAM240 +34 -0
- data/data/matrix_data/PAM250 +25 -0
- data/data/matrix_data/PAM260 +34 -0
- data/data/matrix_data/PAM270 +34 -0
- data/data/matrix_data/PAM280 +34 -0
- data/data/matrix_data/PAM290 +34 -0
- data/data/matrix_data/PAM30 +25 -0
- data/data/matrix_data/PAM300 +34 -0
- data/data/matrix_data/PAM310 +34 -0
- data/data/matrix_data/PAM320 +34 -0
- data/data/matrix_data/PAM330 +34 -0
- data/data/matrix_data/PAM340 +34 -0
- data/data/matrix_data/PAM350 +34 -0
- data/data/matrix_data/PAM360 +34 -0
- data/data/matrix_data/PAM370 +34 -0
- data/data/matrix_data/PAM380 +34 -0
- data/data/matrix_data/PAM390 +34 -0
- data/data/matrix_data/PAM40 +34 -0
- data/data/matrix_data/PAM400 +34 -0
- data/data/matrix_data/PAM410 +34 -0
- data/data/matrix_data/PAM420 +34 -0
- data/data/matrix_data/PAM430 +34 -0
- data/data/matrix_data/PAM440 +34 -0
- data/data/matrix_data/PAM450 +34 -0
- data/data/matrix_data/PAM460 +34 -0
- data/data/matrix_data/PAM470 +34 -0
- data/data/matrix_data/PAM480 +34 -0
- data/data/matrix_data/PAM490 +34 -0
- data/data/matrix_data/PAM50 +34 -0
- data/data/matrix_data/PAM500 +34 -0
- data/data/matrix_data/PAM60 +34 -0
- data/data/matrix_data/PAM70 +25 -0
- data/data/matrix_data/PAM80 +34 -0
- data/data/matrix_data/PAM90 +34 -0
- data/ext/stride_align/backend_avx2.cpp +2 -0
- data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
- data/ext/stride_align/backend_generic.cpp +3 -0
- data/ext/stride_align/backend_impl.hpp +983 -0
- data/ext/stride_align/backend_lasx.cpp +2 -0
- data/ext/stride_align/backend_lsx.cpp +2 -0
- data/ext/stride_align/backend_neon.cpp +2 -0
- data/ext/stride_align/backend_rvv.cpp +2 -0
- data/ext/stride_align/backend_sse41.cpp +2 -0
- data/ext/stride_align/backend_sve.cpp +2 -0
- data/ext/stride_align/backend_sve2.cpp +2 -0
- data/ext/stride_align/backend_vsx.cpp +2 -0
- data/ext/stride_align/beider_morse_impl.cpp +5 -0
- data/ext/stride_align/cpu_detect.cpp +172 -0
- data/ext/stride_align/cpu_detect.hpp +6 -0
- data/ext/stride_align/extconf.rb +114 -0
- data/ext/stride_align/target_profile.hpp +82 -0
- data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
- data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
- data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
- data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
- data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
- data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
- data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
- data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
- data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
- data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
- data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
- data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
- data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
- data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
- data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
- data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
- data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
- data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
- data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
- data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
- data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
- data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
- data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
- data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
- data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
- data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
- data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
- data/lib/stride_align/algorithms.rb +296 -0
- data/lib/stride_align/alignment_path.rb +217 -0
- data/lib/stride_align/backend.rb +47 -0
- data/lib/stride_align/batch.rb +705 -0
- data/lib/stride_align/core.rb +180 -0
- data/lib/stride_align/keyboard.rb +200 -0
- data/lib/stride_align/matrices.rb +403 -0
- data/lib/stride_align/version.rb +5 -0
- data/lib/stride_align.rb +87 -0
- metadata +231 -0
|
@@ -0,0 +1,1247 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Bit-parallel Levenshtein distance (Myers 1999 + Hyyrö 2003).
|
|
4
|
+
//
|
|
5
|
+
// * myers_single_word_u8: pattern of length <= 64 over a uint8 alphabet.
|
|
6
|
+
// One 64-bit add computes 64 cells of the DP row in parallel. This is
|
|
7
|
+
// the hottest path for short bytes/ASCII queries.
|
|
8
|
+
// * myers_multi_word_u8: pattern of arbitrary length over uint8. The
|
|
9
|
+
// pattern is split into ceil(m / 64) blocks; carries propagate between
|
|
10
|
+
// blocks both through the inner-loop addition and through the
|
|
11
|
+
// horizontal-shift step.
|
|
12
|
+
// * myers_distance<Token>: hashmap-PEQ variant for any token type (used
|
|
13
|
+
// for Python str inputs whose code-point alphabet exceeds 256). Same
|
|
14
|
+
// bit-parallel inner loop, just a slower PEQ lookup.
|
|
15
|
+
//
|
|
16
|
+
// SIMD specialization (one target per SIMD lane, lane width 64) lives in
|
|
17
|
+
// each x86 backend header.
|
|
18
|
+
|
|
19
|
+
#include <algorithm>
|
|
20
|
+
#include <array>
|
|
21
|
+
#include <cstddef>
|
|
22
|
+
#include <cstdint>
|
|
23
|
+
#include <cstring>
|
|
24
|
+
#include <limits>
|
|
25
|
+
#include <span>
|
|
26
|
+
#include <type_traits>
|
|
27
|
+
#include <unordered_map>
|
|
28
|
+
#include <vector>
|
|
29
|
+
|
|
30
|
+
#include "stride_align/alignment.hpp"
|
|
31
|
+
|
|
32
|
+
namespace stride_align::levenshtein {
|
|
33
|
+
|
|
34
|
+
// Sentinel meaning "no cutoff applied". The Myers entry points and the
|
|
35
|
+
// SIMD batch path accept a cutoff; when it equals kNoCutoff every
|
|
36
|
+
// column runs to completion and the exact distance is returned. When a
|
|
37
|
+
// finite cutoff is supplied and the algorithm proves the final score
|
|
38
|
+
// must exceed it, the call returns `cutoff + 1` (matching rapidfuzz's
|
|
39
|
+
// convention) without finishing the remaining columns.
|
|
40
|
+
inline constexpr std::size_t kNoCutoff = std::numeric_limits<std::size_t>::max();
|
|
41
|
+
|
|
42
|
+
namespace detail {
|
|
43
|
+
|
|
44
|
+
// Run the Myers/Hyyrö bit-parallel inner loop over a `text`, with PEQ lookup
|
|
45
|
+
// supplied by `peq_for(c)` returning a span of B `std::uint64_t` per text
|
|
46
|
+
// character. `B` is the number of 64-bit blocks the pattern is split into.
|
|
47
|
+
// If `cutoff` is not kNoCutoff and we can prove final >= cutoff + 1, the
|
|
48
|
+
// loop returns cutoff + 1 early.
|
|
49
|
+
template <typename Text, typename PeqFn>
|
|
50
|
+
inline std::size_t myers_inner(
|
|
51
|
+
std::size_t m,
|
|
52
|
+
std::size_t B,
|
|
53
|
+
Text text,
|
|
54
|
+
PeqFn peq_for,
|
|
55
|
+
std::uint64_t* vp,
|
|
56
|
+
std::uint64_t* vn,
|
|
57
|
+
std::uint64_t top_bit_last,
|
|
58
|
+
std::size_t initial_score,
|
|
59
|
+
std::size_t cutoff = kNoCutoff) noexcept(noexcept(peq_for(typename Text::value_type{}))) {
|
|
60
|
+
std::size_t score = initial_score;
|
|
61
|
+
// Track remaining columns so we can prove min-final-score = score -
|
|
62
|
+
// remaining and bail when it already exceeds cutoff.
|
|
63
|
+
std::size_t k = 0;
|
|
64
|
+
const std::size_t n = text.size();
|
|
65
|
+
for (const auto c : text) {
|
|
66
|
+
auto eq_blocks = peq_for(c);
|
|
67
|
+
std::uint64_t add_carry = 0;
|
|
68
|
+
std::uint64_t hp_carry_in = 1U;
|
|
69
|
+
std::uint64_t hn_carry_in = 0;
|
|
70
|
+
std::uint64_t last_hp = 0;
|
|
71
|
+
std::uint64_t last_hn = 0;
|
|
72
|
+
|
|
73
|
+
for (std::size_t b = 0; b < B; ++b) {
|
|
74
|
+
const std::uint64_t eq = eq_blocks[b];
|
|
75
|
+
const std::uint64_t x = eq | vn[b];
|
|
76
|
+
const std::uint64_t xv = x & vp[b];
|
|
77
|
+
|
|
78
|
+
const __uint128_t wide =
|
|
79
|
+
static_cast<__uint128_t>(xv) +
|
|
80
|
+
static_cast<__uint128_t>(vp[b]) +
|
|
81
|
+
static_cast<__uint128_t>(add_carry);
|
|
82
|
+
const std::uint64_t sum = static_cast<std::uint64_t>(wide);
|
|
83
|
+
add_carry = static_cast<std::uint64_t>(wide >> 64);
|
|
84
|
+
|
|
85
|
+
const std::uint64_t d0 = (sum ^ vp[b]) | x;
|
|
86
|
+
const std::uint64_t hp = vn[b] | ~(d0 | vp[b]);
|
|
87
|
+
const std::uint64_t hn = d0 & vp[b];
|
|
88
|
+
|
|
89
|
+
const std::uint64_t hp_shift = (hp << 1) | hp_carry_in;
|
|
90
|
+
const std::uint64_t hn_shift = (hn << 1) | hn_carry_in;
|
|
91
|
+
hp_carry_in = hp >> 63;
|
|
92
|
+
hn_carry_in = hn >> 63;
|
|
93
|
+
|
|
94
|
+
vp[b] = hn_shift | ~(d0 | hp_shift);
|
|
95
|
+
vn[b] = d0 & hp_shift;
|
|
96
|
+
|
|
97
|
+
if (b == B - 1U) {
|
|
98
|
+
last_hp = hp;
|
|
99
|
+
last_hn = hn;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// HP and HN top bits are mutually exclusive under Myers/Hyyrö.
|
|
104
|
+
score += static_cast<std::size_t>((last_hp & top_bit_last) != 0U);
|
|
105
|
+
score -= static_cast<std::size_t>((last_hn & top_bit_last) != 0U);
|
|
106
|
+
(void)m;
|
|
107
|
+
++k;
|
|
108
|
+
// Min possible final score is score - (n - k). Bail if even the
|
|
109
|
+
// best-case can't reach cutoff.
|
|
110
|
+
if (cutoff != kNoCutoff && score > cutoff + (n - k)) {
|
|
111
|
+
return cutoff + 1U;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return score;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
inline void init_vp_vn(
|
|
118
|
+
std::vector<std::uint64_t>& vp,
|
|
119
|
+
std::vector<std::uint64_t>& vn,
|
|
120
|
+
std::size_t m) {
|
|
121
|
+
constexpr std::size_t kWord = 64U;
|
|
122
|
+
const std::size_t B = (m + kWord - 1U) / kWord;
|
|
123
|
+
vp.assign(B, ~std::uint64_t{0});
|
|
124
|
+
vn.assign(B, 0);
|
|
125
|
+
const std::size_t last_bits = m - (B - 1U) * kWord;
|
|
126
|
+
if (last_bits < kWord) {
|
|
127
|
+
vp.back() = (std::uint64_t{1} << last_bits) - 1U;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
} // namespace detail
|
|
132
|
+
|
|
133
|
+
inline std::size_t myers_single_word_u8(
|
|
134
|
+
std::span<const std::uint8_t> pattern,
|
|
135
|
+
std::span<const std::uint8_t> text,
|
|
136
|
+
std::size_t cutoff = kNoCutoff) noexcept {
|
|
137
|
+
const std::size_t m = pattern.size();
|
|
138
|
+
const std::size_t n = text.size();
|
|
139
|
+
if (m == 0) {
|
|
140
|
+
return n;
|
|
141
|
+
}
|
|
142
|
+
if (n == 0) {
|
|
143
|
+
return m;
|
|
144
|
+
}
|
|
145
|
+
// Triangle lower bound: distance >= |m - n|. Bail before the 2 KB
|
|
146
|
+
// PEQ build when a finite cutoff already rules the pair out.
|
|
147
|
+
if (cutoff != kNoCutoff) {
|
|
148
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
149
|
+
if (len_diff > cutoff) {
|
|
150
|
+
return cutoff + 1U;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
std::uint64_t peq[256] = {0};
|
|
155
|
+
const std::uint64_t one = 1;
|
|
156
|
+
for (std::size_t i = 0; i < m; ++i) {
|
|
157
|
+
peq[pattern[i]] |= one << i;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const std::size_t shift = m - 1U;
|
|
161
|
+
std::uint64_t vp = (m == 64U)
|
|
162
|
+
? ~std::uint64_t{0}
|
|
163
|
+
: ((one << m) - 1);
|
|
164
|
+
std::uint64_t vn = 0;
|
|
165
|
+
std::size_t score = m;
|
|
166
|
+
|
|
167
|
+
// Dual loops: the common no-cutoff path skips the early-exit branch
|
|
168
|
+
// and uses a branchless score delta (HP/HN top bits are exclusive).
|
|
169
|
+
if (cutoff == kNoCutoff) {
|
|
170
|
+
for (const std::uint8_t c : text) {
|
|
171
|
+
const std::uint64_t eq = peq[c];
|
|
172
|
+
const std::uint64_t x = eq | vn;
|
|
173
|
+
const std::uint64_t d0 = (((x & vp) + vp) ^ vp) | x;
|
|
174
|
+
const std::uint64_t hp = vn | ~(d0 | vp);
|
|
175
|
+
const std::uint64_t hn = d0 & vp;
|
|
176
|
+
score += static_cast<std::size_t>((hp >> shift) & 1U);
|
|
177
|
+
score -= static_cast<std::size_t>((hn >> shift) & 1U);
|
|
178
|
+
const std::uint64_t hp_shift = (hp << 1) | one;
|
|
179
|
+
const std::uint64_t hn_shift = (hn << 1);
|
|
180
|
+
vp = hn_shift | ~(d0 | hp_shift);
|
|
181
|
+
vn = d0 & hp_shift;
|
|
182
|
+
}
|
|
183
|
+
return score;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
std::size_t k = 0;
|
|
187
|
+
for (const std::uint8_t c : text) {
|
|
188
|
+
const std::uint64_t eq = peq[c];
|
|
189
|
+
const std::uint64_t x = eq | vn;
|
|
190
|
+
const std::uint64_t d0 = (((x & vp) + vp) ^ vp) | x;
|
|
191
|
+
const std::uint64_t hp = vn | ~(d0 | vp);
|
|
192
|
+
const std::uint64_t hn = d0 & vp;
|
|
193
|
+
score += static_cast<std::size_t>((hp >> shift) & 1U);
|
|
194
|
+
score -= static_cast<std::size_t>((hn >> shift) & 1U);
|
|
195
|
+
const std::uint64_t hp_shift = (hp << 1) | one;
|
|
196
|
+
const std::uint64_t hn_shift = (hn << 1);
|
|
197
|
+
vp = hn_shift | ~(d0 | hp_shift);
|
|
198
|
+
vn = d0 & hp_shift;
|
|
199
|
+
++k;
|
|
200
|
+
if (score > cutoff + (n - k)) {
|
|
201
|
+
return cutoff + 1U;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return score;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Thread-local PEQ scratch for the multi-word Myers kernel. Same
|
|
208
|
+
// idea as ``indel.hpp::MultiWordU8Scratch`` — amortise the 256*K
|
|
209
|
+
// PEQ buffer allocation across calls. Dirty-symbol clearing avoids
|
|
210
|
+
// a full 256×K zero on every call when the alphabet is small.
|
|
211
|
+
struct MultiWordLevScratch {
|
|
212
|
+
std::vector<std::uint64_t> peq;
|
|
213
|
+
std::vector<std::uint64_t> vp;
|
|
214
|
+
std::vector<std::uint64_t> vn;
|
|
215
|
+
std::size_t k = 0;
|
|
216
|
+
|
|
217
|
+
std::vector<std::uint8_t> peq_dirty;
|
|
218
|
+
std::array<std::uint32_t, 256> peq_touch{};
|
|
219
|
+
std::uint32_t peq_gen = 0;
|
|
220
|
+
std::size_t peq_layout_k = 0;
|
|
221
|
+
|
|
222
|
+
void resize_for(std::size_t K) {
|
|
223
|
+
if (K > k) {
|
|
224
|
+
peq.assign(256U * K, 0U);
|
|
225
|
+
vp.resize(K);
|
|
226
|
+
vn.resize(K);
|
|
227
|
+
peq_dirty.clear();
|
|
228
|
+
peq_layout_k = K;
|
|
229
|
+
peq_touch.fill(0);
|
|
230
|
+
peq_gen = 0;
|
|
231
|
+
k = K;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
void peq_begin(std::size_t K) {
|
|
236
|
+
resize_for(K);
|
|
237
|
+
if (peq_layout_k != K) {
|
|
238
|
+
std::fill_n(peq.data(), 256U * K, std::uint64_t{0});
|
|
239
|
+
peq_dirty.clear();
|
|
240
|
+
peq_layout_k = K;
|
|
241
|
+
} else {
|
|
242
|
+
for (const std::uint8_t s : peq_dirty) {
|
|
243
|
+
std::fill_n(
|
|
244
|
+
peq.data() + static_cast<std::size_t>(s) * K, K, std::uint64_t{0});
|
|
245
|
+
}
|
|
246
|
+
peq_dirty.clear();
|
|
247
|
+
}
|
|
248
|
+
if (++peq_gen == 0U) {
|
|
249
|
+
peq_touch.fill(0);
|
|
250
|
+
peq_gen = 1U;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
void peq_or_bit(std::uint8_t c, std::size_t bit_index, std::size_t K) {
|
|
255
|
+
if (peq_touch[c] != peq_gen) {
|
|
256
|
+
peq_touch[c] = peq_gen;
|
|
257
|
+
peq_dirty.push_back(c);
|
|
258
|
+
}
|
|
259
|
+
peq[static_cast<std::size_t>(c) * K + (bit_index >> 6U)] |=
|
|
260
|
+
std::uint64_t{1} << (bit_index & 63U);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
void peq_build(std::span<const std::uint8_t> pattern, std::size_t K) {
|
|
264
|
+
peq_begin(K);
|
|
265
|
+
for (std::size_t i = 0; i < pattern.size(); ++i) {
|
|
266
|
+
peq_or_bit(pattern[i], i, K);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
inline MultiWordLevScratch& multi_word_lev_scratch() {
|
|
272
|
+
thread_local MultiWordLevScratch s;
|
|
273
|
+
return s;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Forward declarations: the templated dispatcher below routes
|
|
277
|
+
// patterns in 65..256 chars to K=2/3/4 hand-specialised kernels.
|
|
278
|
+
inline std::size_t myers_multi_word_k2_u8(
|
|
279
|
+
std::span<const std::uint8_t> pattern,
|
|
280
|
+
std::span<const std::uint8_t> text,
|
|
281
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
282
|
+
inline std::size_t myers_multi_word_k3_u8(
|
|
283
|
+
std::span<const std::uint8_t> pattern,
|
|
284
|
+
std::span<const std::uint8_t> text,
|
|
285
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
286
|
+
inline std::size_t myers_multi_word_k4_u8(
|
|
287
|
+
std::span<const std::uint8_t> pattern,
|
|
288
|
+
std::span<const std::uint8_t> text,
|
|
289
|
+
std::size_t cutoff = kNoCutoff) noexcept;
|
|
290
|
+
inline std::size_t myers_multi_word_generic_u8(
|
|
291
|
+
std::span<const std::uint8_t> pattern,
|
|
292
|
+
std::span<const std::uint8_t> text,
|
|
293
|
+
std::size_t cutoff);
|
|
294
|
+
|
|
295
|
+
// Public multi-word entry: dispatches by pattern length to the
|
|
296
|
+
// hand-specialised K=2/3/4 paths (m=65..256), or the generic
|
|
297
|
+
// heap-state variant for longer patterns.
|
|
298
|
+
inline std::size_t myers_multi_word_u8(
|
|
299
|
+
std::span<const std::uint8_t> pattern,
|
|
300
|
+
std::span<const std::uint8_t> text,
|
|
301
|
+
std::size_t cutoff = kNoCutoff) {
|
|
302
|
+
const std::size_t m = pattern.size();
|
|
303
|
+
const std::size_t n = text.size();
|
|
304
|
+
if (m == 0) return n;
|
|
305
|
+
if (n == 0) return m;
|
|
306
|
+
if (cutoff != kNoCutoff) {
|
|
307
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
308
|
+
if (len_diff > cutoff) return cutoff + 1U;
|
|
309
|
+
}
|
|
310
|
+
if (m <= 64U) {
|
|
311
|
+
return myers_single_word_u8(pattern, text, cutoff);
|
|
312
|
+
}
|
|
313
|
+
if (m <= 128U) return myers_multi_word_k2_u8(pattern, text, cutoff);
|
|
314
|
+
if (m <= 192U) return myers_multi_word_k3_u8(pattern, text, cutoff);
|
|
315
|
+
if (m <= 256U) return myers_multi_word_k4_u8(pattern, text, cutoff);
|
|
316
|
+
return myers_multi_word_generic_u8(pattern, text, cutoff);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Templated K = 2..4 Myers kernel: constexpr K means stack-resident
|
|
320
|
+
// ``vp[K]`` / ``vn[K]`` state arrays and a fully-unrolled per-block
|
|
321
|
+
// loop. The compiler pins vp/vn in registers across the text scan.
|
|
322
|
+
//
|
|
323
|
+
// Carry across blocks (the ``xv + vp[b] + add_carry`` 128-bit add in
|
|
324
|
+
// the original myers_inner) is decomposed into two
|
|
325
|
+
// ``__builtin_add_overflow`` calls so the carry chain rides the
|
|
326
|
+
// native carry-flag instruction on every supported arch (ADCX/ADOX
|
|
327
|
+
// on x86, ADCS on ARM).
|
|
328
|
+
//
|
|
329
|
+
// Above K = 4 the register file (16 GPRs on x86-64) overflows because
|
|
330
|
+
// every block needs vp + vn (2 regs) plus per-step temporaries — the
|
|
331
|
+
// dispatch above falls back to the heap-state generic kernel there.
|
|
332
|
+
template <std::size_t K>
|
|
333
|
+
inline std::size_t myers_multi_word_kN_u8(
|
|
334
|
+
std::span<const std::uint8_t> pattern,
|
|
335
|
+
std::span<const std::uint8_t> text,
|
|
336
|
+
std::size_t cutoff) noexcept {
|
|
337
|
+
static_assert(K >= 2 && K <= 4, "myers_multi_word_kN_u8 covers K=2..4");
|
|
338
|
+
const std::size_t m = pattern.size();
|
|
339
|
+
const std::size_t n = text.size();
|
|
340
|
+
if (m == 0U) return n;
|
|
341
|
+
if (n == 0U) return m;
|
|
342
|
+
const std::uint64_t one = 1U;
|
|
343
|
+
|
|
344
|
+
MultiWordLevScratch& scr = multi_word_lev_scratch();
|
|
345
|
+
scr.peq_build(pattern, K);
|
|
346
|
+
|
|
347
|
+
// Stack-resident state. ``K`` is constexpr so the compiler treats
|
|
348
|
+
// these as register-promotable fixed-size arrays.
|
|
349
|
+
std::uint64_t vp[K];
|
|
350
|
+
std::uint64_t vn[K];
|
|
351
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
352
|
+
vp[k] = ~std::uint64_t{0};
|
|
353
|
+
vn[k] = 0;
|
|
354
|
+
}
|
|
355
|
+
const std::size_t last_bits = m - (K - 1U) * 64U; // bits in last block
|
|
356
|
+
if (last_bits < 64U) {
|
|
357
|
+
vp[K - 1U] = (one << last_bits) - 1U;
|
|
358
|
+
}
|
|
359
|
+
const std::uint64_t top_bit_last = one << (last_bits - 1U);
|
|
360
|
+
|
|
361
|
+
std::size_t score = m;
|
|
362
|
+
std::size_t k_col = 0;
|
|
363
|
+
const std::uint64_t* const peq_base = scr.peq.data();
|
|
364
|
+
|
|
365
|
+
for (const std::uint8_t c : text) {
|
|
366
|
+
const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
|
|
367
|
+
|
|
368
|
+
std::uint64_t add_carry = 0;
|
|
369
|
+
std::uint64_t hp_carry_in = 1U;
|
|
370
|
+
std::uint64_t hn_carry_in = 0;
|
|
371
|
+
std::uint64_t last_hp = 0;
|
|
372
|
+
std::uint64_t last_hn = 0;
|
|
373
|
+
|
|
374
|
+
// Per-block loop. Unrolled at compile time because K is constexpr.
|
|
375
|
+
for (std::size_t b = 0; b < K; ++b) {
|
|
376
|
+
const std::uint64_t eq = peq_row[b];
|
|
377
|
+
const std::uint64_t x = eq | vn[b];
|
|
378
|
+
const std::uint64_t xv = x & vp[b];
|
|
379
|
+
|
|
380
|
+
// 128-bit-equivalent add: xv + vp[b] + add_carry. Carries
|
|
381
|
+
// propagate via __builtin_add_overflow; the sum of the two
|
|
382
|
+
// overflow flags is the next block's add_carry.
|
|
383
|
+
std::uint64_t s1;
|
|
384
|
+
const bool c1 = __builtin_add_overflow(xv, vp[b], &s1);
|
|
385
|
+
std::uint64_t sum;
|
|
386
|
+
const bool c2 = __builtin_add_overflow(s1, add_carry, &sum);
|
|
387
|
+
add_carry = static_cast<std::uint64_t>(c1) + static_cast<std::uint64_t>(c2);
|
|
388
|
+
|
|
389
|
+
const std::uint64_t d0 = (sum ^ vp[b]) | x;
|
|
390
|
+
const std::uint64_t hp = vn[b] | ~(d0 | vp[b]);
|
|
391
|
+
const std::uint64_t hn = d0 & vp[b];
|
|
392
|
+
|
|
393
|
+
const std::uint64_t hp_shift = (hp << 1) | hp_carry_in;
|
|
394
|
+
const std::uint64_t hn_shift = (hn << 1) | hn_carry_in;
|
|
395
|
+
hp_carry_in = hp >> 63;
|
|
396
|
+
hn_carry_in = hn >> 63;
|
|
397
|
+
|
|
398
|
+
vp[b] = hn_shift | ~(d0 | hp_shift);
|
|
399
|
+
vn[b] = d0 & hp_shift;
|
|
400
|
+
|
|
401
|
+
if (b == K - 1U) {
|
|
402
|
+
last_hp = hp;
|
|
403
|
+
last_hn = hn;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
score += static_cast<std::size_t>((last_hp & top_bit_last) != 0U);
|
|
408
|
+
score -= static_cast<std::size_t>((last_hn & top_bit_last) != 0U);
|
|
409
|
+
++k_col;
|
|
410
|
+
if (cutoff != kNoCutoff && score > cutoff + (n - k_col)) {
|
|
411
|
+
return cutoff + 1U;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return score;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
inline std::size_t myers_multi_word_k2_u8(
|
|
418
|
+
std::span<const std::uint8_t> pattern,
|
|
419
|
+
std::span<const std::uint8_t> text,
|
|
420
|
+
std::size_t cutoff) noexcept {
|
|
421
|
+
return myers_multi_word_kN_u8<2>(pattern, text, cutoff);
|
|
422
|
+
}
|
|
423
|
+
inline std::size_t myers_multi_word_k3_u8(
|
|
424
|
+
std::span<const std::uint8_t> pattern,
|
|
425
|
+
std::span<const std::uint8_t> text,
|
|
426
|
+
std::size_t cutoff) noexcept {
|
|
427
|
+
return myers_multi_word_kN_u8<3>(pattern, text, cutoff);
|
|
428
|
+
}
|
|
429
|
+
inline std::size_t myers_multi_word_k4_u8(
|
|
430
|
+
std::span<const std::uint8_t> pattern,
|
|
431
|
+
std::span<const std::uint8_t> text,
|
|
432
|
+
std::size_t cutoff) noexcept {
|
|
433
|
+
return myers_multi_word_kN_u8<4>(pattern, text, cutoff);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Generic-K fallback for patterns of m > 256 chars. Heap-resident
|
|
437
|
+
// state via the thread-local scratch's vp/vn vectors. Same fused
|
|
438
|
+
// per-block recurrence as the K=2..4 paths; the inner loop runs the
|
|
439
|
+
// runtime-K loop body instead of a compile-time-unrolled one.
|
|
440
|
+
inline std::size_t myers_multi_word_generic_u8(
|
|
441
|
+
std::span<const std::uint8_t> pattern,
|
|
442
|
+
std::span<const std::uint8_t> text,
|
|
443
|
+
std::size_t cutoff) {
|
|
444
|
+
const std::size_t m = pattern.size();
|
|
445
|
+
const std::size_t n = text.size();
|
|
446
|
+
if (m == 0U) return n;
|
|
447
|
+
if (n == 0U) return m;
|
|
448
|
+
constexpr std::size_t kWord = 64U;
|
|
449
|
+
const std::size_t K = (m + kWord - 1U) / kWord;
|
|
450
|
+
const std::uint64_t one = 1U;
|
|
451
|
+
|
|
452
|
+
MultiWordLevScratch& scr = multi_word_lev_scratch();
|
|
453
|
+
scr.peq_build(pattern, K);
|
|
454
|
+
|
|
455
|
+
std::uint64_t* const vp = scr.vp.data();
|
|
456
|
+
std::uint64_t* const vn = scr.vn.data();
|
|
457
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
458
|
+
vp[k] = ~std::uint64_t{0};
|
|
459
|
+
vn[k] = 0;
|
|
460
|
+
}
|
|
461
|
+
const std::size_t last_bits = m - (K - 1U) * kWord;
|
|
462
|
+
if (last_bits < kWord) {
|
|
463
|
+
vp[K - 1U] = (one << last_bits) - 1U;
|
|
464
|
+
}
|
|
465
|
+
const std::uint64_t top_bit_last = one << (last_bits - 1U);
|
|
466
|
+
|
|
467
|
+
std::size_t score = m;
|
|
468
|
+
std::size_t k_col = 0;
|
|
469
|
+
const std::uint64_t* const peq_base = scr.peq.data();
|
|
470
|
+
|
|
471
|
+
for (const std::uint8_t c : text) {
|
|
472
|
+
const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
|
|
473
|
+
|
|
474
|
+
std::uint64_t add_carry = 0;
|
|
475
|
+
std::uint64_t hp_carry_in = 1U;
|
|
476
|
+
std::uint64_t hn_carry_in = 0;
|
|
477
|
+
std::uint64_t last_hp = 0;
|
|
478
|
+
std::uint64_t last_hn = 0;
|
|
479
|
+
|
|
480
|
+
for (std::size_t b = 0; b < K; ++b) {
|
|
481
|
+
const std::uint64_t eq = peq_row[b];
|
|
482
|
+
const std::uint64_t x = eq | vn[b];
|
|
483
|
+
const std::uint64_t xv = x & vp[b];
|
|
484
|
+
|
|
485
|
+
std::uint64_t s1;
|
|
486
|
+
const bool c1 = __builtin_add_overflow(xv, vp[b], &s1);
|
|
487
|
+
std::uint64_t sum;
|
|
488
|
+
const bool c2 = __builtin_add_overflow(s1, add_carry, &sum);
|
|
489
|
+
add_carry = static_cast<std::uint64_t>(c1) + static_cast<std::uint64_t>(c2);
|
|
490
|
+
|
|
491
|
+
const std::uint64_t d0 = (sum ^ vp[b]) | x;
|
|
492
|
+
const std::uint64_t hp = vn[b] | ~(d0 | vp[b]);
|
|
493
|
+
const std::uint64_t hn = d0 & vp[b];
|
|
494
|
+
|
|
495
|
+
const std::uint64_t hp_shift = (hp << 1) | hp_carry_in;
|
|
496
|
+
const std::uint64_t hn_shift = (hn << 1) | hn_carry_in;
|
|
497
|
+
hp_carry_in = hp >> 63;
|
|
498
|
+
hn_carry_in = hn >> 63;
|
|
499
|
+
|
|
500
|
+
vp[b] = hn_shift | ~(d0 | hp_shift);
|
|
501
|
+
vn[b] = d0 & hp_shift;
|
|
502
|
+
|
|
503
|
+
if (b == K - 1U) {
|
|
504
|
+
last_hp = hp;
|
|
505
|
+
last_hn = hn;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
score += static_cast<std::size_t>((last_hp & top_bit_last) != 0U);
|
|
510
|
+
score -= static_cast<std::size_t>((last_hn & top_bit_last) != 0U);
|
|
511
|
+
++k_col;
|
|
512
|
+
if (cutoff != kNoCutoff && score > cutoff + (n - k_col)) {
|
|
513
|
+
return cutoff + 1U;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return score;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
template <typename Token>
|
|
520
|
+
std::size_t myers_distance(
|
|
521
|
+
std::span<const Token> pattern,
|
|
522
|
+
std::span<const Token> text,
|
|
523
|
+
std::size_t cutoff = kNoCutoff) {
|
|
524
|
+
static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
|
|
525
|
+
const std::size_t m = pattern.size();
|
|
526
|
+
const std::size_t n = text.size();
|
|
527
|
+
if (m == 0) {
|
|
528
|
+
return n;
|
|
529
|
+
}
|
|
530
|
+
if (n == 0) {
|
|
531
|
+
return m;
|
|
532
|
+
}
|
|
533
|
+
if (cutoff != kNoCutoff) {
|
|
534
|
+
const std::size_t len_diff = m > n ? m - n : n - m;
|
|
535
|
+
if (len_diff > cutoff) {
|
|
536
|
+
return cutoff + 1U;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
constexpr std::size_t kWord = 64U;
|
|
541
|
+
const std::size_t B = (m + kWord - 1U) / kWord;
|
|
542
|
+
|
|
543
|
+
std::unordered_map<Token, std::vector<std::uint64_t>> peq;
|
|
544
|
+
const std::uint64_t one = 1;
|
|
545
|
+
for (std::size_t i = 0; i < m; ++i) {
|
|
546
|
+
auto [it, inserted] = peq.try_emplace(pattern[i], std::vector<std::uint64_t>(B, 0));
|
|
547
|
+
it->second[i / kWord] |= one << (i % kWord);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Zero-row for tokens that never appear in the pattern. Sized once per
|
|
551
|
+
// call so all per-call lambdas see the right B.
|
|
552
|
+
const std::vector<std::uint64_t> zero_blocks(B, 0);
|
|
553
|
+
|
|
554
|
+
std::vector<std::uint64_t> vp;
|
|
555
|
+
std::vector<std::uint64_t> vn;
|
|
556
|
+
detail::init_vp_vn(vp, vn, m);
|
|
557
|
+
|
|
558
|
+
const std::size_t last_bits = m - (B - 1U) * kWord;
|
|
559
|
+
const std::uint64_t top_bit_last = std::uint64_t{1} << (last_bits - 1U);
|
|
560
|
+
|
|
561
|
+
return detail::myers_inner(
|
|
562
|
+
m,
|
|
563
|
+
B,
|
|
564
|
+
text,
|
|
565
|
+
[&](Token c) {
|
|
566
|
+
auto it = peq.find(c);
|
|
567
|
+
if (it == peq.end()) {
|
|
568
|
+
return std::span<const std::uint64_t>(zero_blocks.data(), B);
|
|
569
|
+
}
|
|
570
|
+
return std::span<const std::uint64_t>(it->second.data(), B);
|
|
571
|
+
},
|
|
572
|
+
vp.data(),
|
|
573
|
+
vn.data(),
|
|
574
|
+
top_bit_last,
|
|
575
|
+
m,
|
|
576
|
+
cutoff);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// =================================================================
|
|
580
|
+
// Optimal String Alignment (OSA) distance — a.k.a. "restricted
|
|
581
|
+
// Damerau-Levenshtein". Same as Levenshtein but adjacent transpositions
|
|
582
|
+
// cost 1 instead of 2 substitutions. "Restricted" means each character
|
|
583
|
+
// can participate in at most one edit operation, so a transposition
|
|
584
|
+
// can't be combined with another edit on the same characters.
|
|
585
|
+
//
|
|
586
|
+
// Most Python users who ask for "Damerau-Levenshtein" actually want
|
|
587
|
+
// OSA — it's what rapidfuzz exposes as `OSA.distance` and is much
|
|
588
|
+
// faster to compute than true Damerau-Levenshtein (which needs an
|
|
589
|
+
// alphabet-sized auxiliary array per cell).
|
|
590
|
+
// =================================================================
|
|
591
|
+
|
|
592
|
+
namespace detail {
|
|
593
|
+
|
|
594
|
+
// Scalar DP reference implementation. O(m*n) time, O(m) space (rolling
|
|
595
|
+
// rows). Correctness oracle for the bit-parallel variants below; not on
|
|
596
|
+
// any hot path.
|
|
597
|
+
template <typename Token>
|
|
598
|
+
inline std::size_t osa_dp(
|
|
599
|
+
std::span<const Token> pattern,
|
|
600
|
+
std::span<const Token> text) {
|
|
601
|
+
const std::size_t m = pattern.size();
|
|
602
|
+
const std::size_t n = text.size();
|
|
603
|
+
if (m == 0) {
|
|
604
|
+
return n;
|
|
605
|
+
}
|
|
606
|
+
if (n == 0) {
|
|
607
|
+
return m;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Three rolling rows: prev2[i] = d[i][j-2], prev1[i] = d[i][j-1],
|
|
611
|
+
// curr[i] = d[i][j]. The transposition step reads d[i-2][j-2] which
|
|
612
|
+
// is prev2[i-2].
|
|
613
|
+
std::vector<std::size_t> prev2(m + 1U);
|
|
614
|
+
std::vector<std::size_t> prev1(m + 1U);
|
|
615
|
+
std::vector<std::size_t> curr(m + 1U);
|
|
616
|
+
for (std::size_t i = 0; i <= m; ++i) {
|
|
617
|
+
prev1[i] = i;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
for (std::size_t j = 1; j <= n; ++j) {
|
|
621
|
+
curr[0] = j;
|
|
622
|
+
for (std::size_t i = 1; i <= m; ++i) {
|
|
623
|
+
const std::size_t sub_cost =
|
|
624
|
+
(pattern[i - 1U] == text[j - 1U]) ? 0U : 1U;
|
|
625
|
+
std::size_t best = curr[i - 1U] + 1U; // insertion
|
|
626
|
+
best = std::min(best, prev1[i] + 1U); // deletion
|
|
627
|
+
best = std::min(best, prev1[i - 1U] + sub_cost); // substitution
|
|
628
|
+
if (i >= 2U && j >= 2U &&
|
|
629
|
+
pattern[i - 1U] == text[j - 2U] &&
|
|
630
|
+
pattern[i - 2U] == text[j - 1U]) {
|
|
631
|
+
best = std::min(best, prev2[i - 2U] + 1U); // transposition
|
|
632
|
+
}
|
|
633
|
+
curr[i] = best;
|
|
634
|
+
}
|
|
635
|
+
std::swap(prev2, prev1);
|
|
636
|
+
std::swap(prev1, curr);
|
|
637
|
+
}
|
|
638
|
+
return prev1[m];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
} // namespace detail
|
|
642
|
+
|
|
643
|
+
// Bit-parallel OSA for uint8 patterns of length <= 64 (Hyyrö 2002).
|
|
644
|
+
// Augments Myers' Levenshtein recurrence with a transposition mask:
|
|
645
|
+
// TR = (((~D0_prev) & PM) << 1) & PM_old
|
|
646
|
+
// Bit i of TR is set iff the (i-1, j-1) cell was *not* a "diagonal
|
|
647
|
+
// match" in the previous column AND P[i-1] == T[j] AND P[i] == T[j-1]
|
|
648
|
+
// — i.e. exactly the OSA transposition condition with the right
|
|
649
|
+
// predecessor state. The ~D0_prev gate is the non-obvious bit that
|
|
650
|
+
// keeps the algorithm correct under OSA's "each character touched at
|
|
651
|
+
// most once" restriction; without it, the kernel double-counts edits.
|
|
652
|
+
inline std::size_t osa_single_word_u8(
|
|
653
|
+
std::span<const std::uint8_t> pattern,
|
|
654
|
+
std::span<const std::uint8_t> text) noexcept {
|
|
655
|
+
const std::size_t m = pattern.size();
|
|
656
|
+
if (m == 0) {
|
|
657
|
+
return text.size();
|
|
658
|
+
}
|
|
659
|
+
if (text.empty()) {
|
|
660
|
+
return m;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
std::uint64_t peq[256] = {0};
|
|
664
|
+
const std::uint64_t one = 1U;
|
|
665
|
+
for (std::size_t i = 0; i < m; ++i) {
|
|
666
|
+
peq[pattern[i]] |= one << i;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const std::size_t shift = m - 1U;
|
|
670
|
+
std::uint64_t vp = (m == 64U)
|
|
671
|
+
? ~std::uint64_t{0}
|
|
672
|
+
: ((one << m) - 1U);
|
|
673
|
+
std::uint64_t vn = 0;
|
|
674
|
+
std::uint64_t d0_prev = 0;
|
|
675
|
+
std::uint64_t pm_old = 0;
|
|
676
|
+
std::size_t score = m;
|
|
677
|
+
|
|
678
|
+
for (const std::uint8_t c : text) {
|
|
679
|
+
const std::uint64_t pm = peq[c];
|
|
680
|
+
const std::uint64_t trans = (((~d0_prev) & pm) << 1) & pm_old;
|
|
681
|
+
std::uint64_t d0 = (((pm & vp) + vp) ^ vp) | pm | vn;
|
|
682
|
+
d0 |= trans;
|
|
683
|
+
|
|
684
|
+
const std::uint64_t hp = vn | ~(d0 | vp);
|
|
685
|
+
const std::uint64_t hn = d0 & vp;
|
|
686
|
+
// HP/HN top bits are mutually exclusive under Hyyrö OSA.
|
|
687
|
+
score += static_cast<std::size_t>((hp >> shift) & 1U);
|
|
688
|
+
score -= static_cast<std::size_t>((hn >> shift) & 1U);
|
|
689
|
+
const std::uint64_t hp_shift = (hp << 1) | one;
|
|
690
|
+
const std::uint64_t hn_shift = hn << 1;
|
|
691
|
+
vp = hn_shift | ~(d0 | hp_shift);
|
|
692
|
+
vn = hp_shift & d0;
|
|
693
|
+
d0_prev = d0;
|
|
694
|
+
pm_old = pm;
|
|
695
|
+
}
|
|
696
|
+
return score;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Thread-local PEQ + state scratch for the multi-word OSA kernel.
|
|
700
|
+
// Carries the per-block ``vp``, ``vn``, plus the OSA cross-column
|
|
701
|
+
// state vectors ``d0_prev`` and ``pm_old``. Used by the generic-K
|
|
702
|
+
// fallback; the K=2/K=3 hand-spec paths keep everything on the stack.
|
|
703
|
+
struct MultiWordOsaScratch {
|
|
704
|
+
std::vector<std::uint64_t> peq;
|
|
705
|
+
std::vector<std::uint64_t> vp;
|
|
706
|
+
std::vector<std::uint64_t> vn;
|
|
707
|
+
std::vector<std::uint64_t> d0_prev;
|
|
708
|
+
std::vector<std::uint64_t> pm_old;
|
|
709
|
+
std::size_t k = 0;
|
|
710
|
+
|
|
711
|
+
std::vector<std::uint8_t> peq_dirty;
|
|
712
|
+
std::array<std::uint32_t, 256> peq_touch{};
|
|
713
|
+
std::uint32_t peq_gen = 0;
|
|
714
|
+
std::size_t peq_layout_k = 0;
|
|
715
|
+
|
|
716
|
+
void resize_for(std::size_t K) {
|
|
717
|
+
if (K > k) {
|
|
718
|
+
peq.assign(256U * K, 0U);
|
|
719
|
+
vp.resize(K);
|
|
720
|
+
vn.resize(K);
|
|
721
|
+
d0_prev.resize(K);
|
|
722
|
+
pm_old.resize(K);
|
|
723
|
+
peq_dirty.clear();
|
|
724
|
+
peq_layout_k = K;
|
|
725
|
+
peq_touch.fill(0);
|
|
726
|
+
peq_gen = 0;
|
|
727
|
+
k = K;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
void peq_begin(std::size_t K) {
|
|
732
|
+
resize_for(K);
|
|
733
|
+
if (peq_layout_k != K) {
|
|
734
|
+
std::fill_n(peq.data(), 256U * K, std::uint64_t{0});
|
|
735
|
+
peq_dirty.clear();
|
|
736
|
+
peq_layout_k = K;
|
|
737
|
+
} else {
|
|
738
|
+
for (const std::uint8_t s : peq_dirty) {
|
|
739
|
+
std::fill_n(
|
|
740
|
+
peq.data() + static_cast<std::size_t>(s) * K, K, std::uint64_t{0});
|
|
741
|
+
}
|
|
742
|
+
peq_dirty.clear();
|
|
743
|
+
}
|
|
744
|
+
if (++peq_gen == 0U) {
|
|
745
|
+
peq_touch.fill(0);
|
|
746
|
+
peq_gen = 1U;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
void peq_or_bit(std::uint8_t c, std::size_t bit_index, std::size_t K) {
|
|
751
|
+
if (peq_touch[c] != peq_gen) {
|
|
752
|
+
peq_touch[c] = peq_gen;
|
|
753
|
+
peq_dirty.push_back(c);
|
|
754
|
+
}
|
|
755
|
+
peq[static_cast<std::size_t>(c) * K + (bit_index >> 6U)] |=
|
|
756
|
+
std::uint64_t{1} << (bit_index & 63U);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
void peq_build(std::span<const std::uint8_t> pattern, std::size_t K) {
|
|
760
|
+
peq_begin(K);
|
|
761
|
+
for (std::size_t i = 0; i < pattern.size(); ++i) {
|
|
762
|
+
peq_or_bit(pattern[i], i, K);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
inline MultiWordOsaScratch& multi_word_osa_scratch() {
|
|
768
|
+
thread_local MultiWordOsaScratch s;
|
|
769
|
+
return s;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Forward declarations for the K-hand-spec multi-word OSA paths.
|
|
773
|
+
inline std::size_t osa_multi_word_k2_u8(
|
|
774
|
+
std::span<const std::uint8_t> pattern,
|
|
775
|
+
std::span<const std::uint8_t> text) noexcept;
|
|
776
|
+
inline std::size_t osa_multi_word_k3_u8(
|
|
777
|
+
std::span<const std::uint8_t> pattern,
|
|
778
|
+
std::span<const std::uint8_t> text) noexcept;
|
|
779
|
+
inline std::size_t osa_multi_word_generic_u8(
|
|
780
|
+
std::span<const std::uint8_t> pattern,
|
|
781
|
+
std::span<const std::uint8_t> text);
|
|
782
|
+
|
|
783
|
+
// OSA dispatch: bit-parallel single-word for short u8 patterns,
|
|
784
|
+
// K-hand-spec multi-word for m = 65..192, generic multi-word for
|
|
785
|
+
// m > 192. The scalar DP fallback is no longer reached on the byte
|
|
786
|
+
// path — the new multi-word kernels (Hyyrö 2002 generalised across
|
|
787
|
+
// blocks with carry propagation on both the standard Myers add chain
|
|
788
|
+
// and the per-column transposition mask) handle every length.
|
|
789
|
+
inline std::size_t osa_distance_u8(
|
|
790
|
+
std::span<const std::uint8_t> pattern,
|
|
791
|
+
std::span<const std::uint8_t> text) {
|
|
792
|
+
const std::size_t m = pattern.size();
|
|
793
|
+
if (m == 0U) return text.size();
|
|
794
|
+
if (text.empty()) return m;
|
|
795
|
+
if (m <= 64U) return osa_single_word_u8(pattern, text);
|
|
796
|
+
if (m <= 128U) return osa_multi_word_k2_u8(pattern, text);
|
|
797
|
+
if (m <= 192U) return osa_multi_word_k3_u8(pattern, text);
|
|
798
|
+
return osa_multi_word_generic_u8(pattern, text);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Templated K = 2..3 multi-word bit-parallel OSA. Constexpr ``K``
|
|
802
|
+
// keeps the per-block state arrays stack-resident and unrolls the
|
|
803
|
+
// inner block loop. State per block: ``vp[k]``, ``vn[k]``,
|
|
804
|
+
// ``d0_prev[k]``, ``pm_old[k]`` — 4 words/block, so K = 2 fits the
|
|
805
|
+
// x86-64 GPR file comfortably, K = 3 is tight but still register-
|
|
806
|
+
// resident on modern compilers.
|
|
807
|
+
//
|
|
808
|
+
// Carries propagated across blocks within one text column:
|
|
809
|
+
// add_carry — 128-bit ``(pm & vp) + vp + add_carry`` add chain
|
|
810
|
+
// hp_carry_in / hn_carry_in — left-shift of hp / hn
|
|
811
|
+
// trans_carry_in — left-shift of the transposition pre-mask
|
|
812
|
+
//
|
|
813
|
+
// Block 0 of the hp shift initialises ``hp_carry_in = 1`` (matches
|
|
814
|
+
// the standard single-word kernel's ``| 1``). All other carries
|
|
815
|
+
// init to 0.
|
|
816
|
+
template <std::size_t K>
|
|
817
|
+
inline std::size_t osa_multi_word_kN_u8(
|
|
818
|
+
std::span<const std::uint8_t> pattern,
|
|
819
|
+
std::span<const std::uint8_t> text) noexcept {
|
|
820
|
+
static_assert(K >= 2 && K <= 3, "osa_multi_word_kN_u8 covers K=2..3");
|
|
821
|
+
const std::size_t m = pattern.size();
|
|
822
|
+
const std::size_t n = text.size();
|
|
823
|
+
if (m == 0U) return n;
|
|
824
|
+
if (n == 0U) return m;
|
|
825
|
+
const std::uint64_t one = 1U;
|
|
826
|
+
|
|
827
|
+
MultiWordOsaScratch& scr = multi_word_osa_scratch();
|
|
828
|
+
scr.peq_build(pattern, K);
|
|
829
|
+
|
|
830
|
+
std::uint64_t vp[K];
|
|
831
|
+
std::uint64_t vn[K];
|
|
832
|
+
std::uint64_t d0_prev[K];
|
|
833
|
+
std::uint64_t pm_old[K];
|
|
834
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
835
|
+
vp[k] = ~std::uint64_t{0};
|
|
836
|
+
vn[k] = 0;
|
|
837
|
+
d0_prev[k] = 0;
|
|
838
|
+
pm_old[k] = 0;
|
|
839
|
+
}
|
|
840
|
+
const std::size_t last_bits = m - (K - 1U) * 64U;
|
|
841
|
+
if (last_bits < 64U) {
|
|
842
|
+
vp[K - 1U] = (one << last_bits) - 1U;
|
|
843
|
+
}
|
|
844
|
+
const std::uint64_t top_bit_last = one << (last_bits - 1U);
|
|
845
|
+
|
|
846
|
+
std::size_t score = m;
|
|
847
|
+
const std::uint64_t* const peq_base = scr.peq.data();
|
|
848
|
+
|
|
849
|
+
for (const std::uint8_t c : text) {
|
|
850
|
+
const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
|
|
851
|
+
|
|
852
|
+
std::uint64_t add_carry = 0;
|
|
853
|
+
std::uint64_t hp_carry_in = 1U;
|
|
854
|
+
std::uint64_t hn_carry_in = 0;
|
|
855
|
+
std::uint64_t trans_carry_in = 0;
|
|
856
|
+
std::uint64_t last_hp = 0;
|
|
857
|
+
std::uint64_t last_hn = 0;
|
|
858
|
+
|
|
859
|
+
for (std::size_t b = 0; b < K; ++b) {
|
|
860
|
+
const std::uint64_t pm = peq_row[b];
|
|
861
|
+
|
|
862
|
+
// Transposition mask: ``trans = (((~d0_prev) & pm) << 1) & pm_old``
|
|
863
|
+
// with the ``<< 1`` shift carry crossing block boundaries.
|
|
864
|
+
const std::uint64_t trans_premask = (~d0_prev[b]) & pm;
|
|
865
|
+
const std::uint64_t trans_shift = (trans_premask << 1) | trans_carry_in;
|
|
866
|
+
trans_carry_in = trans_premask >> 63;
|
|
867
|
+
const std::uint64_t trans = trans_shift & pm_old[b];
|
|
868
|
+
|
|
869
|
+
// Hyyrö single-step ``d0`` derivation, extended across blocks
|
|
870
|
+
// with an add carry chain on ``(pm & vp) + vp``.
|
|
871
|
+
const std::uint64_t pm_and_vp = pm & vp[b];
|
|
872
|
+
std::uint64_t s1;
|
|
873
|
+
const bool c1 = __builtin_add_overflow(pm_and_vp, vp[b], &s1);
|
|
874
|
+
std::uint64_t sum;
|
|
875
|
+
const bool c2 = __builtin_add_overflow(s1, add_carry, &sum);
|
|
876
|
+
add_carry = static_cast<std::uint64_t>(c1) + static_cast<std::uint64_t>(c2);
|
|
877
|
+
|
|
878
|
+
std::uint64_t d0 = (sum ^ vp[b]) | pm | vn[b];
|
|
879
|
+
d0 |= trans;
|
|
880
|
+
|
|
881
|
+
const std::uint64_t hp = vn[b] | ~(d0 | vp[b]);
|
|
882
|
+
const std::uint64_t hn = d0 & vp[b];
|
|
883
|
+
|
|
884
|
+
const std::uint64_t hp_shift = (hp << 1) | hp_carry_in;
|
|
885
|
+
const std::uint64_t hn_shift = (hn << 1) | hn_carry_in;
|
|
886
|
+
hp_carry_in = hp >> 63;
|
|
887
|
+
hn_carry_in = hn >> 63;
|
|
888
|
+
|
|
889
|
+
vp[b] = hn_shift | ~(d0 | hp_shift);
|
|
890
|
+
vn[b] = d0 & hp_shift;
|
|
891
|
+
|
|
892
|
+
d0_prev[b] = d0;
|
|
893
|
+
pm_old[b] = pm;
|
|
894
|
+
|
|
895
|
+
if (b == K - 1U) {
|
|
896
|
+
last_hp = hp;
|
|
897
|
+
last_hn = hn;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
score += static_cast<std::size_t>((last_hp & top_bit_last) != 0U);
|
|
902
|
+
score -= static_cast<std::size_t>((last_hn & top_bit_last) != 0U);
|
|
903
|
+
}
|
|
904
|
+
return score;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
inline std::size_t osa_multi_word_k2_u8(
|
|
908
|
+
std::span<const std::uint8_t> pattern,
|
|
909
|
+
std::span<const std::uint8_t> text) noexcept {
|
|
910
|
+
return osa_multi_word_kN_u8<2>(pattern, text);
|
|
911
|
+
}
|
|
912
|
+
inline std::size_t osa_multi_word_k3_u8(
|
|
913
|
+
std::span<const std::uint8_t> pattern,
|
|
914
|
+
std::span<const std::uint8_t> text) noexcept {
|
|
915
|
+
return osa_multi_word_kN_u8<3>(pattern, text);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Generic-K fallback for m > 192. Same fused per-block recurrence as
|
|
919
|
+
// the K=2..3 paths but with a runtime K loop and heap-resident state.
|
|
920
|
+
inline std::size_t osa_multi_word_generic_u8(
|
|
921
|
+
std::span<const std::uint8_t> pattern,
|
|
922
|
+
std::span<const std::uint8_t> text) {
|
|
923
|
+
const std::size_t m = pattern.size();
|
|
924
|
+
const std::size_t n = text.size();
|
|
925
|
+
if (m == 0U) return n;
|
|
926
|
+
if (n == 0U) return m;
|
|
927
|
+
constexpr std::size_t kWord = 64U;
|
|
928
|
+
const std::size_t K = (m + kWord - 1U) / kWord;
|
|
929
|
+
const std::uint64_t one = 1U;
|
|
930
|
+
|
|
931
|
+
MultiWordOsaScratch& scr = multi_word_osa_scratch();
|
|
932
|
+
scr.peq_build(pattern, K);
|
|
933
|
+
|
|
934
|
+
std::uint64_t* const vp = scr.vp.data();
|
|
935
|
+
std::uint64_t* const vn = scr.vn.data();
|
|
936
|
+
std::uint64_t* const d0_prev = scr.d0_prev.data();
|
|
937
|
+
std::uint64_t* const pm_old = scr.pm_old.data();
|
|
938
|
+
for (std::size_t k = 0; k < K; ++k) {
|
|
939
|
+
vp[k] = ~std::uint64_t{0};
|
|
940
|
+
vn[k] = 0;
|
|
941
|
+
d0_prev[k] = 0;
|
|
942
|
+
pm_old[k] = 0;
|
|
943
|
+
}
|
|
944
|
+
const std::size_t last_bits = m - (K - 1U) * kWord;
|
|
945
|
+
if (last_bits < kWord) {
|
|
946
|
+
vp[K - 1U] = (one << last_bits) - 1U;
|
|
947
|
+
}
|
|
948
|
+
const std::uint64_t top_bit_last = one << (last_bits - 1U);
|
|
949
|
+
|
|
950
|
+
std::size_t score = m;
|
|
951
|
+
const std::uint64_t* const peq_base = scr.peq.data();
|
|
952
|
+
|
|
953
|
+
for (const std::uint8_t c : text) {
|
|
954
|
+
const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
|
|
955
|
+
|
|
956
|
+
std::uint64_t add_carry = 0;
|
|
957
|
+
std::uint64_t hp_carry_in = 1U;
|
|
958
|
+
std::uint64_t hn_carry_in = 0;
|
|
959
|
+
std::uint64_t trans_carry_in = 0;
|
|
960
|
+
std::uint64_t last_hp = 0;
|
|
961
|
+
std::uint64_t last_hn = 0;
|
|
962
|
+
|
|
963
|
+
for (std::size_t b = 0; b < K; ++b) {
|
|
964
|
+
const std::uint64_t pm = peq_row[b];
|
|
965
|
+
|
|
966
|
+
const std::uint64_t trans_premask = (~d0_prev[b]) & pm;
|
|
967
|
+
const std::uint64_t trans_shift = (trans_premask << 1) | trans_carry_in;
|
|
968
|
+
trans_carry_in = trans_premask >> 63;
|
|
969
|
+
const std::uint64_t trans = trans_shift & pm_old[b];
|
|
970
|
+
|
|
971
|
+
const std::uint64_t pm_and_vp = pm & vp[b];
|
|
972
|
+
std::uint64_t s1;
|
|
973
|
+
const bool c1 = __builtin_add_overflow(pm_and_vp, vp[b], &s1);
|
|
974
|
+
std::uint64_t sum;
|
|
975
|
+
const bool c2 = __builtin_add_overflow(s1, add_carry, &sum);
|
|
976
|
+
add_carry = static_cast<std::uint64_t>(c1) + static_cast<std::uint64_t>(c2);
|
|
977
|
+
|
|
978
|
+
std::uint64_t d0 = (sum ^ vp[b]) | pm | vn[b];
|
|
979
|
+
d0 |= trans;
|
|
980
|
+
|
|
981
|
+
const std::uint64_t hp = vn[b] | ~(d0 | vp[b]);
|
|
982
|
+
const std::uint64_t hn = d0 & vp[b];
|
|
983
|
+
|
|
984
|
+
const std::uint64_t hp_shift = (hp << 1) | hp_carry_in;
|
|
985
|
+
const std::uint64_t hn_shift = (hn << 1) | hn_carry_in;
|
|
986
|
+
hp_carry_in = hp >> 63;
|
|
987
|
+
hn_carry_in = hn >> 63;
|
|
988
|
+
|
|
989
|
+
vp[b] = hn_shift | ~(d0 | hp_shift);
|
|
990
|
+
vn[b] = d0 & hp_shift;
|
|
991
|
+
|
|
992
|
+
d0_prev[b] = d0;
|
|
993
|
+
pm_old[b] = pm;
|
|
994
|
+
|
|
995
|
+
if (b == K - 1U) {
|
|
996
|
+
last_hp = hp;
|
|
997
|
+
last_hn = hn;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
score += static_cast<std::size_t>((last_hp & top_bit_last) != 0U);
|
|
1002
|
+
score -= static_cast<std::size_t>((last_hn & top_bit_last) != 0U);
|
|
1003
|
+
}
|
|
1004
|
+
return score;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
template <typename Token>
|
|
1008
|
+
inline std::size_t osa_distance(
|
|
1009
|
+
std::span<const Token> pattern,
|
|
1010
|
+
std::span<const Token> text) {
|
|
1011
|
+
static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
|
|
1012
|
+
const std::size_t m = pattern.size();
|
|
1013
|
+
const std::size_t n = text.size();
|
|
1014
|
+
if (m == 0) {
|
|
1015
|
+
return n;
|
|
1016
|
+
}
|
|
1017
|
+
if (n == 0) {
|
|
1018
|
+
return m;
|
|
1019
|
+
}
|
|
1020
|
+
if (m > 64U) {
|
|
1021
|
+
return detail::osa_dp<Token>(pattern, text);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Bit-parallel with hashmap PEQ for wide alphabets.
|
|
1025
|
+
std::unordered_map<Token, std::uint64_t> peq;
|
|
1026
|
+
const std::uint64_t one = 1U;
|
|
1027
|
+
for (std::size_t i = 0; i < m; ++i) {
|
|
1028
|
+
peq[pattern[i]] |= one << i;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const std::size_t shift = m - 1U;
|
|
1032
|
+
std::uint64_t vp = (m == 64U) ? ~std::uint64_t{0} : ((one << m) - 1U);
|
|
1033
|
+
std::uint64_t vn = 0;
|
|
1034
|
+
std::uint64_t d0_prev = 0;
|
|
1035
|
+
std::uint64_t pm_old = 0;
|
|
1036
|
+
std::size_t score = m;
|
|
1037
|
+
|
|
1038
|
+
for (const auto c : text) {
|
|
1039
|
+
auto it = peq.find(c);
|
|
1040
|
+
const std::uint64_t pm = (it == peq.end()) ? 0U : it->second;
|
|
1041
|
+
const std::uint64_t trans = (((~d0_prev) & pm) << 1) & pm_old;
|
|
1042
|
+
std::uint64_t d0 = (((pm & vp) + vp) ^ vp) | pm | vn;
|
|
1043
|
+
d0 |= trans;
|
|
1044
|
+
|
|
1045
|
+
const std::uint64_t hp = vn | ~(d0 | vp);
|
|
1046
|
+
const std::uint64_t hn = d0 & vp;
|
|
1047
|
+
score += static_cast<std::size_t>((hp >> shift) & 1U);
|
|
1048
|
+
score -= static_cast<std::size_t>((hn >> shift) & 1U);
|
|
1049
|
+
const std::uint64_t hp_shift = (hp << 1) | one;
|
|
1050
|
+
const std::uint64_t hn_shift = hn << 1;
|
|
1051
|
+
vp = hn_shift | ~(d0 | hp_shift);
|
|
1052
|
+
vn = hp_shift & d0;
|
|
1053
|
+
d0_prev = d0;
|
|
1054
|
+
pm_old = pm;
|
|
1055
|
+
}
|
|
1056
|
+
return score;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
inline double normalize(std::size_t distance, std::size_t a_len, std::size_t b_len) noexcept {
|
|
1060
|
+
const std::size_t longer = (a_len > b_len) ? a_len : b_len;
|
|
1061
|
+
if (longer == 0U) {
|
|
1062
|
+
return 1.0;
|
|
1063
|
+
}
|
|
1064
|
+
const double ratio =
|
|
1065
|
+
static_cast<double>(distance) / static_cast<double>(longer);
|
|
1066
|
+
if (ratio >= 1.0) {
|
|
1067
|
+
return 0.0;
|
|
1068
|
+
}
|
|
1069
|
+
return 1.0 - ratio;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// =================================================================
|
|
1073
|
+
// True Damerau-Levenshtein distance — the unrestricted form. Same
|
|
1074
|
+
// edits as OSA (insertion, deletion, substitution, transposition of
|
|
1075
|
+
// adjacent characters) but a character can participate in more than
|
|
1076
|
+
// one edit. The standard example where OSA and true DL differ is
|
|
1077
|
+
// "CA" -> "ABC":
|
|
1078
|
+
// OSA = 3 (no useful transposition; each edit fights for a single
|
|
1079
|
+
// character)
|
|
1080
|
+
// true DL = 2 (transpose C,A then insert B — though both edits
|
|
1081
|
+
// touch position 0)
|
|
1082
|
+
//
|
|
1083
|
+
// The algorithm uses an (n+2) by (m+2) DP table plus an alphabet-
|
|
1084
|
+
// indexed "last row of each character" map. O(n*m) time, O(n*m + |Σ|)
|
|
1085
|
+
// space. No bit-parallel form is shipped here — Hyyrö 2003 has one
|
|
1086
|
+
// but it's significantly more complex than the OSA bit-parallel and
|
|
1087
|
+
// rarely the bottleneck in practice. SIMD batching falls back to
|
|
1088
|
+
// per-pair scalar dispatch.
|
|
1089
|
+
//
|
|
1090
|
+
// Rationale for keeping both: most callers asking for
|
|
1091
|
+
// "Damerau-Levenshtein" actually want OSA (faster, equivalent in
|
|
1092
|
+
// almost every realistic input), but a strict minority needs the
|
|
1093
|
+
// unrestricted variant.
|
|
1094
|
+
// =================================================================
|
|
1095
|
+
|
|
1096
|
+
namespace detail {
|
|
1097
|
+
|
|
1098
|
+
template <typename Token>
|
|
1099
|
+
inline std::size_t true_damerau_dp(
|
|
1100
|
+
std::span<const Token> pattern,
|
|
1101
|
+
std::span<const Token> text) {
|
|
1102
|
+
const std::size_t n = pattern.size();
|
|
1103
|
+
const std::size_t m = text.size();
|
|
1104
|
+
if (n == 0U) return m;
|
|
1105
|
+
if (m == 0U) return n;
|
|
1106
|
+
|
|
1107
|
+
const std::size_t inf = n + m;
|
|
1108
|
+
|
|
1109
|
+
// (n+2) by (m+2) DP table. Rows 0 and column 0 are sentinels
|
|
1110
|
+
// populated with `inf` so the transposition lookup is never the
|
|
1111
|
+
// minimum when there's no valid prior occurrence.
|
|
1112
|
+
std::vector<std::vector<std::size_t>> h(
|
|
1113
|
+
n + 2U, std::vector<std::size_t>(m + 2U, 0));
|
|
1114
|
+
h[0][0] = inf;
|
|
1115
|
+
for (std::size_t i = 0; i <= n; ++i) {
|
|
1116
|
+
h[i + 1U][0] = inf;
|
|
1117
|
+
h[i + 1U][1] = i;
|
|
1118
|
+
}
|
|
1119
|
+
for (std::size_t j = 0; j <= m; ++j) {
|
|
1120
|
+
h[0][j + 1U] = inf;
|
|
1121
|
+
h[1][j + 1U] = j;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// `da` maps each character to the last row in which it appeared
|
|
1125
|
+
// (1-indexed; 0 means "not yet seen"). Using std::unordered_map so
|
|
1126
|
+
// the algorithm works for arbitrary integral tokens, not just bytes.
|
|
1127
|
+
std::unordered_map<Token, std::size_t> da;
|
|
1128
|
+
|
|
1129
|
+
for (std::size_t i = 1; i <= n; ++i) {
|
|
1130
|
+
std::size_t db = 0;
|
|
1131
|
+
for (std::size_t j = 1; j <= m; ++j) {
|
|
1132
|
+
const Token tj = text[j - 1U];
|
|
1133
|
+
const std::size_t k = [&] {
|
|
1134
|
+
auto it = da.find(tj);
|
|
1135
|
+
return it == da.end() ? std::size_t{0} : it->second;
|
|
1136
|
+
}();
|
|
1137
|
+
const std::size_t l = db;
|
|
1138
|
+
std::size_t cost;
|
|
1139
|
+
if (pattern[i - 1U] == tj) {
|
|
1140
|
+
cost = 0;
|
|
1141
|
+
db = j;
|
|
1142
|
+
} else {
|
|
1143
|
+
cost = 1;
|
|
1144
|
+
}
|
|
1145
|
+
const std::size_t sub = h[i][j] + cost;
|
|
1146
|
+
const std::size_t ins = h[i + 1U][j] + 1U;
|
|
1147
|
+
const std::size_t del = h[i][j + 1U] + 1U;
|
|
1148
|
+
const std::size_t trans = h[k][l] +
|
|
1149
|
+
(i > k ? (i - k - 1U) : 0U) + 1U +
|
|
1150
|
+
(j > l ? (j - l - 1U) : 0U);
|
|
1151
|
+
// Note: when k == 0 or l == 0, h[k][l] = inf (or h[k][...] = j
|
|
1152
|
+
// which is large enough to dominate any other option).
|
|
1153
|
+
h[i + 1U][j + 1U] =
|
|
1154
|
+
std::min({sub, ins, del, trans});
|
|
1155
|
+
}
|
|
1156
|
+
da[pattern[i - 1U]] = i;
|
|
1157
|
+
}
|
|
1158
|
+
return h[n + 1U][m + 1U];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
} // namespace detail
|
|
1162
|
+
|
|
1163
|
+
template <typename Token>
|
|
1164
|
+
inline std::size_t true_damerau_levenshtein_distance(
|
|
1165
|
+
std::span<const Token> pattern,
|
|
1166
|
+
std::span<const Token> text) {
|
|
1167
|
+
static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
|
|
1168
|
+
return detail::true_damerau_dp<Token>(pattern, text);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Byte-specialised Lowrance-Wagner DP: flat ``(n+2) * (m+2)`` table
|
|
1172
|
+
// (single allocation, contiguous memory) and ``std::array<size_t, 256>``
|
|
1173
|
+
// for the "last-seen-row-per-character" tracker (no unordered_map
|
|
1174
|
+
// hash / chain walk per cell). On ASCII inputs the cell cost drops
|
|
1175
|
+
// from ~140 ns to ~30 ns each, which is the bulk of the
|
|
1176
|
+
// rapidfuzz-shim ``DamerauLevenshtein.distance`` gap on
|
|
1177
|
+
// medium-length pairs.
|
|
1178
|
+
inline std::size_t true_damerau_levenshtein_distance_u8(
|
|
1179
|
+
std::span<const std::uint8_t> pattern,
|
|
1180
|
+
std::span<const std::uint8_t> text) {
|
|
1181
|
+
const std::size_t n = pattern.size();
|
|
1182
|
+
const std::size_t m = text.size();
|
|
1183
|
+
if (n == 0U) return m;
|
|
1184
|
+
if (m == 0U) return n;
|
|
1185
|
+
|
|
1186
|
+
const std::size_t inf = n + m;
|
|
1187
|
+
const std::size_t stride = m + 2U;
|
|
1188
|
+
|
|
1189
|
+
// Thread-local scratch — amortises the (n+2)*(m+2) allocation
|
|
1190
|
+
// across calls. Reset to ``inf`` only where the recurrence reads
|
|
1191
|
+
// the sentinel rows / columns.
|
|
1192
|
+
thread_local std::vector<std::size_t> h;
|
|
1193
|
+
h.assign((n + 2U) * stride, 0);
|
|
1194
|
+
|
|
1195
|
+
h[0 * stride + 0] = inf;
|
|
1196
|
+
for (std::size_t i = 0; i <= n; ++i) {
|
|
1197
|
+
h[(i + 1U) * stride + 0] = inf;
|
|
1198
|
+
h[(i + 1U) * stride + 1] = i;
|
|
1199
|
+
}
|
|
1200
|
+
for (std::size_t j = 0; j <= m; ++j) {
|
|
1201
|
+
h[0 * stride + (j + 1U)] = inf;
|
|
1202
|
+
h[1 * stride + (j + 1U)] = j;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// ``da`` maps each byte to the last row it appeared in
|
|
1206
|
+
// (1-indexed). 0 means "not yet seen". 256 entries, fits in 2 KiB —
|
|
1207
|
+
// hot in L1 across the whole call.
|
|
1208
|
+
thread_local std::array<std::size_t, 256> da_buf;
|
|
1209
|
+
da_buf.fill(0);
|
|
1210
|
+
|
|
1211
|
+
for (std::size_t i = 1; i <= n; ++i) {
|
|
1212
|
+
std::size_t db = 0;
|
|
1213
|
+
const std::uint8_t pi = pattern[i - 1U];
|
|
1214
|
+
const std::size_t* const row_prev = h.data() + i * stride;
|
|
1215
|
+
std::size_t* const row_curr = h.data() + (i + 1U) * stride;
|
|
1216
|
+
for (std::size_t j = 1; j <= m; ++j) {
|
|
1217
|
+
const std::uint8_t tj = text[j - 1U];
|
|
1218
|
+
const std::size_t k = da_buf[tj];
|
|
1219
|
+
const std::size_t l = db;
|
|
1220
|
+
|
|
1221
|
+
std::size_t cost;
|
|
1222
|
+
if (pi == tj) {
|
|
1223
|
+
cost = 0;
|
|
1224
|
+
db = j;
|
|
1225
|
+
} else {
|
|
1226
|
+
cost = 1;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const std::size_t sub = row_prev[j] + cost;
|
|
1230
|
+
const std::size_t ins = row_curr[j] + 1U;
|
|
1231
|
+
const std::size_t del = row_prev[j + 1U] + 1U;
|
|
1232
|
+
const std::size_t trans = h[k * stride + l] +
|
|
1233
|
+
(i > k ? (i - k - 1U) : 0U) + 1U +
|
|
1234
|
+
(j > l ? (j - l - 1U) : 0U);
|
|
1235
|
+
|
|
1236
|
+
std::size_t best = sub;
|
|
1237
|
+
if (ins < best) best = ins;
|
|
1238
|
+
if (del < best) best = del;
|
|
1239
|
+
if (trans < best) best = trans;
|
|
1240
|
+
row_curr[j + 1U] = best;
|
|
1241
|
+
}
|
|
1242
|
+
da_buf[pi] = i;
|
|
1243
|
+
}
|
|
1244
|
+
return h[(n + 1U) * stride + (m + 1U)];
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
} // namespace stride_align::levenshtein
|