Более простой способ решения проблемы - это предварительно вычисленная карта [плохое слово] -> [предложения].
Проблема в том, что, хотя удаление буквы создает несколько "плохих слов", для добавления или замены у вас есть много кандидатов.
Так что я бы предложил другое решение;)
Примечание: расстояние редактирования, которое вы описываете, называется Расстояние Левенштейна
Решение описывается поэтапно, обычно скорость поиска должна постоянно улучшаться для каждой идеи, и я попытался сначала организовать их с помощью более простых идей (с точки зрения реализации). Не стесняйтесь останавливаться, когда вам удобно с результатами.
0. Предварительное
- Реализация алгоритма расстояния Левенштейна
- Хранить словарь в отсортированной последовательности (например,
std::set
, хотя отсортированная std::deque
или std::vector
будет более эффективной по производительности)
Ключи Очки:
- При вычислении расстояния Левенштейна используется массив, на каждом шаге следующая строка вычисляется исключительно с предыдущей строкой
- Минимальное расстояние в строке всегда превосходит (или равно) минимальное расстояние в предыдущем ряду
Последнее свойство допускает реализацию короткого замыкания: если вы хотите ограничить себя 2 ошибками (порогом), то всякий раз, когда минимум текущей строки превосходит 2, вы можете отказаться от вычисления. Простая стратегия - вернуть порог + 1 как расстояние.
1. Первый предварительный
Давайте начнем с простого.
Мы реализуем линейное сканирование: для каждого слова мы вычисляем расстояние (короткое замыкание) и перечисляем те слова, которые до сих пор достигли меньшего расстояния.
Очень хорошо работает с небольшими словарями.
2. Улучшение структуры данных
Расстояние Левенштейна составляет , по крайней мере, равно разнице длины.
Используя в качестве ключа пару (длину, слово) вместо просто слова, вы можете ограничить поиск диапазоном длины [length - edit, length + edit]
и значительно сократить пространство поиска.
3. Префиксы и обрезка
Чтобы улучшить это, мы можем заметить, что при построении матрицы расстояний строка за строкой один мир полностью сканируется (слово, которое мы ищем), а другой (референт) - нет: мы используем только одну букву за каждый ряд.
Это очень важное свойство означает, что для двух референтов, которые имеют одинаковую начальную последовательность (префикс), первые строки матрицы будут идентичны.
Помните, что я прошу вас хранить отсортированный словарь? Это означает, что слова с одинаковым префиксом смежны.
Предположим, что вы проверяете свое слово по cartoon
и на car
вы понимаете, что оно не работает (расстояние уже слишком большое), тогда любое слово, начинающееся с car
, также не будет работать, вы можете пропустить слова, если они начинаются с car
.
Сам пропуск можно выполнить либо линейно, либо с помощью поиска (найдите первое слово с более высоким префиксом, чем car
):
- linear лучше всего работает, если префикс длинный (несколько слов для пропуска)
- бинарный поиск лучше всего подходит для короткого префикса (пропущено много слов)
Как долго будет "long", зависит от вашего словаря, и вам придется измерять. Я хотел бы начать с двоичного поиска.
Примечание: разделение по длине работает против разделения префиксов, но оно сокращает намного больше пространства поиска
4. Префиксы и повторное использование
Теперь мы также постараемся максимально использовать вычисления (а не только результат "это не работает")
Предположим, у вас есть два слова:
Сначала вы вычисляете матрицу строка за строкой для cartoon
. Затем при чтении carwash
вам нужно определить длину общего префикса (здесь car
), и вы можете сохранить первые 4 строки матрицы (соответствующие void, c
, a
, r
) .
Поэтому, когда начинаете вычислять carwash
, вы фактически начинаете итерацию в w
.
Для этого просто используйте массив, выделенный прямо в начале поиска, и сделайте его достаточно большим, чтобы вместить большую ссылку (вы должны знать, какая длина в вашем словаре самая большая).
5. Использование «лучшей» структуры данных
Чтобы было легче работать с префиксами, вы можете использовать Trie или Patricia Tree для хранения словаря. Однако это не структура данных STL, и вам нужно будет расширить ее, чтобы сохранить в каждом поддереве диапазон длины сохраняемых слов, поэтому вам придется создавать собственную реализацию. Это не так просто, как кажется, потому что есть проблемы со взрывом памяти, которые могут убить локальность.
Это последний вариант. Это дорого обходится.