kz-maxx

Хеш строки

Полиномиальный хеш строки через P = 13331, unsigned long long, prefix hash и сравнение подстрок за O(1)

Что такое хеш строки?

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

строка → число
одинаковые строки → одинаковый хеш
разные строки → почти всегда разные хеши

Например:

"abc" → какое-то число
"abd" → другое число
Хеши могут совпасть у разных строк. Это называется коллизия. Но на практике при хорошем хеше шанс маленький.

Зачем нужен хеш строки?

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

сравнить две подстроки за O(1)
найти одинаковые подстроки
найти pattern в text
проверить палиндром
решать задачи на строки быстрее

Без хеша сравнение двух подстрок длины len занимает O(len). С хешем можно сравнивать за O(1).

Идея полиномиального хеша

Строку можно представить как число в системе счисления с основанием P.

Например:

s = "abc"

Тогда хеш можно представить так:

hash = a * P^2 + b * P^1 + c * P^0

В коде обычно считаем слева направо:

hash = hash * P + value(s[i])

Почему P = 13331?

P — это база хеша. Часто берут большое нечетное число.

const ull P = 13331;

Значение 13331 часто используют в строковых хешах. Оно достаточно большое и хорошо работает на практике.

Можно встретить и другие базы: 31, 37, 911382323, 1000003. Но если ты хочешь простой ULL-хеш, P = 13331 — нормальный вариант.

Что такое ULL?

ULL — это сокращение для:

unsigned long long

В C++ удобно написать:

using ull = unsigned long long;

Тогда можно писать коротко:

ull x = 0;
unsigned long long хранит числа от 0 до 2^64 - 1. Если число переполняется, C++ автоматически берет остаток по 2^64. Поэтому отдельный mod можно не писать.

Главная формула

Будем считать prefix hash:

h[0] = 0
h[i + 1] = h[i] * P + value(s[i])

Где:

h[i] — хеш первых i символов
P — база, например 13331
value(s[i]) — числовое значение символа

Для символа можно брать:

(unsigned char)s[i] + 1

+1 нужен, чтобы символ не давал значение 0.

Степени P

Чтобы быстро получать хеш подстроки, заранее считаем степени P.

pw[0] = 1
pw[i + 1] = pw[i] * P

Тогда:

pw[i] = P^i

Код построения prefix hash

using ull = unsigned long long;

const ull P = 13331;

vector<ull> h;
vector<ull> pw;

void build(string s) {
    int n = s.size();

    h.assign(n + 1, 0);
    pw.assign(n + 1, 1);

    for (int i = 0; i < n; i++) {
        pw[i + 1] = pw[i] * P;
        h[i + 1] = h[i] * P + (unsigned char)s[i] + 1;
    }
}

Хеш подстроки

Пусть нужно получить хеш подстроки:

s[l...r-1]

То есть l включительно, r не включительно.

Формула:

get(l, r) = h[r] - h[l] * P^(r-l)

Через массив степеней:

ull get(int l, int r) {
    return h[r] - h[l] * pw[r - l];
}
Формат [l, r) очень удобный: l включается, r не включается.

Почему формула работает?

h[r] — это хеш первых r символов.

В нем есть:

s[0...l-1] + s[l...r-1]

Чтобы убрать левую часть s[0...l-1], нужно умножить h[l] на нужную степень P и вычесть.

h[r] содержит prefix до r
h[l] содержит prefix до l
h[l] * P^(r-l) выравнивает степени
разность оставляет только s[l...r-1]

Полный класс StringHash

using ull = unsigned long long;

struct StringHash {
    static const ull P = 13331;

    vector<ull> h;
    vector<ull> pw;

    StringHash() {}

    StringHash(const string &s) {
        build(s);
    }

    void build(const string &s) {
        int n = s.size();

        h.assign(n + 1, 0);
        pw.assign(n + 1, 1);

        for (int i = 0; i < n; i++) {
            pw[i + 1] = pw[i] * P;
            h[i + 1] = h[i] * P + (unsigned char)s[i] + 1;
        }
    }

    ull get(int l, int r) {
        return h[r] - h[l] * pw[r - l];
    }
};
get(l, r) возвращает хеш подстроки s[l...r-1].

Как сравнить две подстроки?

Если есть строка s, можно сравнить две ее подстроки:

s[l1...r1-1]
s[l2...r2-1]

Если их длины равны и хеши равны, считаем, что подстроки равны.

bool equal_substrings(StringHash &hs, int l1, int r1, int l2, int r2) {
    if (r1 - l1 != r2 - l2) {
        return false;
    }

    return hs.get(l1, r1) == hs.get(l2, r2);
}

