Как найти хеш подстроки

Сайт переезжает. Большинство статей уже перенесено на новую версию.
Скоро добавим автоматические переходы, но пока обновленную версию этой статьи можно найти там.

Хэширование в строковых задачах

Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.

«Хорошая» хэш-функция:

  • Быстро считается — за линейное от размера объекта время;
  • Имеет не очень большие значения — влезающие в 64 бита;
  • «Детерминированно-случайная» — если хэш может принимать (n) различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно (frac{1}{n}).

Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюръективными.

Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны (n) строк длины (m), и нас просят (q) раз проверять произвольные две на равенство. Вместо наивной проверки за (O(q cdot n cdot m)), мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

Применения в реальной жизни

  • Чек-суммы. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.
  • Хэш-таблица. Класс unordered_set из STL можно реализовать так: заведём (n) изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию (f) с областью значений ([0, n)). При обработке .insert(x) мы будем добавлять элемент (x) в (f(x))-тый список. При ответе на .find(x) мы будем проверять, лежит ли (x)-тый элемент в (f(x))-том списке. Благодаря «равномерности» хэш-функции, после (k) добавлений ожидаемое количество сравнений будет равно (frac{k}{n}) = (O(1)) при правильном выборе (n).
  • Мемоизация. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.
  • Проверка на изоморфизм. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.
  • Криптография. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.
  • Поиск в многомерных пространствах. Детерминированный поиск ближайшей точки среди (m) точек в (n)-мерном пространстве быстро не решается. Однако можно придумать хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши, и делать поиск только среди элементов с тем же хэшом, что у запроса.

Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы.

Сегодня же мы остановимся на строках.

Полиномиальное хэширование

Лайфхак: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами.

Будем считать, что строка — это последовательность чисел от (1) до (m) (размер алфавита). В C++ char это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: int x = (int) (c - 'a' + 1).

Определим прямой полиномиальный хэш строки как значение следующего многочлена:

[
h_f = (s_0 + s_1 k + s_2 k^2 + ldots + s_n k^n) mod p
]

Здесь (k) — произвольное число больше размера алфавита, а (p) — достаточно большой модуль, вообще говоря, не обязательно простой.

Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени (k):

const int k = 31, mod = 1e9+7;

string s = "abacabadaba";
long long h = 0, m = 1;
for (char c : s) {
    int x = (int) (c - 'a' + 1);
    h = (h + m * x) % mod;
    m = (m * k) % mod;
}

Можем ещё определить обратный полиномиальный хэш:

[
h_b = (s_0 k^n + s_1 k^{n-1} + ldots + s_n) mod p
]

Его преимущество в том, что можно написать на одну строчку кода меньше:

long long h = 0;
for (char c : s) {
    int x = (int) (c - 'a' + 1);
    h = (h * k + x) % mod;
}

Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой (h).

Зачем это нужно?

Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций.

Например, если нужно посчитать хэш от конкатенации строк (a) и (b) (т. е. (b) приписали в конец строки (a)), то можно просто хэш (b) домножить на (k^{|a|}) и сложить с хэшом (a):

[
h(ab) = h(a) + k^{|a|} cdot h(b)
]

Удалить префикс строки можно так:

[
h(b) = frac{h(ab) — h(a)}{k^{|a|}}
]

А суффикс — ещё проще:

[
h(a) = h(ab) — k^{|a|} cdot h(b)
]

В задачах нам часто понадобится домножать (k) в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:

const int maxn = 1e5+5;

int p[maxn];
p[0] = 1;

for (int i = 1; i < maxn; i++)
    p[i] = (p[i-1] * k) % mod;

Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:

int h[maxn];
h[0] = 0; // h[k] -- хэш префикса длины k

// будем считать, что s это уже последовательность int-ов

for (int i = 0; i < n; i++) 
    h[i+1] = (h[i] + p[i] * s[i]) % mod;

Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке:

[
h(s[l:r]) = frac{h_r-h_l}{k^l}
]

Деление по модулю возможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к (n)-ной. Так проще — нужно будет домножать, а не делить.

[
hat{h}(s[l:r]) = k^{n-l} (h_r-h_l)
]

int hash_substring (int l, int r) {
    return (h[r+1] - h[l]) * p[n-l] % mod;
}

Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за (O(1)).

Упражнение. Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще. Обратный хэш подстроки принято считать и использовать в стандартном виде из определения, поскольку там нет необходимости в делении.

Лайфхак. Если взять обратный полиномиальный хэш короткой строки на небольшом алфавите с (k=10), то числовое значение хэша строки будет наглядно соотноситься с самой строкой:

[
h(abacaba)=1213121
]

Этим удобно пользоваться при дебаге.

Примеры задач

Количество разных подстрок. Посчитаем хэши от всех подстрок за (O(n^2)) и добавим их все в std::set. Чтобы получить ответ, просто вызовем set.size().

Поиск подстроки в строке. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.

Сравнение строк (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.

Палиндромность подстроки. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений hash_substring() на первом массиве и на втором.

Количество палиндромов. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно.

Хранение строк в декартовом дереве

Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в декартово дерево. В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в upd() пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.

Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.

Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова.

Вероятность ошибки и почему это всё вообще работает

У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.

Событие, когда два хэша совпали, а не должны, называется коллизией. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в set (O(n^2)) различных случайных значений в промежутке ([0, m)). Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать (m), чтобы не бояться такого?

Выбор констант

Практическое правило: если вам нужно хранить (n) различных хэшей, то безопасный модуль — это число порядка (10 cdot n^2). Обоснование — см. парадокс дней рождений.

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

Можно также брать модуль (2^{64}). У него есть несколько преимуществ:

  • Он большой — второй модуль точно не понадобится.
  • С ним ни о каких переполнениях заботиться не нужно — если все хранить в unsigned long long, процессор сам автоматически сделает эти взятия остатков при переполнении.
  • С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию %.

Всё с этим модулем было прекрасно, пока не придумали тест против него. Однако, его добавляют далеко не на все контесты — имейте это в виду.

В выборе же (k) ограничения не такие серьезные:

  • Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию.
  • Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.

Главное — чтобы значения (k) и модуля не знал человек, который генерирует тесты.

Парадокс дней рождений

В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%.

Более общее утверждение: в мультимножество нужно добавить (Theta(sqrt{n})) случайных чисел от 1 до n, чтобы какие-то два совпали.

Первое доказательство (для любителей матана). Пусть (f(n, d)) это вероятность того, что в группе из (n) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от (1) до (d).

[
f(n, d) = (1-frac{1}{d}) times (1-frac{2}{d}) times … times (1-frac{n-1}{d})
]

Попытаемся оценить (f):

[
begin{aligned}
e^x & = 1 + x + frac{x^2}{2!} + ldots & text{(ряд Тейлора для экспоненты)} \
& simeq 1 + x & text{(аппроксимация для $|x| ll 1$)} \
e^{-frac{n}{d}} & simeq 1 — frac{n}{d} & text{(подставим $frac{n}{d} ll 1$)} \
f(n, d) & simeq e^{-frac{1}{d}} times e^{-frac{2}{d}} times ldots times e^{-frac{n-1}{d}} & \
& = e^{-frac{n(n-1)}{2d}} & \
& simeq e^{-frac{n^2}{2d}} & \
end{aligned}
]

Из последнего выражения более-менее понятно, что вероятность (frac{1}{2}) достигается при (n approx sqrt{d}) и в этой точке изменяется очень быстро.

Второе доказательство (для любителей теорвера). Введем (frac{n(n-1)}{2}) индикаторов — по одному для каждой пары людей ((i, j)) — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна (frac{1}{d}).

Обозначим за (X) число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть (frac{n (n-1)}{2} cdot frac{1}{d}).

Отсюда понятно, что если (d = Theta(n^2)), то ожидание равно константе, а если (d) асимптотически больше или меньше, то (X) стремится нулю или бесконечности соответственно.

Примечание: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1.

Бонус: «мета-задача»

Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.

«Решите» задачу.

Полиномиальные хеши и их применение

Время на прочтение
9 мин

Количество просмотров 75K

Здравствуй, хабр. Сегодня я напишу, как можно использовать полиномиальные хеши (далее просто хеши) при решении различных алгоритмических задач.

Введение

Начнем с определения. Пусть у нас есть строка s0..n-1. Полиномиальным хешем этой строки называется число h = hash(s0..n-1) = s0 + ps1 + p2s2 +… + pn-1sn-1, где p — некоторое натуральное число (позже будет сказано, какое именно), а si — код i-ого символа строки s (почти во всех современных языках он записывается s[i]).

Хеши обладают тем свойством, что у одинаковых строк хеши обязательно равны. Поэтому основная операция, которую позволяют выполнять хеши — быстрое сравнение двух подстрок на равенство. Конечно, чтобы сравнить 2 строки, мы можем написать подобную функцию (будем считать, что длины строк s и t совпадают и равны n):

boolean equals(char[] s, char[] t) {
	for (int i = 0; i < n; i++)
		if (s[i] != t[i]) {
			return false;
		}
	}
	return true;
}

