Модификация алгоритма расстояния Левенштейна, чтобы не вычислять все расстояния - PullRequest
7 голосов
/ 05 октября 2010

Я работаю над реализацией нечеткого поиска, и в рамках этой реализации мы используем Apache StringUtils.getLevenshteinDistance. На данный момент мы рассчитываем на максимальное максимальное среднее время отклика для нашего нечеткого поиска. После различных улучшений и с некоторым профилированием место, где тратится больше всего времени, - это вычисление расстояния Левенштейна. Это занимает примерно 80-90% от общего времени поиска строк из трех букв и более.

Теперь я знаю, что существуют некоторые ограничения в отношении того, что можно сделать здесь, но я читал в предыдущих вопросах SO и в ссылке на Википедию для LD, что, если кто-то желает ограничить порог установленным максимальным расстоянием, это может помогите обуздать время, затрачиваемое на алгоритм, но я не уверен, как именно это сделать.

Если нас интересует только расстояние, если оно меньше, чем порог к, то достаточно вычислить диагональную полосу ширины 2к + 1 в матрице. Таким образом, алгоритм может быть запущен за время O (kl), где l - длина кратчайшего строка. [3]

Ниже вы увидите оригинальный код LH из StringUtils. После этого моя модификация. Я пытаюсь в основном вычислить расстояния заданной длины от диагонали i, j (поэтому в моем примере две диагонали выше и ниже диагонали i, j). Тем не менее, это не может быть правильным, как я это сделал. Например, на самой высокой диагонали всегда будет выбираться значение ячейки, расположенное непосредственно выше, которое будет равно 0. Если кто-нибудь может показать мне, как сделать этот функционал, как я описал, или дать несколько общих советов о том, как это сделать. , будет принята с благодарностью.

public static int getLevenshteinDistance(String s, String t) {
        if (s == null || t == null) {
            throw new IllegalArgumentException("Strings must not be null");
        }

        int n = s.length(); // length of s
        int m = t.length(); // length of t

        if (n == 0) {
            return m;
        } else if (m == 0) {
            return n;
        }

        if (n > m) {
            // swap the input strings to consume less memory
            String tmp = s;
            s = t;
            t = tmp;
            n = m;
            m = t.length();
        }

        int p[] = new int[n+1]; //'previous' cost array, horizontally
        int d[] = new int[n+1]; // cost array, horizontally
        int _d[]; //placeholder to assist in swapping p and d

        // indexes into strings s and t
        int i; // iterates through s
        int j; // iterates through t

        char t_j; // jth character of t

        int cost; // cost

        for (i = 0; i<=n; i++) {
            p[i] = i;
        }

        for (j = 1; j<=m; j++) {
            t_j = t.charAt(j-1);
            d[0] = j;

            for (i=1; i<=n; i++) {
                cost = s.charAt(i-1)==t_j ? 0 : 1;
                // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
                d[i] = Math.min(Math.min(d[i-1]+1, p[i]+1),  p[i-1]+cost);
            }

            // copy current distance counts to 'previous row' distance counts
            _d = p;
            p = d;
            d = _d;
        }

        // our last action in the above loop was to switch d and p, so p now 
        // actually has the most recent cost counts
        return p[n];
    }

Мои модификации (только для циклов for):

  for (j = 1; j<=m; j++) {
        t_j = t.charAt(j-1);
        d[0] = j;

        int k = Math.max(j-2, 1);
        for (i = k; i <= Math.min(j+2, n); i++) {
            cost = s.charAt(i-1)==t_j ? 0 : 1;
            // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
            d[i] = Math.min(Math.min(d[i-1]+1, p[i]+1),  p[i-1]+cost);
        }

        // copy current distance counts to 'previous row' distance counts
        _d = p;
        p = d;
        d = _d;
    }

Ответы [ 6 ]

5 голосов
/ 05 октября 2010

Я писал об автоматах Левенштейна, которые являются одним из способов выполнить такую ​​проверку за O (n) раз, здесь .Образцы исходного кода написаны на Python, но пояснения должны быть полезны, и в ссылочных статьях содержится более подробная информация.

3 голосов
/ 28 февраля 2011

Проблема с реализацией окна связана со значением слева от первой записи и над последней записью в каждой строке.

Один из способов состоит в том, чтобы начинать значения, которые вы изначально вводите, с 1 вместо 0, а затем просто игнорировать любые 0, которые вы встречаете.Вам придется вычесть 1 из вашего окончательного ответа.

Другой способ - заполнить записи слева от первого и выше последнего высокими значениями, чтобы минимальная проверка никогда их не выбрала.Именно так я и выбрал, когда мне пришлось реализовать это на днях:

public static int levenshtein(String s, String t, int threshold) {
    int slen = s.length();
    int tlen = t.length();

    // swap so the smaller string is t; this reduces the memory usage
    // of our buffers
    if(tlen > slen) {
        String stmp = s;
        s = t;
        t = stmp;
        int itmp = slen;
        slen = tlen;
        tlen = itmp;
    }

    // p is the previous and d is the current distance array; dtmp is used in swaps
    int[] p = new int[tlen + 1];
    int[] d = new int[tlen + 1];
    int[] dtmp;

    // the values necessary for our threshold are written; the ones after
    // must be filled with large integers since the tailing member of the threshold 
    // window in the bottom array will run min across them
    int n = 0;
    for(; n < Math.min(p.length, threshold + 1); ++n)
        p[n] = n;
    Arrays.fill(p, n, p.length, Integer.MAX_VALUE);
    Arrays.fill(d, Integer.MAX_VALUE);

    // this is the core of the Levenshtein edit distance algorithm
    // instead of actually building the matrix, two arrays are swapped back and forth
    // the threshold limits the amount of entries that need to be computed if we're 
    // looking for a match within a set distance
    for(int row = 1; row < s.length()+1; ++row) {
        char schar = s.charAt(row-1);
        d[0] = row;

        // set up our threshold window
        int min = Math.max(1, row - threshold);
        int max = Math.min(d.length, row + threshold + 1);

        // since we're reusing arrays, we need to be sure to wipe the value left of the
        // starting index; we don't have to worry about the value above the ending index
        // as the arrays were initially filled with large integers and we progress to the right
        if(min > 1)
            d[min-1] = Integer.MAX_VALUE;

        for(int col = min; col < max; ++col) {
            if(schar == t.charAt(col-1))
                d[col] = p[col-1];
            else 
                // min of: diagonal, left, up
                d[col] = Math.min(p[col-1], Math.min(d[col-1], p[col])) + 1;
        }
        // swap our arrays
        dtmp = p;
        p = d;
        d = dtmp;
    }

        if(p[tlen] == Integer.MAX_VALUE)
            return -1;
    return p[tlen];
}
1 голос
/ 06 декабря 2010

Согласно «Gusfield, Dan (1997). Алгоритмы на строках, деревьях и последовательностях: информатика и вычислительная биология» (стр. 264), вы должны игнорировать нули.

1 голос
/ 06 октября 2010

Я использовал исходный код и помещал его непосредственно перед концом цикла j for:

    if (p[n] > s.length() + 5)
        break;

+5 произвольно, но для наших целей, если расстояния - это длина запроса плюс пять (или любое другое число, на которое мы рассчитываем), не имеет значения, что возвращается, потому что мы считаем, что совпадение просто слишком отличается.Это немного сокращает вещи.Тем не менее, я уверен, что это не та идея, о которой говорилось в вики, если кто-то понимает это лучше.

1 голос
/ 05 октября 2010

Здесь кто-то отвечает на очень похожий вопрос:

Cite:
Я делал это несколько раз. Я делаю это с помощью рекурсивного блуждания по дереву игрового дерева возможных изменений. Существует бюджет k изменений, который я использую, чтобы обрезать дерево. С этой подпрограммой сначала я запускаю ее с k = 0, затем k = 1, затем k = 2, пока я не получу удар, или я не хочу идти выше.

char* a = /* string 1 */;
char* b = /* string 2 */;
int na = strlen(a);
int nb = strlen(b);
bool walk(int ia, int ib, int k){
  /* if the budget is exhausted, prune the search */
  if (k < 0) return false;
  /* if at end of both strings we have a match */ 
  if (ia == na && ib == nb) return true;
  /* if the first characters match, continue walking with no reduction in budget */
  if (ia < na && ib < nb && a[ia] == b[ib] && walk(ia+1, ib+1, k)) return true;
  /* if the first characters don't match, assume there is a 1-character replacement */
  if (ia < na && ib < nb && a[ia] != b[ib] && walk(ia+1, ib+1, k-1)) return true;
  /* try assuming there is an extra character in a */
  if (ia < na && walk(ia+1, ib, k-1)) return true;
  /* try assuming there is an extra character in b */
  if (ib < nb && walk(ia, ib+1, k-1)) return true;
  /* if none of those worked, I give up */
  return false;
}  

только основная часть, больше кода в оригинале

0 голосов
/ 28 января 2016

Apache Commons Lang 3.4 имеет такую ​​реализацию:

