Задача поиска подстроки: для текста T длины n и образца P длины m требуется найти все позиции i (0-индексация), такие что T[i..i+m−1] = P.
Ключевые применения: поиск ключевых слов, фильтрация логов, анализ ДНК-цепочек, подсветка совпадений, предобработка в компиляторах. В заданиях ЕГЭ по информатике встречаются задачи на подсчёт вхождений, проверку наличия/позиции, поиск с различными ограничениями на алфавит/регистр, а также вопросы по асимптотике.
Нижняя оценка: любая корректная процедура в худшем случае смотрит Ω(n) символов, если m фиксировано и P нетривиален.
Идея
Сдвигаем окно длины m по T и сравниваем символы.
для i = 0..n−m:
проверить равенство T[i..i+m−1] и P[0..m−1]
Сравнение обычно выполняется справа налево или слева направо; останов при первом несовпадении.
Корректность и сложность
Корректность очевидна: анализируем все возможные выравнивания.
Сложность: O(n·m) в худшем случае (например, T = aaaa…ab, P = aaaa…ac).
Где полезен: очень короткие m, маленькие тексты, когда накладные расходы на предобработку критичнее.
Интуиция
КМП избегает повторного сравнения ранее совпавших префиксов. При несоответствии на позиции j в образце, окно сдвигается на величину, определённую функцией наибольшего собственного префикса, являющегося суффиксом (π-функция).
Префикс-функция (π)
Для строки S длины L:
π[0] = 0
π[i] = длина наибольшего собственного префикса S[0..i], совпадающего с суффиксом S[0..i]
Стандартная линейная конструкция π требует O(L) времени.
Для поиска π строится по P, после чего выполняется линейный проход по T, поддерживая длину текущего совпадения q с префиксом P[0..q−1].
Поиск (схема)
q = 0
для i = 0..n−1:
пока q > 0 и P[q] ≠ T[i]:
q = π[q−1]
если P[q] = T[i]:
q++
если q = m:
зафиксировать вхождение i−m+1
q = π[m−1]
Инварианты и сложность
Инвариант: в любой момент q – длина максимального префикса P, совпадающего с суффиксом T[0..i].
Сложность: O(n + m) по времени, O(m) памяти.
Практика: устойчив к «злонамеренным» примерам, хорош, когда нужно гарантированное линейное время.
Идея
Сопоставлять не символы, а хеши окон T[i..i+m−1], поддерживая rolling hash. Совпадение хеша → проверка посимвольно (чтобы исключить коллизию).
Полиномиальный хеш
Для строки S:
H(S) = ( Σ_{k=0..|S|−1} S[k] · p^k ) mod M
Рекомендуется большой модуль M и основание p. При скользящем окне выполняется:
H_next = ( (H_current − S_out · p^{m−1}) · p + S_in ) mod M
Сложность и вероятности
Ожидаемая сложность: O(n + m) при малом числе ложных совпадений, худший случай O(n·m), если хеш часто совпадает.
Вероятность ложного совпадения ≈ 1/M при «хорошем» выборе параметров.
Практика: эффективен для множественных образцов (поиск многих P_i одновременно), для потоков, когда удобно работать с числами.