Однако в худшем случае эта функция обязана проверить все символы, что дает асимптотику O(n).

Сравнение строк с помощью хешей

Теперь посмотрим, как справляются с этой задачей хеши. Так как хеш — это всего лишь число, для их сравнения нам потребуется O(1) времени. Правда, для того, чтобы посчитать хеш одной подстроки наивным способом, требуется O(n) времени. Поэтому потребуется немного повозиться с математическими формулами и научиться находить за O(n) хеши

сразу всех

подстрок. Давайте сравним подстроки sL..R и tX..Y (одинаковой длины). Пользуясь определением хеша, мы можем записать:

sL + psL+1 +… + pR-L-1sR-1 + pR-LsR = tX + ptX+1 +… + pY-X-1tY-1 + pY-XtY

Проведем небольшие преобразования в левой части (в правой части все будет происходить аналогично). Запишем хеш подстроки s0..R, он нам понадобится:

hash(s0..R) = s0 + ps1 +… + pL-1sL-1 + pLsL + pL+1sL+1 +… + pR-1sR-1 + pRsR

Разобьем это выражение на две части…

hash(s0..R) = (s0 + ps1 +… + pL-1sL-1) + (pLsL + pL+1sL+1 +… + pR-1sR-1 + pRsR)

… и вынесем из второй скобки множитель pL:

hash(s0..R) = (s0 + ps1 +… + pL-1sL-1) + pL(sL + psL+1 +… + pR-L-1sR-1 + pR-LsR)

Выражение в первой скобке есть не что иное, как хеш подстроки s0..L-1, а во второй — хеш нужной нам подстроки sL..R. Итак, мы получили, что:

hash(s0..R) = hash(s0..L-1) + pLhash(sL..R)

Отсюда вытекает следующая формула для hash(sL..R):

hash(sL..R) = (1 / pL)(hash(s0..R) — hash(s0..L-1))

Аналогично, для второй нашей подстроки будет выполнено равенство hash(tX..Y) = (1 / pX)(hash(t0..Y) — hash(t0..X-1)).

Внимательно глядя на эти формулы, можно заметить, что для вычисления хеша любой подстроки нам необходимо знать лишь хеши префиксов этой строки s0..0, s0..1, …, s0..n-2, s0..n-1. А, так как хеш каждого следующего префикса выражается через хеш предыдущего, их несложно посчитать линейным проходом по строке. Все сразу за O(n) времени. Степени числа p тоже надо заранее предпросчитать и сохранить в массиве.

Пример кода:

// сохраняем в массиве степени числа p, которые нам могут понадобиться
pow[0] = 1;
for (int i = 1; i <= MAX; i++) {
	pow[i] = pow[i - 1] * p;
}

// считаем хеши префиксов строки s
hs[0] = s[0];
for (int i = 1; i < n; i++) {
	hs[i] = hs[i - 1] + pow[i] * s[i];
}

// считаем хеши префиксов строки t
ht[0] = t[0];
for (int i = 1; i < m; i++) {
	ht[i] = ht[i - 1] + pow[i] * t[i];
}

Казалось бы, мы теперь во всеоружии и умеем сравнивать 2 любые подстроки за O(1). Но не все так просто: математические формулы нуждаются в некоторой доработке. К примеру, подобный код:

if ((hs[R] - hs[L - 1]) / pow[L] == (ht[Y] - ht[X - 1]) / pow[X]) { ... }

работать не будет.

  • Замечание первое: L (или X) может оказаться равным нулю, и при вычислении hs[L - 1] произойдет выход за границы массива. Однако если L равно нулю, то интересующий нас хеш подстроки sL..R хранится в точности в hs[R]. Поэтому правильнее вместо hs[L - 1] писать так:
    L == 0 ? 0 : hs[L - 1]

    .

  • Замечание второе: даже тип long содержит всего 64 бита (я использую Java), а наши строки могут быть длиной в несколько тысяч, и при вычислении хешей неизбежно произойдет переполнение. Эту проблему решить проще всего: надо закрыть на нее глаза. Ну, или почти закрыть: хеши у нас будут считаться, по сути, по модулю 264 (и поэтому не потребуется выполнять операции взятия остатка от деления — красота!). Число p для их подсчета должно быть, во-первых, больше кода самого большого символа в строках, а во-вторых, взаимно простым с модулем (в нашем случае — с 264, т.е. оно должно быть нечетным). Почему так, я здесь рассказывать не буду — об этом можно почитать в книжках по алгебре. Конечно, неизбежно появляется вероятность коллизии, но она крайне мала, поэтому при решении олимпиадных задач можно ей просто пренебречь.
  • Замечание третье: так как все операции мы теперь выполняем по модулю, деление для нас недоступно (точнее, доступно, но писать это довольно неэффективно). Поэтому от него надо избавляться. Делается это довольно просто, способом, который в школе называют «пропорцией»: выражение приводится к общему знаменателю, и вместо деления используется умножение:
    if ((hs[R] - (L == 0 ? 0 : hs[L - 1])) * pow[X] ==
        (ht[Y] - (X == 0 ? 0 : ht[X - 1])) * pow[L]) { ... }
    

Теперь мы более подробно рассмотрим задачи, где можно применять хеши.

Задачи, решаемые с помощью хешей

1. Сравнение подстрок

Первое, и главное, применение, как уже было сказано, это быстрое сравнение двух подстрок — на нем основываются все остальные алгоритмы с хешами. Код в прошлом разделе довольно громоздкий, поэтому я напишу более удобный код, который будет использоваться в дальнейшем.
Следующая функция вычисляет хеш подстроки sL..R, умноженный на pL:

long getHash(long[] h, int L, int R) {
	long result = h[R];
	if (L > 0) result -= h[L - 1];
	return result;
}

Теперь сравнение двух подстрок мы выполняем следующей строчкой:

if (getHash(hs, L, R) * pow[X] == getHash(ht, X, Y) * pow[L]) { ... }

Умножение на степени числа p можно назвать «приведением к одной степени». Первый хеш был умножен на pL, а второй — на pX — значит, чтобы сравнение происходило корректно, их надо домножить на недостающий множитель.
Примечание: имеет смысл сначала проверить, совпадают ли длины подстрок. Если нет, то строки в принципе не могут быть равны, и тогда можно не проверять вышезаписанное условие.