<code>/**
 * <p>Find the Levenshtein distance between two Strings if it's less than or equal to a given
 * threshold.</p>
 *
 * <p>This is the number of changes needed to change one String into
 * another, where each change is a single character modification (deletion,
 * insertion or substitution).</p>
 *
 * <p>This implementation follows from Algorithms on Strings, Trees and Sequences by Dan Gusfield
 * and Chas Emerick's implementation of the Levenshtein distance algorithm from
 * <a href="http://www.merriampark.com/ld.htm">http://www.merriampark.com/ld.htm</a></p>
 *
 * <pre>
 * StringUtils.getLevenshteinDistance(null, *, *)             = IllegalArgumentException
 * StringUtils.getLevenshteinDistance(*, null, *)             = IllegalArgumentException
 * StringUtils.getLevenshteinDistance(*, *, -1)               = IllegalArgumentException
 * StringUtils.getLevenshteinDistance("","", 0)               = 0
 * StringUtils.getLevenshteinDistance("aaapppp", "", 8)       = 7
 * StringUtils.getLevenshteinDistance("aaapppp", "", 7)       = 7
 * StringUtils.getLevenshteinDistance("aaapppp", "", 6))      = -1
 * StringUtils.getLevenshteinDistance("elephant", "hippo", 7) = 7
 * StringUtils.getLevenshteinDistance("elephant", "hippo", 6) = -1
 * StringUtils.getLevenshteinDistance("hippo", "elephant", 7) = 7
 * StringUtils.getLevenshteinDistance("hippo", "elephant", 6) = -1
 * 
* * @param s первая строка, не должна быть нулевой * @param t вторая строка, не должна быть нулевой * @param threshold целевой порог,не должно быть отрицательным * @return результатом расстояния или {@code -1}, если расстояние будет больше порога * @throws IllegalArgumentException, если либо String input {@code null}, либо отрицательный порог * / public static int getLevenshteinDistance (CharSequences, CharSequence t, конечный порог int) {if (s == null || t == null) {throw new IllegalArgumentException ("Строки не должны быть нулевыми");} if (threshold <0) {throw new IllegalArgumentException («Порог не должен быть отрицательным»);} / * Эта реализация вычисляет расстояние, только если оно меньше или равно пороговому значению, и возвращает -1, если оно больше.Преимущество - производительность: неограниченное расстояние равно O (нм), но ограничение k позволяет сократить его до времени O (км), вычисляя только диагональную полосу шириной 2k + 1 таблицы затрат.Это также можно использовать для вычисления неограниченного расстояния Левенштейна, начиная порог с 1 и удваивая каждый раз, пока расстояние не будет найдено;это O (dm), где d - расстояние.Одна тонкость возникает из-за необходимости игнорировать записи на границе нашей полосы, например.p [] = | # | # | # | * d [] = * | # | # | # |Мы должны игнорировать запись слева от крайнего левого элемента. Мы должны игнорировать запись над крайним правым элементом. Еще одна тонкость заключается в том, что наша полоса убегает из матрицы, если строки имеют разный размер.Поскольку строка s всегда поменяется местами, чтобы быть короче из двух, полоса всегда будет уходить в верхний правый угол вместо нижнего левого угла матрицы.В качестве конкретного примера, предположим, что s имеет длину 5, t имеет длину 7, и наш порог равен 1. В этом случае мы собираемся пройти полосу длины 3. Матрица будет выглядеть так: 1 2 3 45 1 | # | # ||||2 | # | # | # |||3 || # | # | # ||4 ||| # | # | # |5 |||| # | # |6 ||||| # |7 ||||||Обратите внимание на то, как полоса уходит со стола, так как нет возможности превратить строку длины 5 в строку длины 7 на расстоянии редактирования 1. Кроме того, эта реализация уменьшает использование памяти, используя два одномерных массива и меняя их обратно.и далее вместо того, чтобы выделять всю матрицу n на m.Это требует нескольких незначительных изменений, таких как немедленный возврат, когда обнаружено, что полоса вышла за пределы матрицы, и начальное заполнение массивов большими значениями, так что записи, которые мы не вычисляем, игнорируются.Посмотрите Алгоритмы на Струнах, Деревьях и Последовательностях Дэном Гасфилдом для некоторого обсуждения.* / int n = s.length ();// длина s int m = t.length ();// длина t // если одна строка пуста, расстояние редактирования обязательно равно длине другой if (n == 0) {return m <= threshold?м: -1;} иначе если (m == 0) {вернуть n <= порог?n: -1;} if (n> m) {// меняем две строки, чтобы использовать меньше памяти final CharSequence tmp = s;s = t;t = tmp;n = m;m = t.length ();} int p [] = new int [n + 1];// «предыдущий» массив затрат, по горизонтали int d [] = new int [n + 1];// массив затрат, горизонтально int _d [];// заполнитель для помощи в замене p и d // заполнение начальных значений таблицы final int border = Math.min (n, threshold) + 1;for (int i = 0; i Integer.MAX_VALUE - порог)?n: Math.min (n, j + порог);// полоса может выводить из таблицы, если s и t имеют разные размеры if (min> max) {return -1;} // игнорируем запись слева от самого левого, если (min> 1) {d [min - 1] = Integer.MAX_VALUE;} // перебирает [min, max] в s для (int i = min; i <= max; i ++) {if (s.charAt (i - 1) == t_j) {// по диагонали влево и вверх d [i] = p [i - 1];} else {// 1 + минимум ячейки слева, сверху, по диагонали влево и вверх d [i] = 1 + Math.min (Math.min (d [i - 1], p [i]),p [i - 1]);}} // копируем текущий счетчик расстояний в «предыдущий ряд» счетчик расстояний _d = p;р = д;d = _d;} // если p [n] больше порогового значения, нет гарантии, что оно будет правильным // расстоянием if (p [n] <= threshold) {return p [n];} возврат -1;} </code>
...