Пример сравнения подстрок

Строка:

abacaba

Подстроки:

s[0...3) = aba
s[4...7) = aba

Их хеши равны, значит подстроки одинаковые.

Полная программа: сравнение подстрок

#include <bits/stdc++.h>
using namespace std;

using ull = unsigned long long;

struct StringHash {
    static const ull P = 13331;

    vector<ull> h;
    vector<ull> pw;

    StringHash() {}

    StringHash(const string &s) {
        build(s);
    }

    void build(const string &s) {
        int n = s.size();

        h.assign(n + 1, 0);
        pw.assign(n + 1, 1);

        for (int i = 0; i < n; i++) {
            pw[i + 1] = pw[i] * P;
            h[i + 1] = h[i] * P + (unsigned char)s[i] + 1;
        }
    }

    ull get(int l, int r) {
        return h[r] - h[l] * pw[r - l];
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string s;
    cin >> s;

    StringHash hs(s);

    int l1, r1, l2, r2;
    cin >> l1 >> r1 >> l2 >> r2;

    if (r1 - l1 == r2 - l2 && hs.get(l1, r1) == hs.get(l2, r2)) {
        cout << "YES";
    } else {
        cout << "NO";
    }

    return 0;
}

Input / Output

Input

abacaba
0 3 4 7

Output

YES

Потому что:

s[0...3) = aba
s[4...7) = aba

Если хочешь [l, r] включительно

Иногда удобнее использовать формат:

s[l...r]

Тогда функция будет такой:

ull get_inclusive(int l, int r) {
    return h[r + 1] - h[l] * pw[r - l + 1];
}
Главное — не смешивать форматы. Либо везде [l, r), либо везде [l, r].

Поиск pattern в text через хеш

Нужно найти все позиции, где pattern встречается в text.

Идея:

считаем хеш pattern
считаем хеш text
перебираем все подстроки text длины m
сравниваем их хеш с хешем pattern

Код поиска pattern

vector<int> find_occurrences(string pattern, string text) {
    int m = pattern.size();
    int n = text.size();

    StringHash hp(pattern);
    StringHash ht(text);

    ull need = hp.get(0, m);

    vector<int> ans;

    for (int i = 0; i + m <= n; i++) {
        if (ht.get(i, i + m) == need) {
            ans.push_back(i);
        }
    }

    return ans;
}

Время работы:

O(n + m)

Полная программа поиска pattern

#include <bits/stdc++.h>
using namespace std;

using ull = unsigned long long;

struct StringHash {
    static const ull P = 13331;

    vector<ull> h;
    vector<ull> pw;

    StringHash() {}

    StringHash(const string &s) {
        build(s);
    }

    void build(const string &s) {
        int n = s.size();

        h.assign(n + 1, 0);
        pw.assign(n + 1, 1);

        for (int i = 0; i < n; i++) {
            pw[i + 1] = pw[i] * P;
            h[i + 1] = h[i] * P + (unsigned char)s[i] + 1;
        }
    }

    ull get(int l, int r) {
        return h[r] - h[l] * pw[r - l];
    }
};

vector<int> find_occurrences(string pattern, string text) {
    int m = pattern.size();
    int n = text.size();

    StringHash hp(pattern);
    StringHash ht(text);

    ull need = hp.get(0, m);

    vector<int> ans;

    for (int i = 0; i + m <= n; i++) {
        if (ht.get(i, i + m) == need) {
            ans.push_back(i);
        }
    }

    return ans;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string pattern, text;

    cin >> pattern >> text;

    vector<int> ans = find_occurrences(pattern, text);

    for (int pos : ans) {
        cout << pos << ' ';
    }

    return 0;
}

Пример поиска pattern

Input

aba
abacaba

Output

0 4

Хеш для маленьких латинских букв

Если строка состоит только из a...z, можно брать:

s[i] - 'a' + 1

Тогда код построения будет:

h[i + 1] = h[i] * P + (s[i] - 'a' + 1);
Но универсальнее использовать (unsigned char)s[i] + 1. Тогда код работает не только для маленьких английских букв.

Хеш палиндрома

Хеш можно использовать для проверки, является ли подстрока палиндромом.

Идея:

считаем hash обычной строки
считаем hash перевернутой строки
подстрока палиндром, если ее hash равен hash обратной подстроки

Код проверки палиндрома

bool is_palindrome(string s, int l, int r) {
    int n = s.size();

    string t = s;
    reverse(t.begin(), t.end());

    StringHash hs(s);
    StringHash ht(t);

    int rl = n - r;
    int rr = n - l;

    return hs.get(l, r) == ht.get(rl, rr);
}
Здесь используется формат [l, r). Например, вся строка — это [0, n).

Двойной хеш

Один ULL-хеш обычно работает хорошо, но теоретически коллизии возможны. Чтобы уменьшить шанс коллизии, можно использовать два разных хеша.

hash1 с P1
hash2 с P2
строки равны, если равны оба хеша

Например:

const ull P1 = 13331;
const ull P2 = 1000003;

Простой двойной ULL-хеш

using ull = unsigned long long;

struct DoubleHash {
    static const ull P1 = 13331;
    static const ull P2 = 1000003;