2. Поиск подстроки в строке за O(n + m)

Хеши позволяют искать подстроку в строке за асимптотически минимальное время. Это делает так называемый алгоритм Рабина-Карпа.
Пусть есть строка s длины n, в которой мы хотим найти все вхождения строки t длины m. Найдем хеш строки t (всей строки целиком) и хеши всех префиксов строки s, а затем будем двигаться по строке s окном длины m, сравнивая подстроки si..i+m-1.
Код:

// считаем хеш строки t
long ht = t[0];
for (int i = 1; i < m; i++) {
	ht += pow[i] * t[i];
}

// проверяем все позиции
for (int i = 0; i + m <= n; i++) {
	if (getHash(h, i, i + m - 1) == ht * pow[i]) {
		// обнаружено совпадение на позиции i
	}
}

3. Нахождение z-функции за O(n log n)

Z-функцией строки s называется массив z, i-ый элемент которого равен наидлиннейшему префиксу подстроки, начинающейся с позиции i в строке s, который одновременно является и префиксом всей строки s. Значение z-функции в нулевой позиции будем считать равным длине строки s, хотя некоторые источники принимают его за ноль (но это не критично).

Конечно, есть алгоритм нахождения z-функции за O(n). Но когда его не знаешь или не помнишь (а алгоритм довольно громоздкий), на помощь приходят хеши.

Идея следующая: для каждого i = 0, 1, …, n-1 будем искать zi бинарным поиском, т.е. на каждой итерации сокращая интервал возможных значений вдвое. Это корректно, потому что равенство s0..k-1 = si..i+k-1 обязательно выполняется для всех k, меньших zi, и обязательно не выполняется для больших k.

Код:

int[] z = new int[n];
for (int i = 0; i < n; i++) {
	int left = 1, right = n - i; // текущий интервал значений
	while (left <= right) {
		// находим middle - середину интервала
		int middle = (left + right) / 2;
		// и проверяем, совпадают ли подстроки s[0..middle-1] и s[i..i+middle-1]
		if (getHash(h, 0, middle - 1) * pow[i] == getHash(h, i, i + middle - 1)) {
			// если совпадают, то ответ как равен минимум middle, и надо проверить большие значения
			z[i] = middle;
			left = middle + 1;
		} else {
			// если не совпадают, надо проверить меньшие значения
			right = middle - 1;
		}
	}
}

4. Поиск лексикографически минимального циклического сдвига строки за O(n log n).

Существует алгоритм Дюваля, который позволяет решать эту задачу за O(n), однако я знаю некоторых довольно сильных олимпиадных программистов, которые так и не разобрались в нем. Пока они будут в нем разбираться, мы снова применим хеши.

Алгоритм следующий. Сначала примем саму строку s за лучший (лексикографически минимальный) ответ. Затем для каждого циклического сдвига с помощью бинарного поиска найдем длину максимального общего префикса этого сдвига и текущего лучшего ответа. После этого достаточно сравнить следующие за этим префиксом символы и, если надо, обновить ответ.
Еще заметим, что для удобства здесь рекомендуется приписать строку s к самой себе — не придется делать операции взятия по модулю при обращениям к символам строки s. Будем считать, что это уже сделано.

Код:

int bestPos = 0;
for (int i = 1; i < n; i++) {
	int left = 1, right = n, length = 0;
	// находим length - длину максимального общего префикса
	while (left <= right) {
		int middle = (left + right) / 2;
		if (getHash(h, bestPos, bestPos + middle - 1) * pow[i] ==
		    getHash(h, i, i + middle - 1) * pow[bestPos]) {
			length = middle;
			left = middle + 1;
		} else {
			right = middle - 1;
		}
	}
	// сравниваем следующий за общим префиксом символ и обновляем ответ
	// если длина этого префикса равна n,
	// то текущий циклический сдвиг совпадает с минимальным,
	// и ответ обновлять не нужно
	if (length < n && s[i + length] < s[bestPos + length]) {
		bestPos = i;
	}
}

Примечание: по сути, внутри цикла for написан компаратор, сравнивающий лексикографически два циклических сдвига. Используя его, можно за O(n log2 n) отсортировать все циклические сдвиги.

5. Поиск всех палиндромов в строке за O(n log n).

Опять же, существует решение этой задачи за O(n). И опять мы будем решать ее с помощью хешей.

Подстрока sL..R называется палиндромом, если sL = sR, sL+1 = sR-1, и т.д. Если выражаться русским языком, то это означает, что она читается одинаково как слева направо, так и справа налево.

Возможно, вы уже знаете или догадались, при чем тут хеши. Помимо массива h[], содержащего хеши для подстрок s0..0, s0..1, …, s0..n-2, s0..n-1, посчитаем второй массив rh[] (для «перевернутой» строки), который будем обходить справа налево. Он будет содержать соответственно хеши s0..n-1, s1..n-1, …, sn-2..n-1, sn-1..n-1:

rh[n - 1] = s[n - 1];
for (int i = n - 2, j = 1; i >= 0; i--, j++) {
	rh[i] = rh[i + 1] + pow[j] * s[i];
}

Должно уже быть понятно, как за O(1) определять, является ли строка палиндромом. Я напишу функцию getRevHash(), аналогичную getHash(), а потом приведу необходимое условие сравнения. Вы можете самостоятельно убедиться в правильности этого выражения, проделав математические выкладки, подобные тем, что приводились в начале статьи.

long getRevHash(long[] rh, int L, int R) {
	long result = rh[L];
	if (R < n - 1) result -= rh[R + 1];
	return result;
}

boolean isPalindrome(long[] h, long[] rh, long[] pow, int L, int R) {
	return getHash(h, L, R) * pow[n - R - 1] == getRevHash(rh, L, R) * pow[L];
}

Теперь рассмотрим позицию i в строке. Пусть существует палиндром нечетной длины d с центром в позиции i (в случае четной длины — с центром между позициями i-1 и i). Если обрезать с его краев по одному символу, он останется палиндромом. И так можно продолжать, пока его длина не станет равной нулю.
Таким образом, нам достаточно для каждой позиции хранить 2 значения: сколько существует палиндромов нечетной длины с центром в позиции i, и сколько существует палиндромов четной длины с центром между позициями i-1 и i. Обратите внимание, что эти 2 значения абсолютно независимы друг от друга, и обрабатывать их надо отдельно.

Применим, как и ранее, бинарный поиск:

int[] oddCount = new int[n];
for (int i = 0; i < n; i++) {
	int left = 1, right = min(i + 1, n - i);
	while (left <= right) {
		int middle = (left + right) / 2;
		if (isPalindrome(h, rh, pow, i - middle + 1, i + middle - 1)) {
			oddCount[i] = middle;
			left = middle + 1;
		} else {
			right = middle - 1;
		}
	}
}

int[] evenCount = new int[n];
for (int i = 0; i < n; i++) {
	int left = 1, right = min(i, n - i);
	while (left <= right) {
		int middle = (left + right) / 2;
		if (isPalindrome(h, rh, pow, i - middle, i + middle - 1)) {
			evenCount[i] = middle;
			left = middle + 1;
		} else {
			right = middle - 1;
		}
	}
}

Теперь можно, к примеру, найти общее количество всех палиндромов в строке, или длину максимального палиндрома. Длина максимального нечетного палиндрома с центром в позиции i считается как 2 * oddCount[i] - 1, а максимального четного палиндрома — 2 * evenCount[i].
Еще раз напомню, что нужно быть внимательнее с палиндромами четной и нечетной длины — как правило, их надо обрабатывать независимо друг от друга.

Хеши в матрицах

