Как я могу адаптировать алгоритм Levenshtein Distance для ограничения совпадений одним словом?
Я использую алгоритм Distance Levenshtein в С++ для сравнения двух строк, чтобы измерить, насколько они близки друг к другу. Однако простой алгоритм Levenshtein Distance не отличает границы слов как ограниченные пробелами. Это приводит к вычислениям меньшего расстояния, чем я хочу. Я сравниваю названия, чтобы увидеть, насколько они близки друг к другу, и я хочу, чтобы алгоритм не учитывал символы как совпадающие, если они исходили из нескольких слов.
Например, если я сравниваю эти две строки, я получаю следующий результат: +
, обозначающий соответствие, и -
, обозначающий несоответствие:
Al Chertoff Et
Al Church Department of finance Et
+++++------+--++-----++-+------+++
Al Ch e rt of f Et
Я получаю дистанцию 20 с словом "Chertoff"
, сопоставляющим четыре слова "Church Department of finance"
, тогда как я действительно хочу, чтобы их рассматривали дальше друг от друга, не позволяя символам соответствовать более чем одному слову и получить расстояние 25, когда слово "Chertoff"
наиболее соответствует одному слову "Department"
, с тремя символами, соответствующими:
Al Chertoff Et
Al Church Department of finance Et
+++--------+--++---------------+++
Al e rt Et
Ch off
Как я могу адаптировать Levenshtein Distance для выполнения этого или есть другой алгоритм расстояния, который лучше подходит для этого? Возможно, используя расстояние Левенштейна на каждом слове в отдельном слове, и выбирая слово с наименьшим расстоянием? Однако, что, если совпадение одного слова с глубиной в строке привело к тому, что последующие слова соответствовали плохо, потому что их совпадения были лучше всего в строке? Может ли это быть сделано с расстоянием Левенштейна, подходящим для уровня слов?
Например, кратчайшее расстояние по этой идее для следующего более сложного примера - 20:
Al Chertoff Deport Et
Al Church Department of finance Et
+++++----++++-++---------------+++
Al Ch Dep rt Et
ertoff o
Вместо максимизации "Chertoff"
совпадения и получения более длинного расстояния 24:
Al Chertoff Deport Et
Al Church Department of finance Et
+++--------+--++-----+---------+++
Al e rt o Et
Ch off
Dep rt
Моя текущая реализация расстояния Левенштейна такова:
size_t
levenshtein_distance(const std::string& a_compare1,
const std::string& a_compare2) {
const size_t length1 = a_compare1.size();
const size_t length2 = a_compare2.size();
std::vector<size_t> curr_col(length2 + 1);
std::vector<size_t> prev_col(length2 + 1);
// Prime the previous column for use in the following loop:
for (size_t idx2 = 0; idx2 < length2 + 1; ++idx2) {
prev_col[idx2] = idx2;
}
for (size_t idx1 = 0; idx1 < length1; ++idx1) {
curr_col[0] = idx1 + 1;
for (size_t idx2 = 0; idx2 < length2; ++idx2) {
const size_t compare = a_compare1[idx1] == a_compare2[idx2] ? 0 : 1;
curr_col[idx2 + 1] = std::min(std::min(curr_col[idx2] + 1,
prev_col[idx2 + 1] + 1),
prev_col[idx2] + compare);
}
curr_col.swap(prev_col);
}
return prev_col[length2];
}
Ответы
Ответ 1
Я могу приблизиться к тому, что вы хотите, сделав levenshtein_distance
общий алгоритм на контейнере последовательности и включив функцию стоимости, которая вычисляет расстояние между двумя элементами:
template<typename T, typename C>
size_t
seq_distance(const T& seq1, const T& seq2, const C& cost,
const typename T::value_type& empty = typename T::value_type()) {
const size_t size1 = seq1.size();
const size_t size2 = seq2.size();
std::vector<size_t> curr_col(size2 + 1);
std::vector<size_t> prev_col(size2 + 1);
// Prime the previous column for use in the following loop:
prev_col[0] = 0;
for (size_t idx2 = 0; idx2 < size2; ++idx2) {
prev_col[idx2 + 1] = prev_col[idx2] + cost(empty, seq2[idx2]);
}
for (size_t idx1 = 0; idx1 < size1; ++idx1) {
curr_col[0] = curr_col[0] + cost(seq1[idx1], empty);
for (size_t idx2 = 0; idx2 < size2; ++idx2) {
curr_col[idx2 + 1] = std::min(std::min(
curr_col[idx2] + cost(empty, seq2[idx2]),
prev_col[idx2 + 1] + cost(seq1[idx1], empty)),
prev_col[idx2] + cost(seq1[idx1], seq2[idx2]));
}
curr_col.swap(prev_col);
curr_col[0] = prev_col[0];
}
return prev_col[size2];
}
Учитывая приведенное выше seq_distance
, расстояние редактирования между двумя предложениями, такое, что редактирование не может быть выполнено между границами слов, может быть определено следующим образом:
size_t
letter_distance(char letter1, char letter2) {
return letter1 != letter2 ? 1 : 0;
}
size_t
word_distance(const std::string& word1, const std::string& word2) {
return seq_distance(word1, word2, &letter_distance);
}
size_t
sentence_distance(const std::string& sentence1, const std::string& sentence2) {
std::vector<std::string> words1;
std::vector<std::string> words2;
std::istringstream iss1(sentence1);
std::istringstream iss2(sentence2);
std::copy(std::istream_iterator<std::string>(iss1),
std::istream_iterator<std::string>(),
std::back_inserter(words1));
std::copy(std::istream_iterator<std::string>(iss2),
std::istream_iterator<std::string>(),
std::back_inserter(words2));
return seq_distance(words1, words2, &word_distance);
}
Здесь код работает ideone. Я проверил несколько случаев, и я уверен, что все в порядке, но вы должны попробовать больше, чтобы убедиться, что результаты являются разумными.
Обратите внимание, что это не совсем то, о чем вы просили, так как он игнорирует все пробелы в измерении расстояния редактирования: я думаю, что это не должно быть слишком сложно изменить, чтобы этого не сделать, но я не думал, что это через полностью. В любом случае, это может быть так же хорошо (или даже лучше), в зависимости от ваших потребностей, поэтому я позволю вам решить, хотите ли вы попытаться настроить его.
Просто небольшая заметка, ваш исходный код был немного ошибочным в том, что следующие две строки:
curr_col.reserve(length2 + 1);
prev_col.reserve(length2 + 1);
резервировать емкость в векторах, но фактически не изменять их размеры, поэтому доступ к массиву после этого был undefined. Фактически вы должны resize
вектор, если вы собираетесь обращаться к элементам в диапазоне: reserve
обычно используется для ситуаций, когда вы собираетесь push_back
определенное количество элементов поочередно (что увеличивает размер как вы идете, не все сразу), и вы хотите избежать затрат на множественные внутренние перераспределения (поскольку внутренняя емкость увеличивается только на определенный коэффициент каждый раз, когда мощность превышена).
EDIT:
В этой версии учитываются пробелы между словами как часть расстояния редактирования, но результаты по-прежнему не совсем совпадают с вашими примерами из-за требования добавить несколько пробелов в некоторых случаях.
Ответ 2
Границы слов будут пересекаться, если отдельные слова не имеют одинаковой длины. Если вы хотите, чтобы индексы сравнивались в соответствующих словах, вам нужно будет сделать слова одинаковой длины. Например, здесь Javascript (да, я знаю, что вы спросили или С++, но это для иллюстрации - код, взятый из Википедии). Процедура вычисления расстояния:
var memo = {};
function d(str1, i, len1, str2, j, len2){
var key = [i,len1,j,len2].join(',');
if(memo[key] != undefined) return memo[key];
if(len1 == 0) return len2;
if(len2 == 0) return len1;
var cost = 0;
if(str1[i] != str2[j]) cost = 1;
var dist = Math.min(
d(str1, i+1,len1-1, str2,j,len2)+1,
d(str1,i,len1,str2,j+1,len2-1)+1,
d(str1,i+1,len1-1,str2,j+1,len2-1)+cost);
memo[key] = dist;
return dist;
}
var str1 = "Al Chertoff Deport$$$$ $$ $$$$$$$ Et";
var str2 = "Al Church$$ Department of finance Et";
console.log(d(str1, 0, str1.length, str2, 0, str2.length));
Обратите внимание, как я изменил две строки ввода, чтобы они соответствовали отдельному уровню слов. Запустив это, я получил расстояние 19. Аналогично, если я изменил строки на:
var str1 = "Al Chertoff $$$$$$$$$$ $$ $$$$$$$ Et";
var str2 = "Al Church$$ Department of finance Et";
Я получаю расстояние 24.