Поиск подстроки в строке с использованием хеширования. Алгоритм Рабина-Карпа — различия между версиями
(→Пример худшего случая) |
(→Пример худшего случая) |
||
Строка 57: | Строка 57: | ||
Обозначим за <tex>S_k</tex> строку <tex>S</tex> для фиксированного <tex>k</tex> , а за <tex>S'_k</tex> инвертированную строку <tex>S</tex>. | Обозначим за <tex>S_k</tex> строку <tex>S</tex> для фиксированного <tex>k</tex> , а за <tex>S'_k</tex> инвертированную строку <tex>S</tex>. | ||
− | Покажем, что при <tex>k | + | Покажем, что при <tex>k < 12</tex>, <tex>\mathrm{hash}(S_k) = \mathrm{hash}(S'_k)</tex>. Ведь если это так, то сами по себе <tex>S_k</tex> и <tex>S'_k</tex> встретятся в б''о''льших строках много-много раз. |
Разберемся, что значит <tex>\mathrm{hash}(S_k) = \mathrm{hash}(S'_k)</tex>. Можно смело заменить коды символов на нули и единицы в коэффициентах многочлена — тем самым мы просто сократим обе части на <tex>\sum\limits_{i=0}^{2^k - 1} 65 \cdot p^i</tex>. | Разберемся, что значит <tex>\mathrm{hash}(S_k) = \mathrm{hash}(S'_k)</tex>. Можно смело заменить коды символов на нули и единицы в коэффициентах многочлена — тем самым мы просто сократим обе части на <tex>\sum\limits_{i=0}^{2^k - 1} 65 \cdot p^i</tex>. | ||
Строка 73: | Строка 73: | ||
Нужно понять, на какую максимальную степень двойки делится каждая из <tex>k - 1</tex> скобок. Заметим, что <tex>(i + 1)</tex>-ая скобка <tex>p^{2^{i + 1}} - 1 = (p^{2i} - 1)(p^{2i} + 1)</tex> делится на <tex>i</tex>-ую и ещё на какое-то чётное число <tex>p^{2i} + 1</tex>. Это означает, что если <tex>i</tex>-ая скобка делится на <tex>2^r</tex>, то <tex>(i + 1)</tex>-ая скобка делится по меньшей мере на <tex>2^{r + 1}</tex>. | Нужно понять, на какую максимальную степень двойки делится каждая из <tex>k - 1</tex> скобок. Заметим, что <tex>(i + 1)</tex>-ая скобка <tex>p^{2^{i + 1}} - 1 = (p^{2i} - 1)(p^{2i} + 1)</tex> делится на <tex>i</tex>-ую и ещё на какое-то чётное число <tex>p^{2i} + 1</tex>. Это означает, что если <tex>i</tex>-ая скобка делится на <tex>2^r</tex>, то <tex>(i + 1)</tex>-ая скобка делится по меньшей мере на <tex>2^{r + 1}</tex>. | ||
− | Получается, что <tex>(p^1 - 1)(p^2 - 1)(p^4 - 1)...(p^{2k - 1} - 1)</tex> делится по меньшей мере на <tex>2 \cdot 2^2 \cdot 2^3 \cdot ... = 2^{k(k - 1) / 2}</tex>. Значит достаточно взять <tex>k >= 12</tex>, чтобы в рассматриваемой строке было очень много различных подстрок, чьи хеши совпадут. | + | Получается, что <tex>(p^1 - 1)(p^2 - 1)(p^4 - 1)...(p^{2k - 1} - 1)</tex> делится по меньшей мере на <tex>2 \cdot 2^2 \cdot 2^3 \cdot ... = 2^{k(k - 1) / 2}</tex>. |
+ | |||
+ | Мы показали, что если k < 12, то величина <tex>\mathrm{hash}(S_k) - \mathrm{hash}(S'_k) = 0</tex>, то есть <tex>\mathrm{hash}(S_k) = \mathrm{hash}(S'_k)</tex>. Значит достаточно взять <tex>k >= 12</tex>, чтобы в рассматриваемой строке было очень много различных подстрок, чьи хеши совпадут. | ||
== Сравнение с другими алгоритмами == | == Сравнение с другими алгоритмами == |
Версия 21:37, 15 июня 2015
Алгоритм Рабина-Карпа предназначен для поиска подстроки в строке.
Содержание
Метод хеширования
Наивный алгоритм поиска подстроки в строке работает за в худшем случае — слишком долго. Чтобы ускорить этот процесс, можно воспользоваться методом хеширования.
Определение: |
Пусть дана строка | . Тогда полиномиальным хешем строки называется число , где — некоторое простое число, а - код -ого символа строки .
Проблему переполнения при вычислении хешей довольно больших строк можно решить так: будем считать хеши по модулю
, чтобы модуль брался автоматически при переполнении типов.Использование полиномиального хеша именно с убывающими степенями
позволяет нам, зная хеш некоторой строки, посчитать хеш строки, образованной удалением первого символа и добавлением символа в конец, за :.
.
Получается :
.Решение
Алгоритм
Алгоритм начинается с подсчета
и .Для наивном алгоритме поиска подстроки в строке. В первом случае проверка произойдет быстрее, но вероятность ложного срабатывания, хоть и небольшая, останется. Во втором случае проверка займет время, равное длине образца, но полностью исключит возможность ложного срабатывания.
вычисляется и сравнивается с . Если они оказались равны, то образец скорее всего содержится в строке начиная с позиции , хотя возможны и ложные срабатывания алгоритма. Если требуется свести такие срабатывания к минимуму или исключить вовсе, то применяют сравнение некоторых символов из этих строк, которые выбраны случайным образом, или применяют явное сравнение строк, как вДля ускорения работы алгоритма оптимально предпосчитать
.Псевдокод
Приведем пример псевдокода, который находит все вхождения строки
в строку и возвращает массив позиций, откуда начинаются вхождения.vector<int> rabinKarp (s : string, w : string):
vector<int> answer
int n = s.length
int m = w.length
int hashS = hash(s[0..m-1])
int hashW = hash(w[0..m-1])
for i = 0 to n - m
if hashS == hashW
answer.add(i)
hashS = (p * hashS - p
* hash(s[i]) + hash(s[i + m])) mod r // r — некоторое большое число, p — некоторое просто число
return answer
Новый хеш
был получен с помощью быстрого пересчёта. Для сохранения корректности алгоритма нужно считать, что — пустой символ.Время работы
Изначальный подсчёт хешей выполняется за
.Каждая итерация выполняется за
, В цикле всего итераций.Итоговое время работы алгоритма
.Пример худшего случая
Если количество подстрок данной строки превышает количество хешей (а это выполняется тогда, когда длина строки больше коллизий неизбежно. Но даже при относительно небольших строках вероятность коллизий может быть высока, не говоря уже о способах составления специальных строк, где алгоритм на хешах выдаёт частые ложные срабатывания.
, так как количество различных значений полиномиального хеша совпадает с ), то наступлениеНапример, возьмем за строку Туэ-Морса[1] длиной , , — любое просто число.
Обозначим за
строку для фиксированного , а за инвертированную строку .Покажем, что при
, . Ведь если это так, то сами по себе и встретятся в больших строках много-много раз.Разберемся, что значит
. Можно смело заменить коды символов на нули и единицы в коэффициентах многочлена — тем самым мы просто сократим обе части на .Величина
есть некоторое число . То есть это знакопеременная сумма степеней , где знаки чередуются по тому же правилу, что и символы в строке.Будем последовательно выносить из этой суммы множители за скобку:
Покажем, что
:Нужно понять, на какую максимальную степень двойки делится каждая из
скобок. Заметим, что -ая скобка делится на -ую и ещё на какое-то чётное число . Это означает, что если -ая скобка делится на , то -ая скобка делится по меньшей мере на .Получается, что
делится по меньшей мере на .Мы показали, что если k < 12, то величина
, то есть . Значит достаточно взять , чтобы в рассматриваемой строке было очень много различных подстрок, чьи хеши совпадут.Сравнение с другими алгоритмами
Преимущества
- Быстрая скорость работы — , где — длина строки, — длина образца.
- Простая и понятная реализация.
Недостатки
- Возможно подобрать входные данные так, что количество ложных срабатываний будет недопустимо большим (см. Пример худшего случая).
См. также
- Наивный алгоритм поиска подстроки в строке
- Поиск наибольшей общей подстроки двух строк с использованием хеширования
Примечания
Источники информации
- Кормен, Томас Х., Лейзерсон, Чарльз И., Ривест, Рональд Л., Штайн Клиффорд Алгоритмы: построение и анализ, 3-е издание. Пер. с англ. — М.:Издательский дом "Вильямс", 2014. — 1328 с.: ил. — ISBN 978-5-8459-1794-2 (рус.) — страницы 1036–1041.
- Codeforces: Anti-hash test