    vector<ull> h1;
    vector<ull> h2;
    vector<ull> p1;
    vector<ull> p2;

    DoubleHash(const string &s) {
        int n = s.size();

        h1.assign(n + 1, 0);
        h2.assign(n + 1, 0);
        p1.assign(n + 1, 1);
        p2.assign(n + 1, 1);

        for (int i = 0; i < n; i++) {
            ull x = (unsigned char)s[i] + 1;

            p1[i + 1] = p1[i] * P1;
            p2[i + 1] = p2[i] * P2;

            h1[i + 1] = h1[i] * P1 + x;
            h2[i + 1] = h2[i] * P2 + x;
        }
    }

    pair<ull, ull> get(int l, int r) {
        ull x = h1[r] - h1[l] * p1[r - l];
        ull y = h2[r] - h2[l] * p2[r - l];

        return {x, y};
    }
};
Для обычных олимпиадных задач часто хватает одного ULL-хеша, но двойной хеш безопаснее.

Хеш с mod или ULL?

Вариант Как работает Плюс
ULL переполнение по 2^64 простой и быстрый
mod берем остаток по простому модулю более контролируемый
double hash два независимых хеша меньше шанс коллизии

Твой основной шаблон P = 13331 + ULL

Вот этот вариант можно просто запомнить и использовать:

using ull = unsigned long long;

struct Hash {
    static const ull P = 13331;

    vector<ull> h;
    vector<ull> pw;

    Hash() {}

    Hash(const string &s) {
        build(s);
    }

    void build(const string &s) {
        int n = s.size();

        h.assign(n + 1, 0);
        pw.assign(n + 1, 1);

        for (int i = 0; i < n; i++) {
            ull x = (unsigned char)s[i] + 1;

            h[i + 1] = h[i] * P + x;
            pw[i + 1] = pw[i] * P;
        }
    }

    ull get(int l, int r) {
        return h[r] - h[l] * pw[r - l];
    }
};

Как этим пользоваться?

string s;
cin >> s;

Hash hs(s);

ull x = hs.get(0, 3);
ull y = hs.get(4, 7);

if (x == y) {
    cout << "same";
} else {
    cout << "different";
}

Если:

s = abacaba

Тогда:

get(0, 3) = hash("aba")
get(4, 7) = hash("aba")

Асимптотика

Операция Время Память
Построить хеш строки O(n) O(n)
Получить хеш подстроки O(1) O(1)
Сравнить две подстроки O(1) O(1)
Найти pattern в text O(n + m) O(n + m)

Когда использовать string hash?

нужно много раз сравнивать подстроки
нужно искать одинаковые подстроки
нужно быстро найти pattern в text
нужно проверять палиндромы
нужно получить O(1) для запроса подстроки

Типичные ошибки

  • Путать формат [l, r) и [l, r]
  • Забыть посчитать pw
  • Использовать int вместо unsigned long long
  • Забыть, что хеши могут иметь коллизии
  • Сравнивать подстроки разной длины только по хешу
  • Брать символ как 0, например s[i] - 'a', где для a будет 0
  • Смешивать разные базы P для одной строки
  • В get написать неправильную степень pw[r - l]

Короткий шаблон

using ull = unsigned long long;

const ull P = 13331;

struct Hash {
    vector<ull> h;
    vector<ull> pw;

    Hash(const string &s) {
        int n = s.size();

        h.assign(n + 1, 0);
        pw.assign(n + 1, 1);

        for (int i = 0; i < n; i++) {
            ull x = (unsigned char)s[i] + 1;

            h[i + 1] = h[i] * P + x;
            pw[i + 1] = pw[i] * P;
        }
    }

    ull get(int l, int r) {
        return h[r] - h[l] * pw[r - l];
    }
};

Главное запомнить

using ull = unsigned long long
P = 13331
h[i + 1] = h[i] * P + value(s[i])
pw[i + 1] = pw[i] * P
get(l, r) = h[r] - h[l] * pw[r-l]
get(l, r) дает hash строки s[l...r-1]