Идея
Сравнение выполняется справа налево; при первом несовпадении образец сдвигается сразу на много позиций по заранее вычисленным таблицам.
Плохой символ (bad character)
Таблица Bad[c] – позиция последнего вхождения символа c в P (или −1). Если несовпадение на P[j] и T[i] = c, то возможный сдвиг:
shift_bad = j − Bad[c]
(не меньше 1).
Хороший суффикс (good suffix)
Если совпал суффикс P[j+1..m−1], таблица Good[j] даёт сдвиг так, чтобы выровнять в P другое вхождение этого суффикса или, если его нет, совпадающий префикс.
Сложность
Ожидаемо очень быстро на «случайных» текстах: подстрока перескакивает большие фрагменты → близко к O(n/m) сравнений.
В худшем случае O(n·m) (например, повторяющиеся символы).
Варианты BM (Boyer–Moore–Horspool, Sunday) упрощают таблицы и практично ускоряют средний случай.
Практика: высокопроизводителен при большом алфавите и случайных данных; хорош для прикладных поисков в файлах/логах.
Z-функция
Для S:
Z[i] = длина наибольшего префикса S, совпадающего с S[i..]
Строится за O(|S|). Для поиска подстроки рассматривают строку:
S = P + '#' + T
Позиции, где Z[i] = m, соответствуют вхождениям P в T.
π-функция (альтернатива КМП)
Можно так же конкатенировать P#T и вычислить π; значения π[i] = m указывают на вхождения. Обе техники линейны и просты в коде.
|
Алгоритм |
Время худшее |
Время среднее |
Память |
Особенности |
|
Наивный |
O(n·m) |
O(n·m) |
O(1) |
Простота, мало кода |
|
КМП |
O(n+m) |
O(n+m) |
O(m) |
Гарантированное линейное время |
|
Рабин–Карп |
O(n·m)* |
O(n+m) |
O(1)/O(m) |
Хеши, легко для множественных образцов |
|
Бойер–Мур (+варианты) |
O(n·m) |
близко к O(n/m) |
O( |
Σ |
|
Z/π-подход |
O(n+m) |
O(n+m) |
O(n+m) |
Простая линейная предобработка |
*При хорошо подобранных параметрах ожидаемо линейный.
R1 Если нужна гарантия линейного времени и простота – выбирайте КМП или Z/π.
R2 Для очень больших текстов и «обычных» данных – Бойер–Мур(Хорспул/Санди) даст лучший средний случай.
R3 Для множественного поиска (много образцов) используйте Рабина–Карпа или автоматы (Ахо–Корасик).
R4 В Рабине–Карпе всегда подтверждайте совпадение посимвольно (антиколлизия).
R5 Учитывайте алфавит: для маленького алфавита BM теряет эффективность из-за частых «плохих символов».
R6 Следите за границами массивов таблиц (Bad/Good/π/Z), не выходите за пределы.
R7 Для чувствительности/нечувствительности к регистру нормализуйте вход (например, к нижнему регистру).
R8 Для Юникода используйте сравнение по кодопойнтам, не по байтам UTF-8 без осознания границ символов.
R9 В задачах ЕГЭ заранее оцените `n`, `m` и алфавит – это подскажет оптимальный метод.
R10 Отлаживайте на «злых» случаях: одинаковые символы, периодические строки, очень длинные совпадения с сбоем в конце.
Префикс-функция π для S:
π[0] = 0
для i=1..|S|−1:
j = π[i−1]
пока j>0 и S[i] ≠ S[j]: j = π[j−1]
если S[i] = S[j]: j++
π[i] = j
КМП-поиск P в T:
q = 0
для i=0..n−1:
пока q>0 и P[q] ≠ T[i]: q = π[q−1]
если P[q] = T[i]: q++
если q = m: ответ (i−m+1), q = π[m−1]
Полиномиальный хеш (rolling):
H_next = ( (H − out·p^{m−1})·p + in ) mod M
Сдвиг «плохого символа»:
shift_bad = max(1, j − Bad[T[i]])
Упражнение 1. Построение π-функции и КМП
Даны T и P.
a) Постройте π для P и выведите массив π в явном виде.
b) Используя схему КМП, найдите все позиции вхождений P в T.
c) Докажите, что суммарное число сравнений символов не превышает 2n (до константы).
Упражнение 2. Рабин–Карп: параметры и ложные совпадения
Для фиксированных M и p реализуйте rolling hash.
a) Оцените вероятность ложного совпадения как ≈ 1/M.
b) Подберите M (простое число) так, чтобы вероятность была < 10^{-9}.
c) На случайном тексте оцените ожидаемое число ложных совпадений и подтвердите, что итоговая сложность близка к O(n).
Упражнение 3. Бойер–Мур–Хорспул
Реализуйте BM-Horspool (только «плохой символ» по последнему символу окна).
a) Постройте таблицу сдвигов длиной |Σ|.
b) На текстах из латиницы сравните время с наивным алгоритмом и КМП для m=8, 32, 128.
c) Объясните, почему на случайных текстах BM-Horspool выигрывает, а на малом алфавите – нет.
Упражнение 4. Z-функция для конкатенации
Постройте строку S = P#T.
a) Постройте массив Z.
b) Выведите все позиции i, для которых Z[i] = m; это вхождения P в T.
c) Сравните затраты памяти и времени с КМП; объясните эквивалентность результатов.
Упражнение 5. Периодичность строки и поиск
Пусть образец P имеет период d, т. е. P[i] = P[i−d] для всех допустимых i.
a) Докажите, что m − π[m−1] равен минимальному периоду d_min.
b) Объясните, как знание периода сокращает число сдвигов в КМП.
c) Для P = "abababababa" вычислите π и укажите d_min.
Алгоритмы поиска подстроки образуют базовый инструментарий обработки строк: от гарантированно линейных КМП/Z до практически очень быстрых Бойера–Мура и гибкого Рабина–Карпа. Понимание инвариантов (префиксы/суффиксы), грамотная предобработка и осознанный выбор алгоритма по характеристикам данных позволяют решать задачи строго и эффективно.
Для ЕГЭ по информатике темы поиска подстроки развивают навыки построения и доказательства линейных алгоритмов, анализа асимптотики, аккуратной работы с массивами и строками – компетенции, напрямую конвертируемые в высокие баллы.