5#include <rapidfuzz/details/Matrix.hpp>
6#include <rapidfuzz/details/PatternMatchVector.hpp>
7#include <rapidfuzz/details/common.hpp>
8#include <rapidfuzz/details/distance.hpp>
9#include <rapidfuzz/details/intrinsics.hpp>
10#include <rapidfuzz/details/simd.hpp>
14#include <rapidfuzz/details/types.hpp>
19template <
bool RecordMatrix>
23struct LCSseqResult<true> {
24 ShiftedBitMatrix<uint64_t> S;
30struct LCSseqResult<false> {
34template <
bool RecordMatrix>
35LCSseqResult<true>& getMatrixRef(LCSseqResult<RecordMatrix>& res)
37#if RAPIDFUZZ_IF_CONSTEXPR_AVAILABLE
44 return reinterpret_cast<LCSseqResult<true>&
>(res);
67static constexpr std::array<std::array<uint8_t, 6>, 14> lcs_seq_mbleven2018_matrix = {{
82 {0x96, 0x66, 0x5A, 0x99, 0x69, 0xA5},
84 {0x65, 0x56, 0x95, 0x59},
89template <
typename InputIt1,
typename InputIt2>
90size_t lcs_seq_mbleven2018(
const Range<InputIt1>& s1,
const Range<InputIt2>& s2,
size_t score_cutoff)
92 auto len1 = s1.size();
93 auto len2 = s2.size();
97 if (len1 < len2)
return lcs_seq_mbleven2018(s2, s1, score_cutoff);
99 auto len_diff = len1 - len2;
100 size_t max_misses = len1 + len2 - 2 * score_cutoff;
101 size_t ops_index = (max_misses + max_misses * max_misses) / 2 + len_diff - 1;
102 auto& possible_ops = lcs_seq_mbleven2018_matrix[ops_index];
105 for (uint8_t ops : possible_ops) {
106 auto iter_s1 = s1.begin();
107 auto iter_s2 = s2.begin();
112 while (iter_s1 != s1.end() && iter_s2 != s2.end()) {
113 if (*iter_s1 != *iter_s2) {
119#if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && __GNUC__ < 10
120# pragma GCC diagnostic push
121# pragma GCC diagnostic ignored "-Wconversion"
124#if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && __GNUC__ < 10
125# pragma GCC diagnostic pop
135 max_len = std::max(max_len, cur_len);
138 return (max_len >= score_cutoff) ? max_len : 0;
142template <
typename VecType,
typename InputIt,
int _lto_hack = RAPIDFUZZ_LTO_HACK>
143void lcs_simd(Range<size_t*> scores,
const BlockPatternMatchVector& block,
const Range<InputIt>& s2,
144 size_t score_cutoff)
noexcept
146# ifdef RAPIDFUZZ_AVX2
147 using namespace simd_avx2;
149 using namespace simd_sse2;
151 auto score_iter = scores.begin();
152 static constexpr size_t alignment = native_simd<VecType>::alignment;
153 static constexpr size_t vecs = native_simd<uint64_t>::size;
154 assert(block.size() % vecs == 0);
156 static constexpr size_t interleaveCount = 3;
159 for (; cur_vec + interleaveCount * vecs <= block.size(); cur_vec += interleaveCount * vecs) {
160 std::array<native_simd<VecType>, interleaveCount> S;
161 unroll<size_t, interleaveCount>([&](
size_t j) { S[j] =
static_cast<VecType
>(-1); });
163 for (
const auto& ch : s2) {
164 unroll<size_t, interleaveCount>([&](
size_t j) {
165 alignas(32) std::array<uint64_t, vecs> stored;
166 unroll<size_t, vecs>([&](
size_t i) { stored[i] = block.get(cur_vec + j * vecs + i, ch); });
168 native_simd<VecType> Matches(stored.data());
169 native_simd<VecType> u = S[j] & Matches;
170 S[j] = (S[j] + u) | (S[j] - u);
174 unroll<size_t, interleaveCount>([&](
size_t j) {
175 auto counts = popcount(~S[j]);
176 unroll<size_t, counts.size()>([&](
size_t i) {
177 *score_iter = (counts[i] >= score_cutoff) ?
static_cast<size_t>(counts[i]) : 0;
183 for (; cur_vec < block.size(); cur_vec += vecs) {
184 native_simd<VecType> S =
static_cast<VecType
>(-1);
186 for (
const auto& ch : s2) {
187 alignas(alignment) std::array<uint64_t, vecs> stored;
188 unroll<size_t, vecs>([&](
size_t i) { stored[i] = block.get(cur_vec + i, ch); });
190 native_simd<VecType> Matches(stored.data());
191 native_simd<VecType> u = S & Matches;
192 S = (S + u) | (S - u);
195 auto counts = popcount(~S);
196 unroll<size_t, counts.size()>([&](
size_t i) {
197 *score_iter = (counts[i] >= score_cutoff) ?
static_cast<size_t>(counts[i]) : 0;
205template <
size_t N,
bool RecordMatrix,
typename PMV,
typename InputIt1,
typename InputIt2>
206auto lcs_unroll(
const PMV& block,
const Range<InputIt1>&,
const Range<InputIt2>& s2,
207 size_t score_cutoff = 0) -> LCSseqResult<RecordMatrix>
210 unroll<size_t, N>([&](
size_t i) { S[i] = ~UINT64_C(0); });
212 LCSseqResult<RecordMatrix> res;
213 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
214 auto& res_ = getMatrixRef(res);
215 res_.S = ShiftedBitMatrix<uint64_t>(s2.size(), N, ~UINT64_C(0));
218 auto iter_s2 = s2.begin();
219 for (
size_t i = 0; i < s2.size(); ++i) {
222 static constexpr size_t unroll_factor = 3;
223 for (
unsigned int j = 0; j < N / unroll_factor; ++j) {
224 unroll<size_t, unroll_factor>([&](
size_t word_) {
225 size_t word = word_ + j * unroll_factor;
226 uint64_t Matches = block.get(word, *iter_s2);
227 uint64_t u = S[word] & Matches;
228 uint64_t x = addc64(S[word], u, carry, &carry);
229 S[word] = x | (S[word] - u);
231 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
232 auto& res_ = getMatrixRef(res);
233 res_.S[i][word] = S[word];
238 unroll<size_t, N % unroll_factor>([&](
size_t word_) {
239 size_t word = word_ + N / unroll_factor * unroll_factor;
240 uint64_t Matches = block.get(word, *iter_s2);
241 uint64_t u = S[word] & Matches;
242 uint64_t x = addc64(S[word], u, carry, &carry);
243 S[word] = x | (S[word] - u);
245 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
246 auto& res_ = getMatrixRef(res);
247 res_.S[i][word] = S[word];
255 unroll<size_t, N>([&](
size_t i) { res.sim += popcount(~S[i]); });
257 if (res.sim < score_cutoff) res.sim = 0;
268template <
bool RecordMatrix,
typename PMV,
typename InputIt1,
typename InputIt2>
269auto lcs_blockwise(
const PMV& PM,
const Range<InputIt1>& s1,
const Range<InputIt2>& s2,
270 size_t score_cutoff = 0) -> LCSseqResult<RecordMatrix>
272 assert(score_cutoff <= s1.size());
273 assert(score_cutoff <= s2.size());
275 size_t word_size =
sizeof(uint64_t) * 8;
276 size_t words = PM.size();
277 std::vector<uint64_t> S(words, ~UINT64_C(0));
279 size_t band_width_left = s1.size() - score_cutoff;
280 size_t band_width_right = s2.size() - score_cutoff;
282 LCSseqResult<RecordMatrix> res;
283 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
284 auto& res_ = getMatrixRef(res);
285 size_t full_band = band_width_left + 1 + band_width_right;
286 size_t full_band_words = std::min(words, full_band / word_size + 2);
287 res_.S = ShiftedBitMatrix<uint64_t>(s2.size(), full_band_words, ~UINT64_C(0));
291 size_t first_block = 0;
292 size_t last_block = std::min(words, ceil_div(band_width_left + 1, word_size));
294 auto iter_s2 = s2.begin();
295 for (
size_t row = 0; row < s2.size(); ++row) {
298 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
299 auto& res_ = getMatrixRef(res);
300 res_.S.set_offset(row,
static_cast<ptrdiff_t
>(first_block * word_size));
303 for (
size_t word = first_block; word < last_block; ++word) {
304 const uint64_t Matches = PM.get(word, *iter_s2);
305 uint64_t Stemp = S[word];
307 uint64_t u = Stemp & Matches;
309 uint64_t x = addc64(Stemp, u, carry, &carry);
310 S[word] = x | (Stemp - u);
312 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
313 auto& res_ = getMatrixRef(res);
314 res_.S[row][word - first_block] = S[word];
318 if (row > band_width_right) first_block = (row - band_width_right) / word_size;
320 if (row + 1 + band_width_left <= s1.size())
321 last_block = ceil_div(row + 1 + band_width_left, word_size);
327 for (uint64_t Stemp : S)
328 res.sim += popcount(~Stemp);
330 if (res.sim < score_cutoff) res.sim = 0;
335template <
typename PMV,
typename InputIt1,
typename InputIt2>
336size_t longest_common_subsequence(
const PMV& PM,
const Range<InputIt1>& s1,
const Range<InputIt2>& s2,
339 assert(score_cutoff <= s1.size());
340 assert(score_cutoff <= s2.size());
342 size_t word_size =
sizeof(uint64_t) * 8;
343 size_t words = PM.size();
344 size_t band_width_left = s1.size() - score_cutoff;
345 size_t band_width_right = s2.size() - score_cutoff;
346 size_t full_band = band_width_left + 1 + band_width_right;
347 size_t full_band_words = std::min(words, full_band / word_size + 2);
349 if (full_band_words < words)
return lcs_blockwise<false>(PM, s1, s2, score_cutoff).sim;
351 auto nr = ceil_div(s1.size(), 64);
354 case 1:
return lcs_unroll<1, false>(PM, s1, s2, score_cutoff).sim;
355 case 2:
return lcs_unroll<2, false>(PM, s1, s2, score_cutoff).sim;
356 case 3:
return lcs_unroll<3, false>(PM, s1, s2, score_cutoff).sim;
357 case 4:
return lcs_unroll<4, false>(PM, s1, s2, score_cutoff).sim;
358 case 5:
return lcs_unroll<5, false>(PM, s1, s2, score_cutoff).sim;
359 case 6:
return lcs_unroll<6, false>(PM, s1, s2, score_cutoff).sim;
360 case 7:
return lcs_unroll<7, false>(PM, s1, s2, score_cutoff).sim;
361 case 8:
return lcs_unroll<8, false>(PM, s1, s2, score_cutoff).sim;
362 default:
return lcs_blockwise<false>(PM, s1, s2, score_cutoff).sim;
366template <
typename InputIt1,
typename InputIt2>
367size_t longest_common_subsequence(
const Range<InputIt1>& s1,
const Range<InputIt2>& s2,
size_t score_cutoff)
369 if (s1.empty())
return 0;
370 if (s1.size() <= 64)
return longest_common_subsequence(PatternMatchVector(s1), s1, s2, score_cutoff);
372 return longest_common_subsequence(BlockPatternMatchVector(s1), s1, s2, score_cutoff);
375template <
typename InputIt1,
typename InputIt2>
376size_t lcs_seq_similarity(
const BlockPatternMatchVector& block, Range<InputIt1> s1, Range<InputIt2> s2,
379 auto len1 = s1.size();
380 auto len2 = s2.size();
382 if (score_cutoff > len1 || score_cutoff > len2)
return 0;
384 size_t max_misses = len1 + len2 - 2 * score_cutoff;
387 if (max_misses == 0 || (max_misses == 1 && len1 == len2))
return s1 == s2 ? len1 : 0;
389 if (max_misses < abs_diff(len1, len2))
return 0;
392 if (max_misses >= 5)
return longest_common_subsequence(block, s1, s2, score_cutoff);
395 StringAffix affix = remove_common_affix(s1, s2);
396 size_t lcs_sim = affix.prefix_len + affix.suffix_len;
397 if (!s1.empty() && !s2.empty()) {
398 size_t adjusted_cutoff = score_cutoff >= lcs_sim ? score_cutoff - lcs_sim : 0;
399 lcs_sim += lcs_seq_mbleven2018(s1, s2, adjusted_cutoff);
402 return (lcs_sim >= score_cutoff) ? lcs_sim : 0;
405template <
typename InputIt1,
typename InputIt2>
406size_t lcs_seq_similarity(Range<InputIt1> s1, Range<InputIt2> s2,
size_t score_cutoff)
408 auto len1 = s1.size();
409 auto len2 = s2.size();
412 if (len1 < len2)
return lcs_seq_similarity(s2, s1, score_cutoff);
414 if (score_cutoff > len1 || score_cutoff > len2)
return 0;
416 size_t max_misses = len1 + len2 - 2 * score_cutoff;
419 if (max_misses == 0 || (max_misses == 1 && len1 == len2))
return s1 == s2 ? len1 : 0;
421 if (max_misses < abs_diff(len1, len2))
return 0;
424 StringAffix affix = remove_common_affix(s1, s2);
425 size_t lcs_sim = affix.prefix_len + affix.suffix_len;
426 if (s1.size() && s2.size()) {
427 size_t adjusted_cutoff = score_cutoff >= lcs_sim ? score_cutoff - lcs_sim : 0;
429 lcs_sim += lcs_seq_mbleven2018(s1, s2, adjusted_cutoff);
431 lcs_sim += longest_common_subsequence(s1, s2, adjusted_cutoff);
434 return (lcs_sim >= score_cutoff) ? lcs_sim : 0;
440template <
typename InputIt1,
typename InputIt2>
441Editops recover_alignment(
const Range<InputIt1>& s1,
const Range<InputIt2>& s2,
442 const LCSseqResult<true>& matrix, StringAffix affix)
444 size_t len1 = s1.size();
445 size_t len2 = s2.size();
446 size_t dist = len1 + len2 - 2 * matrix.sim;
447 Editops editops(dist);
448 editops.set_src_len(len1 + affix.prefix_len + affix.suffix_len);
449 editops.set_dest_len(len2 + affix.prefix_len + affix.suffix_len);
451 if (dist == 0)
return editops;
454 size_t band_width_right = s2.size() - matrix.sim;
462 if (matrix.S.test_bit(row - 1, col - 1)) {
464 assert(
static_cast<ptrdiff_t
>(col) >=
465 static_cast<ptrdiff_t
>(row) -
static_cast<ptrdiff_t
>(band_width_right));
468 editops[dist].type = EditType::Delete;
469 editops[dist].src_pos = col + affix.prefix_len;
470 editops[dist].dest_pos = row + affix.prefix_len;
476 if (row && !(matrix.S.test_bit(row - 1, col - 1))) {
479 editops[dist].type = EditType::Insert;
480 editops[dist].src_pos = col + affix.prefix_len;
481 editops[dist].dest_pos = row + affix.prefix_len;
486 assert(s1[col] == s2[row]);
494 editops[dist].type = EditType::Delete;
495 editops[dist].src_pos = col + affix.prefix_len;
496 editops[dist].dest_pos = row + affix.prefix_len;
502 editops[dist].type = EditType::Insert;
503 editops[dist].src_pos = col + affix.prefix_len;
504 editops[dist].dest_pos = row + affix.prefix_len;
510template <
typename InputIt1,
typename InputIt2>
511LCSseqResult<true> lcs_matrix(
const Range<InputIt1>& s1,
const Range<InputIt2>& s2)
513 size_t nr = ceil_div(s1.size(), 64);
517 LCSseqResult<true> res;
521 case 1:
return lcs_unroll<1, true>(PatternMatchVector(s1), s1, s2);
522 case 2:
return lcs_unroll<2, true>(BlockPatternMatchVector(s1), s1, s2);
523 case 3:
return lcs_unroll<3, true>(BlockPatternMatchVector(s1), s1, s2);
524 case 4:
return lcs_unroll<4, true>(BlockPatternMatchVector(s1), s1, s2);
525 case 5:
return lcs_unroll<5, true>(BlockPatternMatchVector(s1), s1, s2);
526 case 6:
return lcs_unroll<6, true>(BlockPatternMatchVector(s1), s1, s2);
527 case 7:
return lcs_unroll<7, true>(BlockPatternMatchVector(s1), s1, s2);
528 case 8:
return lcs_unroll<8, true>(BlockPatternMatchVector(s1), s1, s2);
529 default:
return lcs_blockwise<true>(BlockPatternMatchVector(s1), s1, s2);
533template <
typename InputIt1,
typename InputIt2>
534Editops lcs_seq_editops(Range<InputIt1> s1, Range<InputIt2> s2)
537 StringAffix affix = remove_common_affix(s1, s2);
539 return recover_alignment(s1, s2, lcs_matrix(s1, s2), affix);
542class LCSseq :
public SimilarityBase<LCSseq, size_t, 0, std::numeric_limits<int64_t>::max()> {
543 friend SimilarityBase<LCSseq, size_t, 0, std::numeric_limits<int64_t>::max()>;
544 friend NormalizedMetricBase<LCSseq>;
546 template <
typename InputIt1,
typename InputIt2>
547 static size_t maximum(
const Range<InputIt1>& s1,
const Range<InputIt2>& s2)
549 return std::max(s1.size(), s2.size());
552 template <
typename InputIt1,
typename InputIt2>
553 static size_t _similarity(
const Range<InputIt1>& s1,
const Range<InputIt2>& s2,
size_t score_cutoff,
556 return lcs_seq_similarity(s1, s2, score_cutoff);