Наконец, рассмотрим более изощренные применения хешей. Теперь наше пространство будет двумерным, и сравнивать мы будем подматрицы. К счастью, хеши очень хорошо обобщаются на двумерный случай (трехмерных и более я не встречал).

Теперь вместо числа p и массива pow у нас будут два различных числа p, q и два массива pow1, pow2: по одному числу и по одному массиву в каждом направлении: по вертикали и горизонтали.

Хешем матрицы a0..n-1, 0..m-1 будем называть сумму по всем i = 0, …, n-1, j = 0,…, m-1 величин piqjaij.

Теперь научимся считать хеши подматриц, содержащих левый верхний элемент a00. Очевидно, что hash(a0..0, 0..0) = a00. Почти так же очевидно, что для всех j = 1,…, m-1 hash(a0..0, 0..j) = hash(a0..0, 0..j-1) + qja0j, для всех i = 1,…, n-1 hash(a0..i, 0..0) = hash(a0..i-1, 0..0) + piai0. Это напрямую вытекает из одномерного случая.

Как посчитать хеш подматрицы a0..i, 0..j? Можно догадаться, что hash(a0..i, 0..j) = hash(a0..i-1, 0..j) + hash(a0..i, 0..j-1) — hash(a0..i-1, 0..j-1) + piqjaij. Эту формулу можно получить из следующих соображений: сложим все слагаемые (хеш, напомню, это сумма нескольких слагаемых), составляющие хеш подматриц a0..i-1, 0..j и a0..i, 0..j-1. При этом мы два раза учли слагаемые, составляющие подматрицу a0..i-1, 0..j-1, так что вычтем их, чтобы они учитывались один раз. Теперь не хватает только элемента aij, умноженного на соответствующие степени p и q.

Примерно из тех же соображений, что и в первой части статьи (вы уже заметили причастность формулы включений-исключений?) строится функция для вычисления хеша произвольной подматрицы ax1..x2, y1..y2:

long getMatrixHash(long[][] h, int x1, int x2, int y1, int y2) {
	long result = h[x2][y2];
	if (x1 > 0) result -= h[x1 - 1][y2];
	if (y1 > 0) result -= h[x2][y1 - 1];
	if (x1 > 0 && y1 > 0) result += h[x1 - 1][y1 - 1];
	return result;
}

Эта функция возвращает хеш подматрицы ax1..x2, y1..y2, умноженный на величину px1qy1.

А сравнение двух подматриц aax1..ax2, ay1..ay2 и abx1..bx2, by1..by2 выполняется с помощью следующего выражения:

if (getMatrixHash(h, ax1, ax2, ay1, ay2) * pow1[bx1] * pow2[by1] ==
    getMatrixHash(h, bx1, bx2, by1, by2) * pow1[ax1] * pow2[ay1]) { ... }

Хешами также можно решать задачи, связанные с нахождением самой большой симметричной подматрицы и похожие на них. Причем я не знаю сравнимых с хешами по скорости и простоте алгоритмов, выполняющих эту работу. Здесь используются те же принципы, что и при поиске палиндромов в одномерном случае (т.е. считать «реверснутые» хеши справа налево и снизу вверх, проводить бинпоиск отдельно для подматриц четной и нечетной длины). Предлагаю попробовать решить эту задачу самостоятельно — эта статья вам поможет!

Заключение

Итак, в нашем распоряжении есть довольно неплохой аппарат, позволяющий делать многие вещи либо с лучшей возможной асимптотикой, либо лишь чуть-чуть (в логарифм раз) медленнее, чем специализированные алгоритмы. Неплохо, не так ли?

Алгоритм Рабина-Карпа предназначен для поиска подстроки в строке.

Содержание

  • 1 Метод хеширования
  • 2 Решение
    • 2.1 Алгоритм
    • 2.2 Псевдокод
    • 2.3 Время работы
  • 3 Сравнение с другими алгоритмами
  • 4 См. также
  • 5 Источники информации

Метод хеширования

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

Определение:
Пусть дана строка . Тогда полиномиальным хешем (англ. polynomial hash) строки называется число , где — некоторое простое число, а код -ого символа строки .

Проблему переполнения при вычислении хешей довольно больших строк можно решить так считать хеши по модулю (или ), чтобы модуль брался автоматически при переполнении типов.

Для работы алгоритма потребуется считать хеш подстроки . Делать это можно следующим образом:

Рассмотрим хеш :

Разобьем это выражение на две части:

Вынесем из последней скобки множитель :

Выражение в первой скобке есть не что иное, как хеш подстроки , а во второй — хеш нужной нам подстроки . Итак, мы получили, что:

Отсюда получается следующая формула для :

Однако, как видно из формулы, чтобы уметь считать хеш для всех подстрок начинающихся с , нужно предпосчитать все для . Это займет много памяти. Но поскольку нам нужны только подстроки размером мы можем подсчитать хеш подстроки , а затем пересчитывать хеши для всех за следующим образом:

.

.

Получается : .

Решение

Алгоритм

Алгоритм начинается с подсчета и , а также с подсчета , для ускорения ответов на запрос.

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

Если требуется найти индексы вхождения нескольких образцов, или сравнить две строки выгоднее будет предпосчитать все степени , а также хеши всех префиксов строки .

Псевдокод

Приведем пример псевдокода, который находит все вхождения строки в строку и возвращает массив позиций, откуда начинаются вхождения.

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

Новый хеш был получен с помощью быстрого пересчёта. Для сохранения корректности алгоритма нужно считать, что — пустой символ.

Время работы

Изначальный подсчёт хешей выполняется за .

Каждая итерация выполняется за , В цикле всего итераций.

Итоговое время работы алгоритма .

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

Сравнение с другими алгоритмами

Преимущества:

  • Быстрая скорость работы — , где — длина строки, — длина образца;
  • Простая и понятная реализация;

Недостатки:

  • Возможно подобрать входные данные так, что количество ложных срабатываний будет недопустимо большим;

См. также

  • Наивный алгоритм поиска подстроки в строке
  • Поиск наибольшей общей подстроки двух строк с использованием хеширования

Источники информации

  • Кормен, Томас Х., Лейзерсон, Чарльз И., Ривест, Рональд Л., Штайн Клиффорд Алгоритмы: построение и анализ, 3-е издание. Пер. с англ. — М.:Издательский дом «Вильямс», 2014. — 1328 с.: ил. — ISBN 978-5-8459-1794-2 (рус.) — страницы 1036–1041.

Hash Function

A Hash function is a function that maps any kind of data of arbitrary size to fixed-size values. The values returned by the function are called Hash Values or digests. There are many popular Hash Functions such as DJBX33A, MD5, and SHA-256. This post will discuss the key features, implementation, advantages and drawbacks of the Polynomial Rolling Hash Function.

Note that if two strings are equal, their hash values should also be equal. But the inverse need not be true.

The polynomial rolling hash function

Polynomial rolling hash function is a hash function that uses only multiplications and additions. The following is the function:

text{hash(s)} = text{s}[0] + text{s}[1]cdot p + text{s}[2]cdot p^2 + dots + text{s}[n - 1]times p^{n - 1}quad text{mod} m

or simply,

text{hash(s)} = displaystylesum_{i = 0}^{n - 1} s[i]cdot p^iquad text{mod} m

Where

Below is the implementation of the Polynomial Rolling Hash Function

C

#include <stdio.h>

#include <string.h>

int get_hash(const char* s, const int n) {

    long long p = 31, m = 1e9 + 7;

    long long hash = 0;

    long long p_pow = 1;

    for(int i = 0; i < n; i++) {

        hash = (hash + (s[i] - 'a' + 1) * p_pow) % m;

        p_pow = (p_pow * p) % m;

    }

    return hash;

}

