Поиск подстроки
Склеить шаблон с текстом через разделитель — и задача сводится к уже написанной префикс-функции. Плюс версия, которой хватает памяти на один шаблон.
3 мин
Задача: найти все вхождения строки в строку .
Наивно — сравнивать с каждой позицией: . При это операций.
Сведение к префикс-функции
Склеим шаблон, разделитель и текст:
Разделитель # обязан не встречаться ни в , ни в . Тогда никакой бордер склейки не может быть длиннее : он бы содержал разделитель, а тот в строке ровно один.
Значит, ровно в тех позициях, где закончилось вхождение шаблона.
vector<int> findAll(const string& t, const string& s) {
string all = t + '\x01' + s; // разделителя нет в алфавите задачи
vector<int> pi = prefixFunction(all);
vector<int> res;
for (size_t i = t.size() + 1; i < all.size(); i++)
if (pi[i] == (int)t.size())
res.push_back(i - 2 * t.size()); // начало вхождения в s
return res;
}
по времени и по памяти. Проверено: на 40 000 пар строк совпало с прямым сравнением на каждой позиции.
Про разделитель
Забыть его — классическая ошибка. Без разделителя на строке t = "aa", s = "aaa" префикс-функция склейки aaaaa даёт значения , и значение 2 встречается там, где вхождения нет.
Если алфавит задачи — строчные латинские буквы, годится любой символ вне их: #, $, \x01. Если во входе может быть что угодно, разделитель приходится добывать иначе — например, работать не с символами, а с числами и взять .
Когда текст не влезает в память
Текст читается по одному символу из потока, целиком его хранить нельзя. Хеши тут неудобны: для них нужен весь массив префиксных хешей.
А префикс-функция — удобна. Посмотрите на её код: обращение идёт только к pi[i-1] и к pi[len-1], где len не превосходит . То есть весь хвост массива, относящийся к тексту, не нужен — достаточно:
- значений для первых позиций (это часть про шаблон, она не меняется);
- одного последнего значения.
vector<int> pi = prefixFunction(t); // считаем только для шаблона
int len = 0, count = 0;
char c;
while (cin >> c) { // текст приходит по одному символу
while (len > 0 && c != t[len]) len = pi[len - 1];
if (c == t[len]) len++;
if (len == (int)t.size()) { count++; len = pi[len - 1]; }
}
Память вместо . При тексте в 50 миллионов символов это разница между «решено» и «превышен предел памяти».
Обратите внимание на len = pi[len - 1] после найденного вхождения: без этого мы бы застряли на состоянии len == |t|, а нам нужно продолжить поиск со следующей позиции.
Как это называется
Алгоритм известен как Кнута — Морриса — Пратта (КМП). Классическое изложение вводит его отдельно, с двумя указателями по тексту и шаблону; изложение через префикс-функцию склейки короче и не требует нового кода.
Родственные постановки
Циклический сдвиг. Является ли циклическим сдвигом ? Ищем в строке . Если и вхождение нашлось — да.
Наибольший префикс , являющийся суффиксом . Это последнее значение у склейки, без всяких доработок.
Сравнение с хешами. Полиномиальное хеширование решает ту же задачу и обычно пишется быстрее. Разница в том, что хеши вероятностны и ломаются, а префикс-функция даёт точный ответ и работает в потоке.