RapidFuzz
Loading...
Searching...
No Matches
Levenshtein_impl.hpp
1/* SPDX-License-Identifier: MIT */
2/* Copyright © 2022-present Max Bachmann */
3
4#include <cstddef>
5#include <cstdint>
6#include <limits>
7#include <rapidfuzz/details/GrowingHashmap.hpp>
8#include <rapidfuzz/details/Matrix.hpp>
9#include <rapidfuzz/details/PatternMatchVector.hpp>
10#include <rapidfuzz/details/common.hpp>
11#include <rapidfuzz/details/distance.hpp>
12#include <rapidfuzz/details/intrinsics.hpp>
13#include <rapidfuzz/details/type_traits.hpp>
14#include <rapidfuzz/distance/Indel.hpp>
15#include <sys/types.h>
16
17namespace rapidfuzz {
18namespace detail {
19
20struct LevenshteinRow {
21 uint64_t VP;
22 uint64_t VN;
23
24 LevenshteinRow() : VP(~UINT64_C(0)), VN(0)
25 {}
26
27 LevenshteinRow(uint64_t VP_, uint64_t VN_) : VP(VP_), VN(VN_)
28 {}
29};
30
31template <bool RecordMatrix, bool RecordBitRow>
32struct LevenshteinResult;
33
34template <>
35struct LevenshteinResult<true, false> {
36 ShiftedBitMatrix<uint64_t> VP;
37 ShiftedBitMatrix<uint64_t> VN;
38
39 size_t dist;
40};
41
42template <>
43struct LevenshteinResult<false, true> {
44 size_t first_block;
45 size_t last_block;
46 size_t prev_score;
47 std::vector<LevenshteinRow> vecs;
48
49 size_t dist;
50};
51
52template <>
53struct LevenshteinResult<false, false> {
54 size_t dist;
55};
56
57template <bool RecordMatrix, bool RecordBitRow>
58LevenshteinResult<true, false>& getMatrixRef(LevenshteinResult<RecordMatrix, RecordBitRow>& res)
59{
60#if RAPIDFUZZ_IF_CONSTEXPR_AVAILABLE
61 return res;
62#else
63 // this is a hack since the compiler doesn't know early enough that
64 // this is never called when the types differ.
65 // On C++17 this properly uses if constexpr
66 assert(RecordMatrix);
67 return reinterpret_cast<LevenshteinResult<true, false>&>(res);
68#endif
69}
70
71template <bool RecordMatrix, bool RecordBitRow>
72LevenshteinResult<false, true>& getBitRowRef(LevenshteinResult<RecordMatrix, RecordBitRow>& res)
73{
74#if RAPIDFUZZ_IF_CONSTEXPR_AVAILABLE
75 return res;
76#else
77 // this is a hack since the compiler doesn't know early enough that
78 // this is never called when the types differ.
79 // On C++17 this properly uses if constexpr
80 assert(RecordBitRow);
81 return reinterpret_cast<LevenshteinResult<false, true>&>(res);
82#endif
83}
84
85template <typename InputIt1, typename InputIt2>
86size_t generalized_levenshtein_wagner_fischer(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
87 LevenshteinWeightTable weights, size_t max)
88{
89 size_t cache_size = s1.size() + 1;
90 std::vector<size_t> cache(cache_size);
91 assume(cache_size != 0);
92
93 for (size_t i = 0; i < cache_size; ++i)
94 cache[i] = i * weights.delete_cost;
95
96 for (const auto& ch2 : s2) {
97 auto cache_iter = cache.begin();
98 size_t temp = *cache_iter;
99 *cache_iter += weights.insert_cost;
100
101 for (const auto& ch1 : s1) {
102 if (ch1 != ch2)
103 temp = std::min({*cache_iter + weights.delete_cost, *(cache_iter + 1) + weights.insert_cost,
104 temp + weights.replace_cost});
105 ++cache_iter;
106 std::swap(*cache_iter, temp);
107 }
108 }
109
110 size_t dist = cache.back();
111 return (dist <= max) ? dist : max + 1;
112}
113
118static inline size_t levenshtein_maximum(size_t len1, size_t len2, LevenshteinWeightTable weights)
119{
120 size_t max_dist = len1 * weights.delete_cost + len2 * weights.insert_cost;
121
122 if (len1 >= len2)
123 max_dist = std::min(max_dist, len2 * weights.replace_cost + (len1 - len2) * weights.delete_cost);
124 else
125 max_dist = std::min(max_dist, len1 * weights.replace_cost + (len2 - len1) * weights.insert_cost);
126
127 return max_dist;
128}
129
134template <typename InputIt1, typename InputIt2>
135size_t levenshtein_min_distance(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
136 LevenshteinWeightTable weights)
137{
138 if (s1.size() > s2.size())
139 return (s1.size() - s2.size()) * weights.delete_cost;
140 else
141 return (s2.size() - s1.size()) * weights.insert_cost;
142}
143
144template <typename InputIt1, typename InputIt2>
145size_t generalized_levenshtein_distance(Range<InputIt1> s1, Range<InputIt2> s2,
146 LevenshteinWeightTable weights, size_t max)
147{
148 size_t min_edits = levenshtein_min_distance(s1, s2, weights);
149 if (min_edits > max) return max + 1;
150
151 /* common affix does not effect Levenshtein distance */
152 remove_common_affix(s1, s2);
153
154 return generalized_levenshtein_wagner_fischer(s1, s2, weights, max);
155}
156
157/*
158 * An encoded mbleven model table.
159 *
160 * Each 8-bit integer represents an edit sequence, with using two
161 * bits for a single operation.
162 *
163 * Each Row of 8 integers represent all possible combinations
164 * of edit sequences for a gived maximum edit distance and length
165 * difference between the two strings, that is below the maximum
166 * edit distance
167 *
168 * 01 = DELETE, 10 = INSERT, 11 = SUBSTITUTE
169 *
170 * For example, 3F -> 0b111111 means three substitutions
171 */
172static constexpr std::array<std::array<uint8_t, 7>, 9> levenshtein_mbleven2018_matrix = {{
173 /* max edit distance 1 */
174 {0x03}, /* len_diff 0 */
175 {0x01}, /* len_diff 1 */
176 /* max edit distance 2 */
177 {0x0F, 0x09, 0x06}, /* len_diff 0 */
178 {0x0D, 0x07}, /* len_diff 1 */
179 {0x05}, /* len_diff 2 */
180 /* max edit distance 3 */
181 {0x3F, 0x27, 0x2D, 0x39, 0x36, 0x1E, 0x1B}, /* len_diff 0 */
182 {0x3D, 0x37, 0x1F, 0x25, 0x19, 0x16}, /* len_diff 1 */
183 {0x35, 0x1D, 0x17}, /* len_diff 2 */
184 {0x15}, /* len_diff 3 */
185}};
186
187template <typename InputIt1, typename InputIt2>
188size_t levenshtein_mbleven2018(const Range<InputIt1>& s1, const Range<InputIt2>& s2, size_t max)
189{
190 size_t len1 = s1.size();
191 size_t len2 = s2.size();
192 assert(len1 > 0);
193 assert(len2 > 0);
194 assert(*s1.begin() != *s2.begin());
195 assert(*std::prev(s1.end()) != *std::prev(s2.end()));
196
197 if (len1 < len2) return levenshtein_mbleven2018(s2, s1, max);
198
199 size_t len_diff = len1 - len2;
200
201 if (max == 1) return max + static_cast<size_t>(len_diff == 1 || len1 != 1);
202
203 size_t ops_index = (max + max * max) / 2 + len_diff - 1;
204 auto& possible_ops = levenshtein_mbleven2018_matrix[ops_index];
205 size_t dist = max + 1;
206
207 for (uint8_t ops : possible_ops) {
208 auto iter_s1 = s1.begin();
209 auto iter_s2 = s2.begin();
210 size_t cur_dist = 0;
211
212 if (!ops) break;
213
214 while (iter_s1 != s1.end() && iter_s2 != s2.end()) {
215 if (*iter_s1 != *iter_s2) {
216 cur_dist++;
217 if (!ops) break;
218 if (ops & 1) iter_s1++;
219 if (ops & 2) iter_s2++;
220#if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && __GNUC__ < 10
221# pragma GCC diagnostic push
222# pragma GCC diagnostic ignored "-Wconversion"
223#endif
224 ops >>= 2;
225#if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && __GNUC__ < 10
226# pragma GCC diagnostic pop
227#endif
228 }
229 else {
230 iter_s1++;
231 iter_s2++;
232 }
233 }
234 cur_dist += static_cast<size_t>(std::distance(iter_s1, s1.end()) + std::distance(iter_s2, s2.end()));
235 dist = std::min(dist, cur_dist);
236 }
237
238 return (dist <= max) ? dist : max + 1;
239}
240
259template <bool RecordMatrix, bool RecordBitRow, typename PM_Vec, typename InputIt1, typename InputIt2>
260auto levenshtein_hyrroe2003(const PM_Vec& PM, const Range<InputIt1>& s1, const Range<InputIt2>& s2,
261 size_t max = std::numeric_limits<size_t>::max())
262 -> LevenshteinResult<RecordMatrix, RecordBitRow>
263{
264 assert(s1.size() != 0);
265
266 /* VP is set to 1^m. Shifting by bitwidth would be undefined behavior */
267 uint64_t VP = ~UINT64_C(0);
268 uint64_t VN = 0;
269
270 LevenshteinResult<RecordMatrix, RecordBitRow> res;
271 res.dist = s1.size();
272 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
273 auto& res_ = getMatrixRef(res);
274 res_.VP = ShiftedBitMatrix<uint64_t>(s2.size(), 1, ~UINT64_C(0));
275 res_.VN = ShiftedBitMatrix<uint64_t>(s2.size(), 1, 0);
276 }
277
278 /* mask used when computing D[m,j] in the paper 10^(m-1) */
279 uint64_t mask = UINT64_C(1) << (s1.size() - 1);
280
281 /* Searching */
282 auto iter_s2 = s2.begin();
283 for (size_t i = 0; iter_s2 != s2.end(); ++iter_s2, ++i) {
284 /* Step 1: Computing D0 */
285 uint64_t PM_j = PM.get(0, *iter_s2);
286 uint64_t X = PM_j;
287 uint64_t D0 = (((X & VP) + VP) ^ VP) | X | VN;
288
289 /* Step 2: Computing HP and HN */
290 uint64_t HP = VN | ~(D0 | VP);
291 uint64_t HN = D0 & VP;
292
293 /* Step 3: Computing the value D[m,j] */
294 res.dist += bool(HP & mask);
295 res.dist -= bool(HN & mask);
296
297 /* Step 4: Computing Vp and VN */
298 HP = (HP << 1) | 1;
299 HN = (HN << 1);
300
301 VP = HN | ~(D0 | HP);
302 VN = HP & D0;
303
304 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
305 auto& res_ = getMatrixRef(res);
306 res_.VP[i][0] = VP;
307 res_.VN[i][0] = VN;
308 }
309 }
310
311 if (res.dist > max) res.dist = max + 1;
312
313 RAPIDFUZZ_IF_CONSTEXPR (RecordBitRow) {
314 auto& res_ = getBitRowRef(res);
315 res_.first_block = 0;
316 res_.last_block = 0;
317 res_.prev_score = s2.size();
318 res_.vecs.emplace_back(VP, VN);
319 }
320
321 return res;
322}
323
324#ifdef RAPIDFUZZ_SIMD
325template <typename VecType, typename InputIt, int _lto_hack = RAPIDFUZZ_LTO_HACK>
326void levenshtein_hyrroe2003_simd(Range<size_t*> scores, const detail::BlockPatternMatchVector& block,
327 const std::vector<size_t>& s1_lengths, const Range<InputIt>& s2,
328 size_t score_cutoff) noexcept
329{
330# ifdef RAPIDFUZZ_AVX2
331 using namespace simd_avx2;
332# else
333 using namespace simd_sse2;
334# endif
335 static constexpr size_t alignment = native_simd<VecType>::alignment;
336 static constexpr size_t vec_width = native_simd<VecType>::size;
337 static constexpr size_t vecs = native_simd<uint64_t>::size;
338 assert(block.size() % vecs == 0);
339
340 native_simd<VecType> zero(VecType(0));
341 native_simd<VecType> one(1);
342 size_t result_index = 0;
343
344 for (size_t cur_vec = 0; cur_vec < block.size(); cur_vec += vecs) {
345 /* VP is set to 1^m */
346 native_simd<VecType> VP(static_cast<VecType>(-1));
347 native_simd<VecType> VN(VecType(0));
348
349 alignas(alignment) std::array<VecType, vec_width> currDist_;
350 unroll<size_t, vec_width>(
351 [&](size_t i) { currDist_[i] = static_cast<VecType>(s1_lengths[result_index + i]); });
352 native_simd<VecType> currDist(reinterpret_cast<uint64_t*>(currDist_.data()));
353 /* mask used when computing D[m,j] in the paper 10^(m-1) */
354 alignas(alignment) std::array<VecType, vec_width> mask_;
355 unroll<size_t, vec_width>([&](size_t i) {
356 if (s1_lengths[result_index + i] == 0)
357 mask_[i] = 0;
358 else
359 mask_[i] = static_cast<VecType>(UINT64_C(1) << (s1_lengths[result_index + i] - 1));
360 });
361 native_simd<VecType> mask(reinterpret_cast<uint64_t*>(mask_.data()));
362
363 for (const auto& ch : s2) {
364 /* Step 1: Computing D0 */
365 alignas(alignment) std::array<uint64_t, vecs> stored;
366 unroll<size_t, vecs>([&](size_t i) { stored[i] = block.get(cur_vec + i, ch); });
367
368 native_simd<VecType> X(stored.data());
369 auto D0 = (((X & VP) + VP) ^ VP) | X | VN;
370
371 /* Step 2: Computing HP and HN */
372 auto HP = VN | ~(D0 | VP);
373 auto HN = D0 & VP;
374
375 /* Step 3: Computing the value D[m,j] */
376 currDist += andnot(one, (HP & mask) == zero);
377 currDist -= andnot(one, (HN & mask) == zero);
378
379 /* Step 4: Computing Vp and VN */
380 HP = (HP << 1) | one;
381 HN = (HN << 1);
382
383 VP = HN | ~(D0 | HP);
384 VN = HP & D0;
385 }
386
387 alignas(alignment) std::array<VecType, vec_width> distances;
388 currDist.store(distances.data());
389
390 unroll<size_t, vec_width>([&](size_t i) {
391 size_t score = 0;
392 /* strings of length 0 are not handled correctly */
393 if (s1_lengths[result_index] == 0) {
394 score = s2.size();
395 }
396 /* calculate score under consideration of wraparounds in parallel counter */
397 else {
398 RAPIDFUZZ_IF_CONSTEXPR (std::numeric_limits<VecType>::max() <
399 std::numeric_limits<size_t>::max())
400 {
401 size_t min_dist = abs_diff(s1_lengths[result_index], s2.size());
402 size_t wraparound_score = static_cast<size_t>(std::numeric_limits<VecType>::max()) + 1;
403
404 score = (min_dist / wraparound_score) * wraparound_score;
405 VecType remainder = static_cast<VecType>(min_dist % wraparound_score);
406
407 if (distances[i] < remainder) score += wraparound_score;
408 }
409
410 score += distances[i];
411 }
412 scores[result_index] = (score <= score_cutoff) ? score : score_cutoff + 1;
413 result_index++;
414 });
415 }
416}
417#endif
418
419template <typename InputIt1, typename InputIt2>
420size_t levenshtein_hyrroe2003_small_band(const BlockPatternMatchVector& PM, const Range<InputIt1>& s1,
421 const Range<InputIt2>& s2, size_t max)
422{
423 /* VP is set to 1^m. */
424 uint64_t VP = ~UINT64_C(0) << (64 - max - 1);
425 uint64_t VN = 0;
426
427 const auto words = PM.size();
428 size_t currDist = max;
429 uint64_t diagonal_mask = UINT64_C(1) << 63;
430 uint64_t horizontal_mask = UINT64_C(1) << 62;
431 ptrdiff_t start_pos = static_cast<ptrdiff_t>(max) + 1 - 64;
432
433 /* score can decrease along the horizontal, but not along the diagonal */
434 size_t break_score = 2 * max + s2.size() - s1.size();
435
436 /* Searching */
437 size_t i = 0;
438 if (s1.size() > max) {
439 for (; i < s1.size() - max; ++i, ++start_pos) {
440 /* Step 1: Computing D0 */
441 uint64_t PM_j = 0;
442 if (start_pos < 0) {
443 PM_j = PM.get(0, s2[i]) << (-start_pos);
444 }
445 else {
446 size_t word = static_cast<size_t>(start_pos) / 64;
447 size_t word_pos = static_cast<size_t>(start_pos) % 64;
448
449 PM_j = PM.get(word, s2[i]) >> word_pos;
450
451 if (word + 1 < words && word_pos != 0) PM_j |= PM.get(word + 1, s2[i]) << (64 - word_pos);
452 }
453 uint64_t X = PM_j;
454 uint64_t D0 = (((X & VP) + VP) ^ VP) | X | VN;
455
456 /* Step 2: Computing HP and HN */
457 uint64_t HP = VN | ~(D0 | VP);
458 uint64_t HN = D0 & VP;
459
460 /* Step 3: Computing the value D[m,j] */
461 currDist += !bool(D0 & diagonal_mask);
462
463 if (currDist > break_score) return max + 1;
464
465 /* Step 4: Computing Vp and VN */
466 VP = HN | ~((D0 >> 1) | HP);
467 VN = (D0 >> 1) & HP;
468 }
469 }
470
471 for (; i < s2.size(); ++i, ++start_pos) {
472 /* Step 1: Computing D0 */
473 uint64_t PM_j = 0;
474 if (start_pos < 0) {
475 PM_j = PM.get(0, s2[i]) << (-start_pos);
476 }
477 else {
478 size_t word = static_cast<size_t>(start_pos) / 64;
479 size_t word_pos = static_cast<size_t>(start_pos) % 64;
480
481 PM_j = PM.get(word, s2[i]) >> word_pos;
482
483 if (word + 1 < words && word_pos != 0) PM_j |= PM.get(word + 1, s2[i]) << (64 - word_pos);
484 }
485 uint64_t X = PM_j;
486 uint64_t D0 = (((X & VP) + VP) ^ VP) | X | VN;
487
488 /* Step 2: Computing HP and HN */
489 uint64_t HP = VN | ~(D0 | VP);
490 uint64_t HN = D0 & VP;
491
492 /* Step 3: Computing the value D[m,j] */
493 currDist += bool(HP & horizontal_mask);
494 currDist -= bool(HN & horizontal_mask);
495 horizontal_mask >>= 1;
496
497 if (currDist > break_score) return max + 1;
498
499 /* Step 4: Computing Vp and VN */
500 VP = HN | ~((D0 >> 1) | HP);
501 VN = (D0 >> 1) & HP;
502 }
503
504 return (currDist <= max) ? currDist : max + 1;
505}
506
507template <bool RecordMatrix, typename InputIt1, typename InputIt2>
508auto levenshtein_hyrroe2003_small_band(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
509 size_t max) -> LevenshteinResult<RecordMatrix, false>
510{
511 assert(max <= s1.size());
512 assert(max <= s2.size());
513 assert(s2.size() >= s1.size() - max);
514
515 /* VP is set to 1^m. Shifting by bitwidth would be undefined behavior */
516 uint64_t VP = ~UINT64_C(0) << (64 - max - 1);
517 uint64_t VN = 0;
518
519 LevenshteinResult<RecordMatrix, false> res;
520 res.dist = max;
521 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
522 auto& res_ = getMatrixRef(res);
523 res_.VP = ShiftedBitMatrix<uint64_t>(s2.size(), 1, ~UINT64_C(0));
524 res_.VN = ShiftedBitMatrix<uint64_t>(s2.size(), 1, 0);
525
526 ptrdiff_t start_offset = static_cast<ptrdiff_t>(max) + 2 - 64;
527 for (size_t i = 0; i < s2.size(); ++i) {
528 res_.VP.set_offset(i, start_offset + static_cast<ptrdiff_t>(i));
529 res_.VN.set_offset(i, start_offset + static_cast<ptrdiff_t>(i));
530 }
531 }
532
533 uint64_t diagonal_mask = UINT64_C(1) << 63;
534 uint64_t horizontal_mask = UINT64_C(1) << 62;
535
536 /* score can decrease along the horizontal, but not along the diagonal */
537 size_t break_score = 2 * max + s2.size() - (s1.size());
538 HybridGrowingHashmap<typename Range<InputIt1>::value_type, std::pair<ptrdiff_t, uint64_t>> PM;
539
540 auto iter_s1 = s1.begin();
541 for (ptrdiff_t j = -static_cast<ptrdiff_t>(max); j < 0; ++iter_s1, ++j) {
542 auto& x = PM[*iter_s1];
543 x.second = shr64(x.second, j - x.first) | (UINT64_C(1) << 63);
544 x.first = j;
545 }
546
547 /* Searching */
548 size_t i = 0;
549 auto iter_s2 = s2.begin();
550 for (; i < s1.size() - max; ++iter_s2, ++iter_s1, ++i) {
551 /* Step 1: Computing D0 */
552 /* update bitmasks online */
553 uint64_t PM_j = 0;
554 {
555 auto& x = PM[*iter_s1];
556 x.second = shr64(x.second, static_cast<ptrdiff_t>(i) - x.first) | (UINT64_C(1) << 63);
557 x.first = static_cast<ptrdiff_t>(i);
558 }
559 {
560 auto x = PM.get(*iter_s2);
561 PM_j = shr64(x.second, static_cast<ptrdiff_t>(i) - x.first);
562 }
563
564 uint64_t X = PM_j;
565 uint64_t D0 = (((X & VP) + VP) ^ VP) | X | VN;
566
567 /* Step 2: Computing HP and HN */
568 uint64_t HP = VN | ~(D0 | VP);
569 uint64_t HN = D0 & VP;
570
571 /* Step 3: Computing the value D[m,j] */
572 res.dist += !bool(D0 & diagonal_mask);
573
574 if (res.dist > break_score) {
575 res.dist = max + 1;
576 return res;
577 }
578
579 /* Step 4: Computing Vp and VN */
580 VP = HN | ~((D0 >> 1) | HP);
581 VN = (D0 >> 1) & HP;
582
583 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
584 auto& res_ = getMatrixRef(res);
585 res_.VP[i][0] = VP;
586 res_.VN[i][0] = VN;
587 }
588 }
589
590 for (; i < s2.size(); ++iter_s2, ++i) {
591 /* Step 1: Computing D0 */
592 /* update bitmasks online */
593 uint64_t PM_j = 0;
594 if (iter_s1 != s1.end()) {
595 auto& x = PM[*iter_s1];
596 x.second = shr64(x.second, static_cast<ptrdiff_t>(i) - x.first) | (UINT64_C(1) << 63);
597 x.first = static_cast<ptrdiff_t>(i);
598 ++iter_s1;
599 }
600 {
601 auto x = PM.get(*iter_s2);
602 PM_j = shr64(x.second, static_cast<ptrdiff_t>(i) - x.first);
603 }
604
605 uint64_t X = PM_j;
606 uint64_t D0 = (((X & VP) + VP) ^ VP) | X | VN;
607
608 /* Step 2: Computing HP and HN */
609 uint64_t HP = VN | ~(D0 | VP);
610 uint64_t HN = D0 & VP;
611
612 /* Step 3: Computing the value D[m,j] */
613 res.dist += bool(HP & horizontal_mask);
614 res.dist -= bool(HN & horizontal_mask);
615 horizontal_mask >>= 1;
616
617 if (res.dist > break_score) {
618 res.dist = max + 1;
619 return res;
620 }
621
622 /* Step 4: Computing Vp and VN */
623 VP = HN | ~((D0 >> 1) | HP);
624 VN = (D0 >> 1) & HP;
625
626 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
627 auto& res_ = getMatrixRef(res);
628 res_.VP[i][0] = VP;
629 res_.VN[i][0] = VN;
630 }
631 }
632
633 if (res.dist > max) res.dist = max + 1;
634
635 return res;
636}
637
641template <bool RecordMatrix, bool RecordBitRow, typename InputIt1, typename InputIt2>
642auto levenshtein_hyrroe2003_block(const BlockPatternMatchVector& PM, const Range<InputIt1>& s1,
643 const Range<InputIt2>& s2, size_t max = std::numeric_limits<size_t>::max(),
644 size_t stop_row = std::numeric_limits<size_t>::max())
645 -> LevenshteinResult<RecordMatrix, RecordBitRow>
646{
647 LevenshteinResult<RecordMatrix, RecordBitRow> res;
648 if (max < abs_diff(s1.size(), s2.size())) {
649 res.dist = max + 1;
650 return res;
651 }
652
653 size_t word_size = sizeof(uint64_t) * 8;
654 size_t words = PM.size();
655 std::vector<LevenshteinRow> vecs(words);
656 std::vector<size_t> scores(words);
657 uint64_t Last = UINT64_C(1) << ((s1.size() - 1) % word_size);
658
659 for (size_t i = 0; i < words - 1; ++i)
660 scores[i] = (i + 1) * word_size;
661
662 scores[words - 1] = s1.size();
663
664 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
665 auto& res_ = getMatrixRef(res);
666 size_t full_band = std::min(s1.size(), 2 * max + 1);
667 size_t full_band_words = std::min(words, full_band / word_size + 2);
668 res_.VP = ShiftedBitMatrix<uint64_t>(s2.size(), full_band_words, ~UINT64_C(0));
669 res_.VN = ShiftedBitMatrix<uint64_t>(s2.size(), full_band_words, 0);
670 }
671
672 RAPIDFUZZ_IF_CONSTEXPR (RecordBitRow) {
673 auto& res_ = getBitRowRef(res);
674 res_.first_block = 0;
675 res_.last_block = 0;
676 res_.prev_score = 0;
677 }
678
679 max = std::min(max, std::max(s1.size(), s2.size()));
680
681 /* first_block is the index of the first block in Ukkonen band. */
682 size_t first_block = 0;
683 /* last_block is the index of the last block in Ukkonen band. */
684 size_t last_block =
685 std::min(words, ceil_div(std::min(max, (max + s1.size() - s2.size()) / 2) + 1, word_size)) - 1;
686
687 /* Searching */
688 auto iter_s2 = s2.begin();
689 for (size_t row = 0; row < s2.size(); ++iter_s2, ++row) {
690 uint64_t HP_carry = 1;
691 uint64_t HN_carry = 0;
692
693 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
694 auto& res_ = getMatrixRef(res);
695 res_.VP.set_offset(row, static_cast<ptrdiff_t>(first_block * word_size));
696 res_.VN.set_offset(row, static_cast<ptrdiff_t>(first_block * word_size));
697 }
698
699 auto advance_block = [&](size_t word) {
700 /* Step 1: Computing D0 */
701 uint64_t PM_j = PM.get(word, *iter_s2);
702 uint64_t VN = vecs[word].VN;
703 uint64_t VP = vecs[word].VP;
704
705 uint64_t X = PM_j | HN_carry;
706 uint64_t D0 = (((X & VP) + VP) ^ VP) | X | VN;
707
708 /* Step 2: Computing HP and HN */
709 uint64_t HP = VN | ~(D0 | VP);
710 uint64_t HN = D0 & VP;
711
712 uint64_t HP_carry_temp = HP_carry;
713 uint64_t HN_carry_temp = HN_carry;
714 if (word < words - 1) {
715 HP_carry = HP >> 63;
716 HN_carry = HN >> 63;
717 }
718 else {
719 HP_carry = bool(HP & Last);
720 HN_carry = bool(HN & Last);
721 }
722
723 /* Step 4: Computing Vp and VN */
724 HP = (HP << 1) | HP_carry_temp;
725 HN = (HN << 1) | HN_carry_temp;
726
727 vecs[word].VP = HN | ~(D0 | HP);
728 vecs[word].VN = HP & D0;
729
730 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
731 auto& res_ = getMatrixRef(res);
732 res_.VP[row][word - first_block] = vecs[word].VP;
733 res_.VN[row][word - first_block] = vecs[word].VN;
734 }
735
736 return static_cast<int64_t>(HP_carry) - static_cast<int64_t>(HN_carry);
737 };
738
739 auto get_row_num = [&](size_t word) {
740 if (word + 1 == words) return s1.size() - 1;
741 return (word + 1) * word_size - 1;
742 };
743
744 for (size_t word = first_block; word <= last_block /* - 1*/; word++) {
745 /* Step 3: Computing the value D[m,j] */
746 scores[word] = static_cast<size_t>(static_cast<ptrdiff_t>(scores[word]) + advance_block(word));
747 }
748
749 max = static_cast<size_t>(
750 std::min(static_cast<ptrdiff_t>(max),
751 static_cast<ptrdiff_t>(scores[last_block]) +
752 std::max(static_cast<ptrdiff_t>(s2.size()) - static_cast<ptrdiff_t>(row) - 1,
753 static_cast<ptrdiff_t>(s1.size()) -
754 (static_cast<ptrdiff_t>((1 + last_block) * word_size - 1) - 1))));
755
756 /*---------- Adjust number of blocks according to Ukkonen ----------*/
757 // todo on the last word instead of word_size often s1.size() % 64 should be used
758
759 /* Band adjustment: last_block */
760 /* If block is not beneath band, calculate next block. Only next because others are certainly beneath
761 * band. */
762 if (last_block + 1 < words) {
763 ptrdiff_t cond = static_cast<ptrdiff_t>(max + 2 * word_size + row + s1.size()) -
764 static_cast<ptrdiff_t>(scores[last_block] + 2 + s2.size());
765 if (static_cast<ptrdiff_t>(get_row_num(last_block)) < cond) {
766 last_block++;
767 vecs[last_block].VP = ~UINT64_C(0);
768 vecs[last_block].VN = 0;
769
770 size_t chars_in_block = (last_block + 1 == words) ? ((s1.size() - 1) % word_size + 1) : 64;
771 scores[last_block] = scores[last_block - 1] + chars_in_block -
772 opt_static_cast<size_t>(HP_carry) + opt_static_cast<size_t>(HN_carry);
773 // todo probably wrong types
774 scores[last_block] = static_cast<size_t>(static_cast<ptrdiff_t>(scores[last_block]) +
775 advance_block(last_block));
776 }
777 }
778
779 for (; last_block >= first_block; --last_block) {
780 /* in band if score <= k where score >= score_last - word_size + 1 */
781 bool in_band_cond1 = scores[last_block] < max + word_size;
782
783 /* in band if row <= max - score - len2 + len1 + i
784 * if the condition is met for the first cell in the block, it
785 * is met for all other cells in the blocks as well
786 *
787 * this uses a more loose condition similar to edlib:
788 * https://github.com/Martinsos/edlib
789 */
790 ptrdiff_t cond = static_cast<ptrdiff_t>(max + 2 * word_size + row + s1.size() + 1) -
791 static_cast<ptrdiff_t>(scores[last_block] + 2 + s2.size());
792 bool in_band_cond2 = static_cast<ptrdiff_t>(get_row_num(last_block)) <= cond;
793
794 if (in_band_cond1 && in_band_cond2) break;
795 }
796
797 /* Band adjustment: first_block */
798 for (; first_block <= last_block; ++first_block) {
799 /* in band if score <= k where score >= score_last - word_size + 1 */
800 bool in_band_cond1 = scores[first_block] < max + word_size;
801
802 /* in band if row >= score - max - len2 + len1 + i
803 * if this condition is met for the last cell in the block, it
804 * is met for all other cells in the blocks as well
805 */
806 ptrdiff_t cond = static_cast<ptrdiff_t>(scores[first_block] + s1.size() + row) -
807 static_cast<ptrdiff_t>(max + s2.size());
808 bool in_band_cond2 = static_cast<ptrdiff_t>(get_row_num(first_block)) >= cond;
809
810 if (in_band_cond1 && in_band_cond2) break;
811 }
812
813 /* distance is larger than max, so band stops to exist */
814 if (last_block < first_block) {
815 res.dist = max + 1;
816 return res;
817 }
818
819 RAPIDFUZZ_IF_CONSTEXPR (RecordBitRow) {
820 if (row == stop_row) {
821 auto& res_ = getBitRowRef(res);
822 if (first_block == 0)
823 res_.prev_score = stop_row + 1;
824 else {
825 /* count backwards to find score at last position in previous block */
826 size_t relevant_bits = std::min((first_block + 1) * 64, s1.size()) % 64;
827 uint64_t mask = ~UINT64_C(0);
828 if (relevant_bits) mask >>= 64 - relevant_bits;
829
830 res_.prev_score = scores[first_block] + popcount(vecs[first_block].VN & mask) -
831 popcount(vecs[first_block].VP & mask);
832 }
833
834 res_.first_block = first_block;
835 res_.last_block = last_block;
836 res_.vecs = std::move(vecs);
837
838 /* unknown so make sure it is <= max */
839 res_.dist = 0;
840 return res;
841 }
842 }
843 }
844
845 res.dist = scores[words - 1];
846
847 if (res.dist > max) res.dist = max + 1;
848
849 return res;
850}
851
852template <typename InputIt1, typename InputIt2>
853size_t uniform_levenshtein_distance(const BlockPatternMatchVector& block, Range<InputIt1> s1,
854 Range<InputIt2> s2, size_t score_cutoff, size_t score_hint)
855{
856 /* upper bound */
857 score_cutoff = std::min(score_cutoff, std::max(s1.size(), s2.size()));
858 if (score_hint < 31) score_hint = 31;
859
860 // when no differences are allowed a direct comparision is sufficient
861 if (score_cutoff == 0) return s1 != s2;
862
863 if (score_cutoff < abs_diff(s1.size(), s2.size())) return score_cutoff + 1;
864
865 // important to catch, since this causes block to be empty -> raises exception on access
866 if (s1.empty()) return (s2.size() <= score_cutoff) ? s2.size() : score_cutoff + 1;
867
868 /* do this first, since we can not remove any affix in encoded form
869 * todo actually we could at least remove the common prefix and just shift the band
870 */
871 if (score_cutoff >= 4) {
872 // todo could safe up to 25% even without max when ignoring irrelevant paths
873 // in the upper and lower corner
874 size_t full_band = std::min(s1.size(), 2 * score_cutoff + 1);
875
876 if (s1.size() < 65)
877 return levenshtein_hyrroe2003<false, false>(block, s1, s2, score_cutoff).dist;
878 else if (full_band <= 64)
879 return levenshtein_hyrroe2003_small_band(block, s1, s2, score_cutoff);
880
881 while (score_hint < score_cutoff) {
882 full_band = std::min(s1.size(), 2 * score_hint + 1);
883
884 size_t score;
885 if (full_band <= 64)
886 score = levenshtein_hyrroe2003_small_band(block, s1, s2, score_hint);
887 else
888 score = levenshtein_hyrroe2003_block<false, false>(block, s1, s2, score_hint).dist;
889
890 if (score <= score_hint) return score;
891
892 if (std::numeric_limits<size_t>::max() / 2 < score_hint) break;
893
894 score_hint *= 2;
895 }
896
897 return levenshtein_hyrroe2003_block<false, false>(block, s1, s2, score_cutoff).dist;
898 }
899
900 /* common affix does not effect Levenshtein distance */
901 remove_common_affix(s1, s2);
902 if (s1.empty() || s2.empty()) return s1.size() + s2.size();
903
904 return levenshtein_mbleven2018(s1, s2, score_cutoff);
905}
906
907template <typename InputIt1, typename InputIt2>
908size_t uniform_levenshtein_distance(Range<InputIt1> s1, Range<InputIt2> s2, size_t score_cutoff,
909 size_t score_hint)
910{
911 /* Swapping the strings so the second string is shorter */
912 if (s1.size() < s2.size()) return uniform_levenshtein_distance(s2, s1, score_cutoff, score_hint);
913
914 /* upper bound */
915 score_cutoff = std::min(score_cutoff, std::max(s1.size(), s2.size()));
916 if (score_hint < 31) score_hint = 31;
917
918 // when no differences are allowed a direct comparision is sufficient
919 if (score_cutoff == 0) return s1 != s2;
920
921 // at least length difference insertions/deletions required
922 if (score_cutoff < (s1.size() - s2.size())) return score_cutoff + 1;
923
924 /* common affix does not effect Levenshtein distance */
925 remove_common_affix(s1, s2);
926 if (s1.empty() || s2.empty()) return s1.size() + s2.size();
927
928 if (score_cutoff < 4) return levenshtein_mbleven2018(s1, s2, score_cutoff);
929
930 // todo could safe up to 25% even without score_cutoff when ignoring irrelevant paths
931 // in the upper and lower corner
932 size_t full_band = std::min(s1.size(), 2 * score_cutoff + 1);
933
934 /* when the short strings has less then 65 elements Hyyrös' algorithm can be used */
935 if (s2.size() < 65)
936 return levenshtein_hyrroe2003<false, false>(PatternMatchVector(s2), s2, s1, score_cutoff).dist;
937 else if (full_band <= 64)
938 return levenshtein_hyrroe2003_small_band<false>(s1, s2, score_cutoff).dist;
939 else {
940 BlockPatternMatchVector PM(s1);
941 while (score_hint < score_cutoff) {
942 // todo use small band implementation if possible
943 size_t score = levenshtein_hyrroe2003_block<false, false>(PM, s1, s2, score_hint).dist;
944
945 if (score <= score_hint) return score;
946
947 if (std::numeric_limits<size_t>::max() / 2 < score_hint) break;
948
949 score_hint *= 2;
950 }
951
952 return levenshtein_hyrroe2003_block<false, false>(PM, s1, s2, score_cutoff).dist;
953 }
954}
955
959template <typename InputIt1, typename InputIt2>
960void recover_alignment(Editops& editops, const Range<InputIt1>& s1, const Range<InputIt2>& s2,
961 const LevenshteinResult<true, false>& matrix, size_t src_pos, size_t dest_pos,
962 size_t editop_pos)
963{
964 size_t dist = matrix.dist;
965 size_t col = s1.size();
966 size_t row = s2.size();
967
968 while (row && col) {
969 /* Deletion */
970 if (matrix.VP.test_bit(row - 1, col - 1)) {
971 assert(dist > 0);
972 dist--;
973 col--;
974 editops[editop_pos + dist].type = EditType::Delete;
975 editops[editop_pos + dist].src_pos = col + src_pos;
976 editops[editop_pos + dist].dest_pos = row + dest_pos;
977 }
978 else {
979 row--;
980
981 /* Insertion */
982 if (row && matrix.VN.test_bit(row - 1, col - 1)) {
983 assert(dist > 0);
984 dist--;
985 editops[editop_pos + dist].type = EditType::Insert;
986 editops[editop_pos + dist].src_pos = col + src_pos;
987 editops[editop_pos + dist].dest_pos = row + dest_pos;
988 }
989 /* Match/Mismatch */
990 else {
991 col--;
992
993 /* Replace (Matches are not recorded) */
994 if (s1[col] != s2[row]) {
995 assert(dist > 0);
996 dist--;
997 editops[editop_pos + dist].type = EditType::Replace;
998 editops[editop_pos + dist].src_pos = col + src_pos;
999 editops[editop_pos + dist].dest_pos = row + dest_pos;
1000 }
1001 }
1002 }
1003 }
1004
1005 while (col) {
1006 dist--;
1007 col--;
1008 editops[editop_pos + dist].type = EditType::Delete;
1009 editops[editop_pos + dist].src_pos = col + src_pos;
1010 editops[editop_pos + dist].dest_pos = row + dest_pos;
1011 }
1012
1013 while (row) {
1014 dist--;
1015 row--;
1016 editops[editop_pos + dist].type = EditType::Insert;
1017 editops[editop_pos + dist].src_pos = col + src_pos;
1018 editops[editop_pos + dist].dest_pos = row + dest_pos;
1019 }
1020}
1021
1022template <typename InputIt1, typename InputIt2>
1023void levenshtein_align(Editops& editops, const Range<InputIt1>& s1, const Range<InputIt2>& s2,
1024 size_t max = std::numeric_limits<size_t>::max(), size_t src_pos = 0,
1025 size_t dest_pos = 0, size_t editop_pos = 0)
1026{
1027 /* upper bound */
1028 max = std::min(max, std::max(s1.size(), s2.size()));
1029 size_t full_band = std::min(s1.size(), 2 * max + 1);
1030
1031 LevenshteinResult<true, false> matrix;
1032 if (s1.empty() || s2.empty())
1033 matrix.dist = s1.size() + s2.size();
1034 else if (s1.size() <= 64)
1035 matrix = levenshtein_hyrroe2003<true, false>(PatternMatchVector(s1), s1, s2);
1036 else if (full_band <= 64)
1037 matrix = levenshtein_hyrroe2003_small_band<true>(s1, s2, max);
1038 else
1039 matrix = levenshtein_hyrroe2003_block<true, false>(BlockPatternMatchVector(s1), s1, s2, max);
1040
1041 assert(matrix.dist <= max);
1042 if (matrix.dist != 0) {
1043 if (editops.size() == 0) editops.resize(matrix.dist);
1044
1045 recover_alignment(editops, s1, s2, matrix, src_pos, dest_pos, editop_pos);
1046 }
1047}
1048
1049template <typename InputIt1, typename InputIt2>
1050LevenshteinResult<false, true> levenshtein_row(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
1051 size_t max, size_t stop_row)
1052{
1053 return levenshtein_hyrroe2003_block<false, true>(BlockPatternMatchVector(s1), s1, s2, max, stop_row);
1054}
1055
1056template <typename InputIt1, typename InputIt2>
1057size_t levenshtein_distance(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
1058 LevenshteinWeightTable weights = {1, 1, 1},
1059 size_t score_cutoff = std::numeric_limits<size_t>::max(),
1060 size_t score_hint = std::numeric_limits<size_t>::max())
1061{
1062 if (weights.insert_cost == weights.delete_cost) {
1063 /* when insertions + deletions operations are free there can not be any edit distance */
1064 if (weights.insert_cost == 0) return 0;
1065
1066 /* uniform Levenshtein multiplied with the common factor */
1067 if (weights.insert_cost == weights.replace_cost) {
1068 // score_cutoff can make use of the common divisor of the three weights
1069 size_t new_score_cutoff = ceil_div(score_cutoff, weights.insert_cost);
1070 size_t new_score_hint = ceil_div(score_hint, weights.insert_cost);
1071 size_t distance = uniform_levenshtein_distance(s1, s2, new_score_cutoff, new_score_hint);
1072 distance *= weights.insert_cost;
1073 return (distance <= score_cutoff) ? distance : score_cutoff + 1;
1074 }
1075 /*
1076 * when replace_cost >= insert_cost + delete_cost no substitutions are performed
1077 * therefore this can be implemented as InDel distance multiplied with the common factor
1078 */
1079 else if (weights.replace_cost >= weights.insert_cost + weights.delete_cost) {
1080 // score_cutoff can make use of the common divisor of the three weights
1081 size_t new_score_cutoff = ceil_div(score_cutoff, weights.insert_cost);
1082 size_t distance = rapidfuzz::indel_distance(s1, s2, new_score_cutoff);
1083 distance *= weights.insert_cost;
1084 return (distance <= score_cutoff) ? distance : score_cutoff + 1;
1085 }
1086 }
1087
1088 return generalized_levenshtein_distance(s1, s2, weights, score_cutoff);
1089}
1090struct HirschbergPos {
1091 size_t left_score;
1092 size_t right_score;
1093 size_t s1_mid;
1094 size_t s2_mid;
1095};
1096
1097template <typename InputIt1, typename InputIt2>
1098HirschbergPos find_hirschberg_pos(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
1099 size_t max = std::numeric_limits<size_t>::max())
1100{
1101 assert(s1.size() > 1);
1102 assert(s2.size() > 1);
1103
1104 HirschbergPos hpos = {};
1105 size_t left_size = s2.size() / 2;
1106 size_t right_size = s2.size() - left_size;
1107 hpos.s2_mid = left_size;
1108 size_t s1_len = s1.size();
1109 size_t best_score = std::numeric_limits<size_t>::max();
1110 size_t right_first_pos = 0;
1111 size_t right_last_pos = 0;
1112 // todo: we could avoid this allocation by counting up the right score twice
1113 // not sure whats faster though
1114 std::vector<size_t> right_scores;
1115 {
1116 auto right_row = levenshtein_row(s1.reversed(), s2.reversed(), max, right_size - 1);
1117 if (right_row.dist > max) return find_hirschberg_pos(s1, s2, max * 2);
1118
1119 right_first_pos = right_row.first_block * 64;
1120 right_last_pos = std::min(s1_len, right_row.last_block * 64 + 64);
1121
1122 right_scores.resize(right_last_pos - right_first_pos + 1, 0);
1123 assume(right_scores.size() != 0);
1124 right_scores[0] = right_row.prev_score;
1125
1126 for (size_t i = right_first_pos; i < right_last_pos; ++i) {
1127 size_t col_pos = i % 64;
1128 size_t col_word = i / 64;
1129 uint64_t col_mask = UINT64_C(1) << col_pos;
1130
1131 right_scores[i - right_first_pos + 1] = right_scores[i - right_first_pos];
1132 right_scores[i - right_first_pos + 1] -= bool(right_row.vecs[col_word].VN & col_mask);
1133 right_scores[i - right_first_pos + 1] += bool(right_row.vecs[col_word].VP & col_mask);
1134 }
1135 }
1136
1137 auto left_row = levenshtein_row(s1, s2, max, left_size - 1);
1138 if (left_row.dist > max) return find_hirschberg_pos(s1, s2, max * 2);
1139
1140 auto left_first_pos = left_row.first_block * 64;
1141 auto left_last_pos = std::min(s1_len, left_row.last_block * 64 + 64);
1142
1143 size_t left_score = left_row.prev_score;
1144 // take boundary into account
1145 if (s1_len >= left_first_pos + right_first_pos) {
1146 size_t right_index = s1_len - left_first_pos - right_first_pos;
1147 if (right_index < right_scores.size()) {
1148 best_score = right_scores[right_index] + left_score;
1149 hpos.left_score = left_score;
1150 hpos.right_score = right_scores[right_index];
1151 hpos.s1_mid = left_first_pos;
1152 }
1153 }
1154
1155 for (size_t i = left_first_pos; i < left_last_pos; ++i) {
1156 size_t col_pos = i % 64;
1157 size_t col_word = i / 64;
1158 uint64_t col_mask = UINT64_C(1) << col_pos;
1159
1160 left_score -= bool(left_row.vecs[col_word].VN & col_mask);
1161 left_score += bool(left_row.vecs[col_word].VP & col_mask);
1162
1163 if (s1_len < i + 1 + right_first_pos) continue;
1164
1165 size_t right_index = s1_len - i - 1 - right_first_pos;
1166 if (right_index >= right_scores.size()) continue;
1167
1168 if (right_scores[right_index] + left_score < best_score) {
1169 best_score = right_scores[right_index] + left_score;
1170 hpos.left_score = left_score;
1171 hpos.right_score = right_scores[right_index];
1172 hpos.s1_mid = i + 1;
1173 }
1174 }
1175
1176 if (hpos.left_score + hpos.right_score > max)
1177 return find_hirschberg_pos(s1, s2, max * 2);
1178 else {
1179 assert(levenshtein_distance(s1, s2) == hpos.left_score + hpos.right_score);
1180 return hpos;
1181 }
1182}
1183
1184template <typename InputIt1, typename InputIt2>
1185void levenshtein_align_hirschberg(Editops& editops, Range<InputIt1> s1, Range<InputIt2> s2,
1186 size_t src_pos = 0, size_t dest_pos = 0, size_t editop_pos = 0,
1187 size_t max = std::numeric_limits<size_t>::max())
1188{
1189 /* prefix and suffix are no-ops, which do not need to be added to the editops */
1190 StringAffix affix = remove_common_affix(s1, s2);
1191 src_pos += affix.prefix_len;
1192 dest_pos += affix.prefix_len;
1193
1194 max = std::min(max, std::max(s1.size(), s2.size()));
1195 size_t full_band = std::min(s1.size(), 2 * max + 1);
1196
1197 size_t matrix_size = 2 * full_band * s2.size() / 8;
1198 if (matrix_size < 1024 * 1024 || s1.size() < 65 || s2.size() < 10) {
1199 levenshtein_align(editops, s1, s2, max, src_pos, dest_pos, editop_pos);
1200 }
1201 /* Hirschbergs algorithm */
1202 else {
1203 auto hpos = find_hirschberg_pos(s1, s2, max);
1204
1205 if (editops.size() == 0) editops.resize(hpos.left_score + hpos.right_score);
1206
1207 levenshtein_align_hirschberg(editops, s1.subseq(0, hpos.s1_mid), s2.subseq(0, hpos.s2_mid), src_pos,
1208 dest_pos, editop_pos, hpos.left_score);
1209 levenshtein_align_hirschberg(editops, s1.subseq(hpos.s1_mid), s2.subseq(hpos.s2_mid),
1210 src_pos + hpos.s1_mid, dest_pos + hpos.s2_mid,
1211 editop_pos + hpos.left_score, hpos.right_score);
1212 }
1213}
1214
1215class Levenshtein : public DistanceBase<Levenshtein, size_t, 0, std::numeric_limits<int64_t>::max(),
1216 LevenshteinWeightTable> {
1217 friend DistanceBase<Levenshtein, size_t, 0, std::numeric_limits<int64_t>::max(), LevenshteinWeightTable>;
1218 friend NormalizedMetricBase<Levenshtein, LevenshteinWeightTable>;
1219
1220 template <typename InputIt1, typename InputIt2>
1221 static size_t maximum(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
1222 LevenshteinWeightTable weights)
1223 {
1224 return levenshtein_maximum(s1.size(), s2.size(), weights);
1225 }
1226
1227 template <typename InputIt1, typename InputIt2>
1228 static size_t _distance(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
1229 LevenshteinWeightTable weights, size_t score_cutoff, size_t score_hint)
1230 {
1231 return levenshtein_distance(s1, s2, weights, score_cutoff, score_hint);
1232 }
1233};
1234
1235template <typename InputIt1, typename InputIt2>
1236Editops levenshtein_editops(const Range<InputIt1>& s1, const Range<InputIt2>& s2, size_t score_hint)
1237{
1238 Editops editops;
1239 if (score_hint < 31) score_hint = 31;
1240
1241 size_t score_cutoff = std::max(s1.size(), s2.size());
1242 /* score_hint currently leads to calculating the levenshtein distance twice
1243 * 1) to find the real distance
1244 * 2) to find the alignment
1245 * this is only worth it when at least 50% of the runtime could be saved
1246 * todo: maybe there is a way to join these two calculations in the future
1247 * so it is worth it in more cases
1248 */
1249 if (std::numeric_limits<size_t>::max() / 2 > score_hint && 2 * score_hint < score_cutoff)
1250 score_cutoff = Levenshtein::distance(s1, s2, {1, 1, 1}, score_cutoff, score_hint);
1251
1252 levenshtein_align_hirschberg(editops, s1, s2, 0, 0, 0, score_cutoff);
1253
1254 editops.set_src_len(s1.size());
1255 editops.set_dest_len(s2.size());
1256 return editops;
1257}
1258
1259} // namespace detail
1260} // namespace rapidfuzz
Editops levenshtein_editops(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, size_t score_hint=std::numeric_limits< size_t >::max())
Return list of EditOp describing how to turn s1 into s2.
Definition Levenshtein.hpp:288
size_t levenshtein_distance(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, LevenshteinWeightTable weights={1, 1, 1}, size_t score_cutoff=std::numeric_limits< size_t >::max(), size_t score_hint=std::numeric_limits< size_t >::max())
Calculates the minimum number of insertions, deletions, and substitutions required to change one sequ...
Definition Levenshtein.hpp:146