int main() {

    char s[] = "geeksforgeeks";

    int n = strlen(s);

    printf("Hash of %s is %dn", s, get_hash(s, n));

    return 0;

}

C++

#include <bits/stdc++.h>

using namespace std;

struct Hash {

    long long p = 31, m = 1e9 + 7;

    long long hash_value;

    Hash(const string& s)

    {

        long long hash_so_far = 0;

        long long p_pow = 1;

        const long long n = s.length();

        for (long long i = 0; i < n; ++i) {

            hash_so_far

                = (hash_so_far + (s[i] - 'a' + 1) * p_pow)

                  % m;

            p_pow = (p_pow * p) % m;

        }

        hash_value = hash_so_far;

    }

    bool operator==(const Hash& other)

    {

        return (hash_value == other.hash_value);

    }

};

int main()

{

    const string s = "geeksforgeeks";

    Hash h(s);

    cout << "Hash of " << s << " is: " << h.hash_value

         << 'n';

    return 0;

}

Java

class Hash {

    final int p = 31, m = 1000000007;

    int hash_value;

    Hash(String S)

    {

        int hash_so_far = 0;

        final char[] s = S.toCharArray();

        long p_pow = 1;

        final int n = s.length;

        for (int i = 0; i < n; i++) {

            hash_so_far = (int)((hash_so_far

                                 + (s[i] - 'a' + 1) * p_pow)

                                % m);

            p_pow = (p_pow * p) % m;

        }

        hash_value = hash_so_far;

    }

}

class Main {

    public static void main(String[] args)

    {

        String s = "geeksforgeeks";

        Hash h = new Hash(s);

        System.out.println("Hash of " + s + " is "

                           + h.hash_value);

    }

}

Python3

class Hash:

    def __init__(self, s: str):

        self.hash_value = 0

        self.p, self.m = 31, 10**9 + 7

        self.length = len(s)

        hash_so_far = 0

        p_pow = 1

        for i in range(self.length):

            hash_so_far = (hash_so_far + (1 + ord(s[i]) - ord('a')) * p_pow) % self.m

            p_pow = (p_pow * self.p) % self.m

        self.hash_value = hash_so_far

    def __eq__(self, other):

        return self.hash_value == other.hash_value

if __name__ == '__main__':

    s = "geeksforgeeks"

    h = Hash(s)

    print("Hash value of {} is {}".format(s, h.hash_value))

C#

using System;

public struct Hash

{

    public long p;   

    public long m;   

    public long hash_value;    

    public Hash(string s)

    {

        p = 31;

        m = 1000000007;

        long hash_so_far = 0;

        long p_pow = 1;

        long n = s.Length;

        for (long i = 0; i < n; ++i)

        {

            hash_so_far = (hash_so_far + (s[(int)i] - 'a' + 1) * p_pow) % m;

            p_pow = (p_pow * p) % m;

        }

        hash_value = hash_so_far;

    }

    public static bool operator ==(Hash a, Hash b)

    {

        return a.hash_value == b.hash_value;

    }

    public static bool operator !=(Hash a, Hash b)

    {

        return !(a == b);

    }

}

public class MainClass

{

    public static void Main()

    {

        string s = "geeksforgeeks";

        Hash h = new Hash(s);

        Console.WriteLine($"Hash of {s} is: {h.hash_value}");

    }

}

Javascript

function get_hash(s) {

    const p = 31;

    const m = 1e9 + 7;

    let hash = 0;

    let pPow = 1;

    for (let i = 0; i < s.length; i++) {

        hash = (hash + (s.charCodeAt(i) - 'a'.charCodeAt(0) + 1) * pPow) % m;

        pPow = (pPow * p) % m;

    }

    return hash;

}

const s = "geeksforgeeks";

console.log(`Hash of ${s} is ${get_hash(s)}`);

Output

Hash of geeksforgeeks is 609871790

Time Complexity: O(N)

Auxiliary Space: O(1)

Collisions in Polynomial Rolling Hash

Since the output of the Hash function is an integer in the range [0, m)                 , there are high chances for two strings producing the same hash value.

For instance, the strings text{``countermand''}                  and text{``furnace''}                  produce the same hash value for p = 31                  and m = 10^9 + 7                 .

Also, the strings text{``answers''}                  and  text{``stead''}                  produce the same hash value for p = 37                  and m = 10^9 + 9                 .

We can guarantee a collision within a very small domain. Consider a set of strings, S                 , consisting of only lower-case letters, such that the length of any string in S                  doesn’t exceed 7                 .

We have |S| = (26 + 26^2 + 26^3 + 26^4 + 26^5 + 26^6 + 26^7) = 8353082582gt 10^9 + 7                 . Since the range of the Hash Function is [0, m)                 , one-one mapping is impossible. Hence, we can guarantee a collision by arbitrarily generating two strings whose length doesn’t exceed 7                 .

Collision Resolution

We can note that the value of m                  affects the chances of collision. We have seen that the probability of collision is cfrac{1}{m}                 . We can increase the value of m                  to reduce the probability of collision. But that affects the speed of the algorithm. Larger the value of m                 , the slower the algorithm. Also, some languages (C, C++, Java) have a limit on the size of the integer. Hence, we can’t increase the value of m                  to a very large value.

Then how can we minimise the chances of a collision?

Note that the hash of a string depends on two parameters: p                  and m                 .

We have seen that the strings text{``countermand''}                  and text{``furnace''}                  produce the same hash value for p = 31                  and m = 10^9 + 7                 . But for p = 37                  and m = 10^9 + 9                 , they produce different hashes.

Observation:

If two strings produce the same hash values for a pair (p1, m1)                 , they will produce different hashes for a different pair, (p2, m2)                 .

Strategy:

We cannot, however, nullify the chances of collision because there are infinitely many strings. But, surely, we can reduce the probability of two strings colliding.

We can reduce the probability of collision by generating a pair of hashes for a given string. The first hash is generated using p = 31                  and m = 10^9 + 7                 , while the second hash is generated using p = 37                  and m = 10^9 + 9                 .

Why will this work?

We are generating two hashes using two different modulo values, m1                  and m2                 . The probability of a collision is now cfrac{1}{m1} times cfrac{1}{m2}                 . Since both m1                  and m2                  are greater than 10^9                 , the probability that a collision occurs is now less than displaystyle10^{-18}                  which is so much better than the original probability of collision, 10^{-9}                 .

Below is the implementation for the same

C++

#include <bits/stdc++.h>

using namespace std;

struct Hash {

    const int p1 = 31, m1 = 1e9 + 7;

    const int p2 = 37, m2 = 1e9 + 9;

    int hash1 = 0, hash2 = 0;

    Hash(const string& s) {

        compute_hash1(s);

        compute_hash2(s);

    }

    void compute_hash1(const string& s) {

        long p_pow = 1;

        for(char ch: s) {

            hash1 = (hash1 + (ch + 1 - 'a') * p_pow) % m1;

            p_pow = (p_pow * p1) % m1;

        }

    }

    void compute_hash2(const string& s) {

        long p_pow = 1;

        for(char ch: s) {

            hash2 = (hash2 + (ch + 1 - 'a') * p_pow) % m2;

            p_pow = (p_pow * p2) % m2;

        }

    }

    bool operator==(const Hash& other) {

        return (hash1 == other.hash1 && hash2 == other.hash2);

    }

};

int main() {

    const string s = "geeksforgeeks";

    Hash h(s);

    cout << "Hash values of " << s << " are: ";

    cout << "(" << h.hash1 << ", " << h.hash2 << ")" << 'n';

    return 0;

}

C

#include <stdio.h>

#include <string.h>

