Есть ли разреженный алгоритм дистанции редактирования?

Скажем, у вас две строки длиной 100 000, содержащие нули и единицы. Вы можете вычислить их расстояние редактирования примерно в 10 ^ 10 операций.

Если каждая строка содержит только 100 единиц, а остальные - нули, я могу представить каждую строку, используя 100 целых чисел, где они указаны.

Есть ли более быстрый алгоритм для вычисления расстояния редактирования, используя это разреженное представление? Еще лучше будет алгоритм, который также использует пространство 100 ^ 2 вместо 10 ^ 10 пространства.

Чтобы дать что-то для тестирования, рассмотрите эти две строки по 10 штук каждый. Целые числа говорят, где они находятся в каждой строке.

[9959, 10271, 12571, 21699, 29220, 39972, 70600, 72783, 81449, 83262]

[9958, 10270, 12570, 29221, 34480, 37952, 39973, 83263, 88129, 94336]

В алгоритмических терминах, если у нас есть две разреженные двоичные строки длины n каждая из которых представлена целыми числами k, существует ли алгоритм расстояний редактирования времени O(k^2)?

Ответ 1

Конечно! Существует так мало возможных операций с таким количеством 0. Я имею в виду, ответ не более 200.

Взгляни на

10001010000000001
vs       ||||||
10111010100000010

Посмотрите на все нули с трубами. Неважно, какой из них вы в конечном итоге удаляете? Нету. Это ключ.


Решение 1

Рассмотрим нормальное решение n * m:

dp(int i, int j) {
    // memo & base case
    if( str1[i-1] == str1[j-1] ) {
        return dp(i-1, j-1);
    }
    return 1 + min( dp(i-1, j), dp(i-1, j-1), dp(i, j-1) );
}

Если бы почти каждый персонаж был 0, что бы заработало больше времени?

if( str1[i-1] == str1[j-1] ) { // They will be equal so many times, (99900)^2 times!
    return dp(i-1, j-1);
}

Я мог представить себе, что он обрушивается на десятки тысяч рекурсий. Все, на что вы на самом деле нуждаетесь в логике, - это 200 критических точек. Вы можете игнорировать остальные. Простая модификация

if( str1[i-1] == str1[j-1] ) {
    if( str1[i-1] == 1 )
        return dp(i-1, j-1); // Already hit a critical point

    // rightmost location of a 1 in str1 or str2, that is <= i-1
    best = binarySearch(CriticalPoints, i-1);
    return dp(best + 1, best + 1); // Use that critical point
    // Important! best+1 because we still want to compute the answer at best
    // Without it, we would skip over in a case where str1[best] is 1, and str2[best] is 0.
}

CriticalPoints будет массивом, содержащим индекс каждого 1 в str1 или str2. Убедитесь, что он отсортирован перед бинарным поиском. Имейте в виду эти гоччи. Моя логика: Хорошо мне нужно, чтобы убедиться, чтобы вычислить ответ на индекс best сам, так что давайте идти с best + 1 в качестве параметра. Но, если best == я - 1, мы застреваем в цикле. Я обработаю это с быстрой str1[i-1] == 1. Сделано, плех.

Вы можете быстро проверить правильность, отметив, что в худшем случае вы нажмете все 200 * 100000 комбинаций я и j, которые будут делать критические точки, и когда эти критические точки вызовут min(a, b, c), он будет делать только три рекурсивные вызовы функций. Если какая-либо из этих функций является критическими точками, то она является частью тех 200 * 100000, которые мы уже подсчитали, и мы можем ее игнорировать. Если это не так, то в O (log (200)) он попадает в один вызов в другой критической точке (теперь это то, что мы знаем, является частью 200 * 100000, которые мы уже подсчитали). Таким образом, каждая критическая точка занимает в худшем случае 3*log(200) время, исключая вызовы в другие критические точки. Аналогично, самый первый вызов функции попадет в критический момент времени log(200). Таким образом, мы имеем верхнюю границу O (200 * 100000 * 3 * log (200) + log (200)).

Кроме того, убедитесь, что ваша таблица заметок - это hashmap, а не массив. 10 ^ 10 памяти не поместится на вашем компьютере.


Решение 2

Вы знаете, что ответ не более 200, поэтому просто не позволяйте себе вычислять больше, чем многие операции.

dp(int i, int j) { // O(100000 * 205), sounds good to me.
    if( abs(i - j) > 205 )
        return 205; // The answer in this case is at least 205, so it irrelevant to calculating the answer because when min is called, it wont be smallest.
    // memo & base case
    if( str1[i-1] == str1[j-1] ) {
        return dp(i-1, j-1);
    }
    return 1 + min( dp(i-1, j), dp(i-1, j-1), dp(i, j-1) );
}

Это очень просто, но я оставляю это для решения два, потому что это решение, кажется, вышло из воздуха, в отличие от анализа проблемы и выяснения, где находится медленная часть и как ее оптимизировать. Храните это в своем наборе инструментов, так как вы должны кодировать это решение.


Решение 3

Как и решение 2, мы могли бы сделать это следующим образом:

dp(int i, int j, int threshold = 205) {
    if( threshold == 0 )
        return 205;
    // memo & base case
    if( str1[i-1] == str1[j-1] ) {
        return dp(i-1, j-1);
    }
    return 1 + min( dp(i-1, j, threshold - 1), dp(i-1, j-1, threshold - 1), dp(i, j-1, threshold - 1) );
}

Вы можете беспокоиться о том, что dp (i-1, j-1) стекает вниз, но порог держит я и j близко друг к другу, поэтому он вычисляет подмножество решения 2. Это происходит потому, что порог уменьшается каждый раз, когда я и j становятся дальше Кроме. dp(i-1, j-1, threshold) сделает его идентичным решению 2 (таким образом, это немного быстрее).


Космос

Эти решения дадут вам ответ очень быстро, но если вы хотите также оптимизировать пространство, было бы легко заменить str1[i] на (i in Str1CriticalPoints)? 1: 0 (i in Str1CriticalPoints)? 1: 0, используя hashmap. Это дало бы окончательное решение, которое все еще очень быстро (хотя будет медленнее на 10 раз), а также избегает сохранения длинных строк в памяти (до того момента, когда он может работать на Arduino). Я не думаю, что это необходимо.

Обратите внимание, что исходное решение не использует пробел 10 ^ 10. Вы упоминаете "еще лучше, менее 10 ^ 10 пробелов", что подразумевает, что пространство 10 ^ 10 будет приемлемым. К сожалению, даже с достаточным объемом оперативной памяти, итерация, хотя это пространство занимает 10 ^ 10 раз, что определенно неприемлемо. Ни одно из моих решений не использует пространство 10 ^ 10: только 2 * 10 ^ 5, чтобы удерживать строки, чего можно избежать, как обсуждалось выше. 10 ^ 10 Байт для 10 ГБ.


EDIT: в качестве примечаний для манифеста, вам нужно только проверить abs(i - j) > 105, так как остальные 100 вставок, необходимых для приравнивания i и j будут тянуть число операций выше 200.