kz-maxx

Рабин-Карп

Поиск pattern в text через string hash, P = 13331, ULL и сравнение подстрок за O(1)

Что делает Рабин-Карп?

Алгоритм Рабина-Карпа ищет строку pattern внутри строки text.

pattern — что ищем
text — где ищем
answer — позиции, где pattern встречается в text

Например:

pattern = aba
text = abacaba

Ответ:

0 4

Потому что aba встречается в abacaba на позициях 0 и 4.

Главная идея

Вместо того чтобы сравнивать строки посимвольно, Рабин-Карп сравнивает их хеши.

считаем hash(pattern)
считаем hash каждой подстроки text длины pattern.size()
если hash совпал, значит строки почти точно равны

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

Наивный поиск

Обычный способ: поставить pattern в каждую позицию text и сравнить символы.

for (int i = 0; i + m <= n; i++) {
    bool ok = true;

    for (int j = 0; j < m; j++) {
        if (text[i + j] != pattern[j]) {
            ok = false;
            break;
        }
    }

    if (ok) {
        cout << i << ' ';
    }
}

Время:

O(n * m)

Если n и m большие, это медленно.

Рабин-Карп через хеш

Пусть:

m = pattern.size()
n = text.size()

Тогда перебираем все подстроки text длины m:

text[0...m)
text[1...m+1)
text[2...m+2)
...

И сравниваем их хеш с хешем pattern.

hash(text[i...i+m)) == hash(pattern)
значит pattern найден в позиции i

Какой хеш будем использовать?

Будем использовать полиномиальный hash:

hash = hash * P + value(symbol)

В этой реализации:

using ull = unsigned long long;

const ull P = 13331;
ULL работает по overflow modulo 2^64. То есть отдельный mod писать не нужно.

Почему P = 13331?

P — база полиномиального хеша. Обычно берут большое нечетное число.

13331 часто используют для string hash. Для обычных олимпиадных задач это нормальный и простой вариант.

P = 13331
тип = unsigned long long
overflow работает как mod 2^64

Prefix hash

Чтобы быстро получать hash любой подстроки, заранее считаем prefix hash.

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

Также считаем степени P:

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

Тогда hash подстроки можно получить за O(1).

Формула hash подстроки

Будем использовать формат:

s[l...r)

Это значит: l включается, r не включается.

Формула:

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

Код:

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

Базовый класс Hash

using ull = unsigned long long;

const ull P = 13331;

struct Hash {
    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];
    }
};
get(l, r) возвращает hash подстроки s[l...r-1].

Почему value символа = (unsigned char)s[i] + 1?

Символ нужно превратить в число. Можно использовать:

ull x = (unsigned char)s[i] + 1;

Так код работает не только для a...z, но и для разных символов.

+1 нужен, чтобы значение символа не было 0. Нулевые значения в хеше лучше избегать.

Код Рабина-Карпа

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

    vector<int> ans;

    if (m > n) {
        return ans;
    }

    Hash hp(pattern);
    Hash ht(text);

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

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

    return ans;
}

Здесь:

hp — hash pattern
ht — hash text
need — hash всей pattern
ht.get(i, i + m) — hash подстроки text длины m

Полная программа

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

using ull = unsigned long long;

const ull P = 13331;

struct Hash {
    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];
    }
};

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

    vector<int> ans;

    if (m > n) {
        return ans;
    }

    Hash hp(pattern);
    Hash ht(text);

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

    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 = rabin_karp(pattern, text);

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

    return 0;
}

Input / Output

Input

aba
abacaba

Output

0 4

Разбор примера

Дано:

pattern = aba
text = abacaba

Длина pattern:

m = 3

Проверяем подстроки text длины 3:

text[0...3) = aba → подходит
text[1...4) = bac → не подходит
text[2...5) = aca → не подходит
text[3...6) = cab → не подходит
text[4...7) = aba → подходит

Ответ:

0 4

Коллизии

Коллизия — это ситуация, когда две разные строки имеют одинаковый hash.

string A != string B
но hash(A) == hash(B)

Для ULL hash шанс маленький, но теоретически он есть.

Если задача очень строгая, можно использовать double hash или дополнительно проверять строки посимвольно после совпадения хеша.

Безопасный вариант с проверкой строки

Можно сделать так: если hash совпал, дополнительно проверить подстроку через compare.

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

    vector<int> ans;

    if (m > n) {
        return ans;
    }

    Hash hp(pattern);
    Hash ht(text);

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

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

    return ans;
}

Этот вариант полностью защищает от коллизий, но если совпадений хеша много, может стать медленнее.

Double hash

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

hash1 с P1 = 13331
hash2 с P2 = 1000003
строки равны, если совпали оба hash

Код double hash

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() {}

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

    void build(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;

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

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

    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};
    }
};

Рабин-Карп с double hash

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

    vector<int> ans;

    if (m > n) {
        return ans;
    }

    DoubleHash hp(pattern);
    DoubleHash ht(text);

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

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

    return ans;
}
Для большинства задач один ULL hash уже достаточно удобный, но double hash безопаснее.

Поиск нескольких pattern

Рабин-Карп удобно использовать, если нужно искать много строк одинаковой длины.

Идея:

считаем hash всех patterns
кладем их в set
перебираем подстроки text такой же длины
проверяем, есть ли hash в set

Код для нескольких pattern одинаковой длины

vector<int> find_many_patterns(vector<string> patterns, string text) {
    vector<int> ans;

    if (patterns.empty()) {
        return ans;
    }

    int m = patterns[0].size();
    int n = text.size();

    unordered_set<ull> st;

    for (string p : patterns) {
        if ((int)p.size() == m) {
            Hash hp(p);
            st.insert(hp.get(0, m));
        }
    }

    Hash ht(text);

    for (int i = 0; i + m <= n; i++) {
        ull cur = ht.get(i, i + m);

        if (st.count(cur)) {
            ans.push_back(i);
        }
    }

    return ans;
}
Если нужны разные длины patterns, обычно группируют patterns по длине.

Рабин-Карп и KMP

Алгоритм Идея Время
KMP prefix function и откаты O(n + m)
Рабин-Карп hash подстрок O(n + m)

KMP не имеет коллизий. Рабин-Карп проще расширять на сравнение подстрок и много patterns.

Когда использовать Рабин-Карп?

нужно найти pattern в text
нужно быстро сравнивать подстроки
нужно искать много patterns
нужно проверять одинаковые substring
нужен простой hash-based алгоритм

Асимптотика

Операция Время Память
Построить hash pattern O(m) O(m)
Построить hash text O(n) O(n)
Hash одной подстроки O(1) O(1)
Рабин-Карп O(n + m) O(n + m)
Безопасный вариант с compare обычно O(n + m) O(n + m)

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

  • Путать формат [l, r) и [l, r]
  • Забыть посчитать массив pw
  • Использовать int вместо unsigned long long
  • Забыть, что hash может иметь collision
  • Сравнивать подстроки разной длины только по hash
  • В get написать pw[r] вместо pw[r - l]
  • Брать символ как 0, например s[i] - 'a' для буквы a
  • Не обработать случай, когда pattern.size() > text.size()

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

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];
    }
};

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

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

    vector<int> ans;

    if (m > n) {
        return ans;
    }

    Hash hp(pattern);
    Hash ht(text);

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

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

    return ans;
}

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

Рабин-Карп ищет pattern через hash
P = 13331
using ull = unsigned long long
h[i + 1] = h[i] * P + value(s[i])
get(l, r) = h[r] - h[l] * pw[r-l]
если hash подстроки равен hash pattern, нашли occurrence