int get_hash1(const char* s, int length) {

    const int p = 31, m = 1e9 + 7;

    int hash_value = 0;

    long p_pow = 1;

    for(int i = 0; i < length; i++) {

        hash_value = (hash_value + (s[i] - 'a' + 1) * p_pow) % m;

        p_pow = (p_pow * p) % m;

    }

    return hash_value;

}

int get_hash2(const char* s, int length) {

    const int p = 37, m = 1e9 + 9;

    int hash_value = 0;

    long p_pow = 1;

    for(int i = 0; i < length; i++) {

        hash_value = (hash_value + (s[i] - 'a' + 1) * p_pow) % m;

        p_pow = (p_pow * p) % m;

    }

    return hash_value;

}

int main() {

    char s[] = "geeksforgeeks";

    int length = strlen(s);

    int hash1 = get_hash1(s, length);

    int hash2 = get_hash2(s, length);

    printf("Hash values of %s are: (%d, %d)n", s, hash1, hash2);

    return 0;

}

Java

class Hash {

    final int p1 = 31, m1 = 1000000007;

    final int p2 = 37, m2 = 1000000009;

    int hash_value1, hash_value2;

    Hash(String s) {

        compute_hash1(s);

        compute_hash2(s);

    }

    void compute_hash1(String s) {

        int hash_so_far = 0;

        final char[] s_array = s.toCharArray();

        long p_pow = 1;

        final int n = s_array.length;

        for (int i = 0; i < n; i++) {

            hash_so_far = (int)((hash_so_far + (s_array[i] - 'a' + 1) * p_pow) % m1);

            p_pow = (p_pow * p1) % m1;

        }

        hash_value1 = hash_so_far;

    }

    void compute_hash2(String s) {

        int hash_so_far = 0;

        final char[] s_array = s.toCharArray();

        long p_pow = 1;

        final int n = s_array.length;

        for (int i = 0; i < n; i++) {

            hash_so_far = (int)((hash_so_far + (s_array[i] - 'a' + 1) * p_pow) % m2);

            p_pow = (p_pow * p2) % m2;

        }

        hash_value2 = hash_so_far;

    }

}

class Main {

    public static void main(String[] args) {

        String s = "geeksforgeeks";

        Hash h = new Hash(s);

        System.out.println("Hash values of " + s + " are: " + h.hash_value1 + ", " + h.hash_value2);

    }

}

Python3

class Hash:

    def __init__(self, s: str):

        self.p1, self.m1 = 31, 10**9 + 7

        self.p2, self.m2 = 37, 10**9 + 9

        self.hash1, self.hash2 = 0, 0

        self.compute_hashes(s)

    def compute_hashes(self, s: str):

        pow1, pow2 = 1, 1

        hash1, hash2 = 0, 0

        for ch in s:

            seed = 1 + ord(ch) - ord('a')

            hash1 = (hash1 + seed * pow1) % self.m1

            hash2 = (hash2 + seed * pow2) % self.m2

            pow1 = (pow1 * self.p1) % self.m1

            pow2 = (pow2 * self.p2) % self.m2

        self.hash1, self.hash2 = hash1, hash2

    def __eq__(self, other):

        return self.hash1 == other.hash1 and self.hash2 == other.hash2

    def __str__(self):

        return f'({self.hash1}, {self.hash2})'

if __name__ == '__main__':

    s = "geeksforgeeks"

    hash = Hash(s)

    print("Hash of " + s + " is " + str(hash))

C#

using System;

class Hash {

    readonly int p1 = 31, m1 = 1000000007;

    readonly int p2 = 37, m2 = 1000000009;

    public int hash_value1, hash_value2;

    public Hash(string s) {

        compute_hash1(s);

        compute_hash2(s);

    }

    void compute_hash1(string s) {

        int hash_so_far = 0;

        char[] s_array = s.ToCharArray();

        long p_pow = 1;

        int n = s_array.Length;

        for (int i = 0; i < n; i++) {

            hash_so_far = (int)((hash_so_far + (s_array[i] - 'a' + 1) * p_pow) % m1);

            p_pow = (p_pow * p1) % m1;

        }

        hash_value1 = hash_so_far;

    }

    void compute_hash2(string s) {

        int hash_so_far = 0;

        char[] s_array = s.ToCharArray();

        long p_pow = 1;

        int n = s_array.Length;

        for (int i = 0; i < n; i++) {

            hash_so_far = (int)((hash_so_far + (s_array[i] - 'a' + 1) * p_pow) % m2);

            p_pow = (p_pow * p2) % m2;

        }

        hash_value2 = hash_so_far;

    }

}

class Program {

    public static void Main(string[] args) {

        string s = "geeksforgeeks";

        Hash h = new Hash(s);

        Console.WriteLine("Hash values of " + s + " are: " + h.hash_value1 + ", " + h.hash_value2);

    }

}

Javascript

function get_hash1(s, length) {

  const p = 31, m = 1e9 + 7;

  let hash_value = 0;

  let p_pow = 1;

  for (let i = 0; i < length; i++) {

    hash_value = (hash_value + (s.charCodeAt(i) - 97 + 1) * p_pow) % m;

    p_pow = (p_pow * p) % m;

  }

  return hash_value;

}

function get_hash2(s, length) {

  const p = 37, m = 1e9 + 9;

  let hash_value = 0;

  let p_pow = 1;

  for (let i = 0; i < length; i++) {

    hash_value = (hash_value + (s.charCodeAt(i) - 97 + 1) * p_pow) % m;

    p_pow = (p_pow * p) % m;

  }

  return hash_value;

}

const s = "geeksforgeeks";

const length = s.length;

const hash1 = get_hash1(s, length);

const hash2 = get_hash2(s, length);

console.log(`Hash values of ${s} are: (${hash1}, ${hash2})`);

Output

Hash values of geeksforgeeks are: (609871790, 642799661)

Time Complexity: O(N)

Auxiliary Space: O(1)

Features of Polynomial rolling hash function

Calculation of Hashes of any substring of a given string in O(1)

Note that computing the hash of the string S will also compute the hashes of all of the prefixes. We just have to store the hash values of the prefixes while computing. Say text{hash[i]} denotes the hash of the prefix text{S[0…i]}, we have

text{hash[i...j]}cdot p^i = text{hash[0...j]} - text{hash[0...(i - 1)]}

This allows us to quickly compute the hash of the substring text{S[i...j]}                  in O(1)                  provided we have powers of p                  ready.

The behaviour of the hash when a character is changed

Recall that the hash of a string s                  is given by

text{hash(s)} = displaystylesum_{i = 0}^{n - 1} s[i]cdot p^iquad text{mod} m

Say, we change a character ch1                  at some index i                  to some other character ch2                 . How will the hash change?

If text{hash_old}                  denotes the hash value before changing and text{hash_new}                  is the hash value after changing, then the relation between them is given by

text{hash_new} = text{hash_old} - p^icdot(ch1) + p^icdot(ch2)

Therefore, queries can be performed very quickly instead of recalculating the hash from beginning, provided we have the powers of p                  ready.

A more elegant implementation is provided below. 

C++

#include <bits/stdc++.h>

using namespace std;

long long power(long long x, long long y, long long p) {

    long long result = 1;

    for(; y; y >>= 1, x = x * x % p) {

        if(y & 1) {

            result = result * x % p;

        }

    }

    return result;

}

long long inverse(long long x, long long p) {

    return power(x, p - 2, p);

}

class Hash {

private:

    int length;

    const int mod1 = 1e9 + 7, mod2 = 1e9 + 9;

    const int p1 = 31, p2 = 37;

    vector<int> hash1, hash2;

    pair<int, int> hash_pair;

public:

    inline static vector<int> inv_pow1, inv_pow2;

    inline static int inv_size = 1;

    Hash() {}

    Hash(const string& s) {

        length = s.size();

        hash1.resize(length);

        hash2.resize(length);

        int h1 = 0, h2 = 0;

        long long p_pow1 = 1, p_pow2 = 1;

        for(int i = 0; i < length; i++) {

            h1 = (h1 + (s[i] - 'a' + 1) * p_pow1) % mod1;

            h2 = (h2 + (s[i] - 'a' + 1) * p_pow2) % mod2;

            p_pow1 = (p_pow1 * p1) % mod1;

            p_pow2 = (p_pow2 * p2) % mod2;

            hash1[i] = h1;

            hash2[i] = h2;

        }

        hash_pair = make_pair(h1, h2);

        if(inv_size < length) {

            for(; inv_size < length; inv_size <<= 1);

            inv_pow1.resize(inv_size, -1);

            inv_pow2.resize(inv_size, -1);

            inv_pow1[inv_size - 1] = inverse(power(p1, inv_size - 1, mod1), mod1);

            inv_pow2[inv_size - 1] = inverse(power(p2, inv_size - 1, mod2), mod2);

            for(int i = inv_size - 2; i >= 0 && inv_pow1[i] == -1; i--) {

                inv_pow1[i] = (1LL * inv_pow1[i + 1] * p1) % mod1;

                inv_pow2[i] = (1LL * inv_pow2[i + 1] * p2) % mod2;

            }

        }

    }

    int size() {

        return length;

    }

    pair<int, int> prefix(const int index) {

        return {hash1[index], hash2[index]};

    }

    pair<int, int> substr(const int l, const int r) {

        if(l == 0) {

            return {hash1[r], hash2[r]};

        }

        int temp1 = hash1[r] - hash1[l - 1];

        int temp2 = hash2[r] - hash2[l - 1];

        temp1 += (temp1 < 0 ? mod1 : 0);

        temp2 += (temp2 < 0 ? mod2 : 0);

        temp1 = (temp1 * 1LL * inv_pow1[l]) % mod1;

        temp2 = (temp2 * 1LL * inv_pow2[l]) % mod2;

        return {temp1, temp2};

    }

    bool operator==(const Hash& other) {

        return (hash_pair == other.hash_pair);

    }

};

int main() {

    string my_str = "geeksforgeeks";

    const int n = my_str.length();

    auto hash = Hash(my_str);

    auto hash_pair = hash.substr(0, n - 1);

    cout << "Hashes of the string " << my_str << " are:n";

    cout << hash_pair.first << ' ' << hash_pair.second << 'n';

    return 0;

}

Python3

class RollingHash:

    def __init__(self, s):

        self.length = len(s)

        self.mod1 = 10**9 + 7

        self.mod2 = 10**9 + 9

        self.p1 = 31

        self.p2 = 37

        self.hash1 = [0] * self.length

        self.hash2 = [0] * self.length

        h1 = h2 = 0

        p_pow1 = p_pow2 = 1

        for i in range(self.length):

            h1 = (h1 + (ord(s[i]) - ord('a') + 1) * p_pow1) % self.mod1

            h2 = (h2 + (ord(s[i]) - ord('a') + 1) * p_pow2) % self.mod2

            p_pow1 = (p_pow1 * self.p1) % self.mod1

            p_pow2 = (p_pow2 * self.p2) % self.mod2

            self.hash1[i] = h1

            self.hash2[i] = h2

    def prefix(self, index):

        return (self.hash1[index], self.hash2[index])

    def substr(self, l, r):

        if l == 0:

            return (self.hash1[r], self.hash2[r])

        temp1 = self.hash1[r] - self.hash1[l-1]

        temp2 = self.hash2[r] - self.hash2[l-1]

        temp1 += self.mod1 if temp1 < 0 else 0

        temp2 += self.mod2 if temp2 < 0 else 0

        temp1 = (temp1 * pow(self.p1, self.length-l, self.mod1)) % self.mod1

        temp2 = (temp2 * pow(self.p2, self.length-l, self.mod2)) % self.mod2

        return (temp1, temp2)

    def __eq__(self, other):

        return self.prefix(self.length-1) == other.prefix(other.length-1)

my_str = "geeksforgeeks"

hash = RollingHash(my_str)

hash_pair = hash.substr(0, len(my_str)-1)

print("Hashes of the string", my_str, "are:")

print(hash_pair)

In the above implementation, we are computing the inverses of powers of $p$ in linear time.

Applications:

Consider this problem: Given a sequence S of N strings and Q queries. In each query, you are given two indices, i and j, your task is to find the length of the longest common prefix of the strings S[i] and S[j].

Before getting into the approach to solve this problem, note that the constraints are:

1le N le 10^5\ 1le Q le 10^5\ 1le |S| le 10^5\ text{The Sum of |S| over all test cases doesn't exceed } 10^6

Using Hashing, the problem can be solved in O(N + Q/log|S|_{max}). The approach is to compute hashes for all the strings in O(N) time, Then for each query, we can binary search the length of the longest common prefix using hashing. The implementation for this approach is provided below.

C++14

#include <bits/stdc++.h>

using namespace std;

long long power(long long x, long long y, long long p) {

    long long result = 1;

    for(; y; y >>= 1, x = x * x % p) {

        if(y & 1) {

            result = result * x % p;

        }

    }

    return result;

}

long long inverse(long long x, long long p) {

    return power(x, p - 2, p);

}

class Hash {

private:

    int length;

    const int mod1 = 1e9 + 7, mod2 = 1e9 + 9;

    const int p1 = 31, p2 = 37;

    vector<int> hash1, hash2;

    pair<int, int> hash_pair;

public:

    inline static vector<int> inv_pow1, inv_pow2;

    inline static int inv_size = 1;

    Hash() {}

    Hash(const string& s) {

        length = s.size();

        hash1.resize(length);

        hash2.resize(length);

        int h1 = 0, h2 = 0;

        long long p_pow1 = 1, p_pow2 = 1;

        for(int i = 0; i < length; i++) {

            h1 = (h1 + (s[i] - 'a' + 1) * p_pow1) % mod1;

            h2 = (h2 + (s[i] - 'a' + 1) * p_pow2) % mod2;

            p_pow1 = (p_pow1 * p1) % mod1;

            p_pow2 = (p_pow2 * p2) % mod2;

            hash1[i] = h1;

            hash2[i] = h2;

        }

        hash_pair = make_pair(h1, h2);

        if(inv_size < length) {

            for(; inv_size < length; inv_size <<= 1);

            inv_pow1.resize(inv_size, -1);

            inv_pow2.resize(inv_size, -1);

            inv_pow1[inv_size - 1] = inverse(power(p1, inv_size - 1, mod1), mod1);

            inv_pow2[inv_size - 1] = inverse(power(p2, inv_size - 1, mod2), mod2);

            for(int i = inv_size - 2; i >= 0 && inv_pow1[i] == -1; i--) {

                inv_pow1[i] = (1LL * inv_pow1[i + 1] * p1) % mod1;

                inv_pow2[i] = (1LL * inv_pow2[i + 1] * p2) % mod2;

            }

        }

    }

    int size() {

        return length;

    }

    pair<int, int> prefix(const int index) {

        return {hash1[index], hash2[index]};

    }

    pair<int, int> substr(const int l, const int r) {

        if(l == 0) {

            return {hash1[r], hash2[r]};

        }

        int temp1 = hash1[r] - hash1[l - 1];

        int temp2 = hash2[r] - hash2[l - 1];

        temp1 += (temp1 < 0 ? mod1 : 0);

        temp2 += (temp2 < 0 ? mod2 : 0);

        temp1 = (temp1 * 1LL * inv_pow1[l]) % mod1;

        temp2 = (temp2 * 1LL * inv_pow2[l]) % mod2;

        return {temp1, temp2};

    }

    bool operator==(const Hash& other) {

        return (hash_pair == other.hash_pair);

    }

};

void query(vector<Hash>& hashes, const int N) {

    int i = 0, j = 0;

    cin >> i >> j;

    i--, j--;

    int lb = 0, ub = min(hashes[i].size(), hashes[j].size());

    int max_length = 0;

    while(lb <= ub) {

        int mid = (lb + ub) >> 1;

        if(hashes[i].prefix(mid) == hashes[j].prefix(mid)) {

            if(mid + 1 > max_length) {

                max_length = mid + 1;

            }

            lb = mid + 1;

        }

        else {

            ub = mid - 1;

        }

    }

    cout << max_length << 'n';

}

int main() {

    int N = 0, Q = 0;

    cin >> N >> Q;

    vector<Hash> hashes;

    for(int i = 0; i < N; i++) {

        string s;

        cin >> s;

        hashes.push_back(Hash(s));

    }

    for(; Q > 0; Q--) {

        query(hashes, N);

    }

    return 0;

}

Input:
5 4
geeksforgeeks geeks hell geeksforpeaks hello
1 2
1 3
3 5
1 4
Expected Output:
5
0
4
8

Last Updated :
10 Apr, 2023

Like Article

Save Article

Хэширование в строковых задачах

Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.

«Хорошая» хэш-функция:

  • Быстро считается — за линейное от размера объекта время;
  • Имеет не очень большие значения — влезающие в 64 бита;
  • «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $frac{1}{n}$.

Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюръективными.

Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q cdot n cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

Применения в реальной жизни

  • Чек-суммы. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.
  • Хэш-таблица. Класс unordered_set из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке .insert(x) мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на .find(x) мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $frac{k}{n}$ = $O(1)$ при правильном выборе $n$.
  • Мемоизация. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.
  • Проверка на изоморфизм. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.
  • Криптография. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.
  • Поиск в многомерных пространствах. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши, и делать поиск только среди элементов с тем же хэшом, что у запроса.

Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы.

Сегодня же мы остановимся на строках.

Полиномиальное хэширование

Лайфхак: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами.

Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ char это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: int x = (int) (c - 'a' + 1).

Определим прямой полиномиальный хэш строки как значение следующего многочлена:

$$
h_f = (s_0 + s_1 k + s_2 k^2 + ldots + s_n k^n) mod p
$$

Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой.

Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$:

const int k = 31, mod = 1e9+7;

string s = "abacabadaba";
long long h = 0, m = 1;
for (char c : s) {
    int x = (int) (c - 'a' + 1);
    h = (h + m * x) % mod;
    m = (m * k) % mod;
}

Можем ещё определить обратный полиномиальный хэш:

$$
h_b = (s_0 k^n + s_1 k^{n-1} + ldots + s_n) mod p
$$

Его преимущество в том, что можно написать на одну строчку кода меньше:

long long h = 0;
for (char c : s) {
    int x = (int) (c - 'a' + 1);
    h = (h * k + x) % mod;
}

Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$.

Зачем это нужно?

Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций.

Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$:

$$
h(ab) = h(a) + k^{|a|} cdot h(b)
$$

Удалить префикс строки можно так:

$$
h(b) = frac{h(ab) — h(a)}{k^{|a|}}
$$

А суффикс — ещё проще:

$$
h(a) = h(ab) — k^{|a|} cdot h(b)
$$

В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:

const int maxn = 1e5+5;

int p[maxn];
p[0] = 1;

for (int i = 1; i < maxn; i++)
    p[i] = (p[i-1] * k) % mod;

Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:

int h[maxn];
h[0] = 0; // h[k] -- хэш префикса длины k

// будем считать, что s это уже последовательность int-ов

for (int i = 0; i < n; i++) 
    h[i+1] = (h[i] + p[i] * s[i]) % mod;

Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке:

$$
h(s[l:r]) = frac{h_r-h_l}{k^l}
$$

Деление по модулю возможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить.

$$
hat{h}(s[l:r]) = k^{n-l} (h_r-h_l)
$$

int hash_substring (int l, int r) {
    return (h[r+1] - h[l]) * p[n-l] % mod;
}

Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$.

Упражнение. Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще. Обратный хэш подстроки принято считать и использовать в стандартном виде из определения, поскольку там нет необходимости в делении.

Лайфхак. Если взять обратный полиномиальный хэш короткой строки на небольшом алфавите с $k=10$, то числовое значение хэша строки будет наглядно соотноситься с самой строкой:

$$
h(abacaba)=1213121
$$

Этим удобно пользоваться при дебаге.

Примеры задач

Количество разных подстрок. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в std::set. Чтобы получить ответ, просто вызовем set.size().

Поиск подстроки в строке. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.

Сравнение строк (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.

Палиндромность подстроки. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений hash_substring() на первом массиве и на втором.

Количество палиндромов. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно.

Хранение строк в декартовом дереве

Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в декартово дерево. В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в upd() пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.

Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.

Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова.

Вероятность ошибки и почему это всё вообще работает

У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.

Событие, когда два хэша совпали, а не должны, называется коллизией. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в set $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого?

Выбор констант

Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 cdot n^2$. Обоснование — см. парадокс дней рождений.

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

Можно также брать модуль $2^{64}$. У него есть несколько преимуществ:

  • Он большой — второй модуль точно не понадобится.
  • С ним ни о каких переполнениях заботиться не нужно — если все хранить в unsigned long long, процессор сам автоматически сделает эти взятия остатков при переполнении.
  • С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию %.

Всё с этим модулем было прекрасно, пока не придумали тест против него. Однако, его добавляют далеко не на все контесты — имейте это в виду.

В выборе же $k$ ограничения не такие серьезные:

  • Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию.
  • Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.

Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты.

Парадокс дней рождений

В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%.

Более общее утверждение: в мультимножество нужно добавить $Theta(sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали.

Первое доказательство (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения.
Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$.

$$
f(n, d) = (1-frac{1}{d}) times (1-frac{2}{d}) times … times (1-frac{n-1}{d})
$$

Попытаемся оценить $f$:

$$
begin{aligned}
e^x & = 1 + x + frac{x^2}{2!} + ldots & text{(ряд Тейлора для экспоненты)} \
& simeq 1 + x & text{(аппроксимация для $|x| ll 1$)} \
e^{-frac{n}{d}} & simeq 1 — frac{n}{d} & text{(подставим $frac{n}{d} ll 1$)} \
f(n, d) & simeq e^{-frac{1}{d}} times e^{-frac{2}{d}} times ldots times e^{-frac{n-1}{d}} & \
& = e^{-frac{n(n-1)}{2d}} & \
& simeq e^{-frac{n^2}{2d}} & \
end{aligned}
$$

Из последнего выражения более-менее понятно, что вероятность $frac{1}{2}$ достигается при $n approx sqrt{d}$ и в этой точке изменяется очень быстро.

Второе доказательство (для любителей теорвера). Введем $frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $frac{1}{d}$.

Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $frac{n (n-1)}{2} cdot frac{1}{d}$.

Отсюда понятно, что если $d = Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно.

Примечание: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1.

Бонус: «мета-задача»

Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.

«Решите» задачу.

Понравилась статья? Поделить с друзьями:
  • Как исправить косточку на ноге большого пальца в домашних условиях
  • Как найти длину средней линии по клеткам
  • Как найти угол через тангенс онлайн
  • Как найти решетку на ноутбуке
  • Наушники mi true wireless работают по одному как исправить