kz-maxx

String hash

Полиномиальный hash: P = 13331, unsigned long long, prefix hash және substring салыстыру O(1)

String hash деген не?

String hash — string-тен алынатын бір сан. Идея: string-терді әр символ бойынша салыстырмай, олардың hash мәндерін салыстыру.

string → сан
бірдей string → бірдей hash
әртүрлі string → көбіне әртүрлі hash

Мысалы:

"abc" → бір сан
"abd" → басқа сан
Әртүрлі string-тердің hash мәні бірдей болып қалуы мүмкін. Бұл collision деп аталады. Бірақ жақсы hash қолдансақ, шанс өте аз.

String hash не үшін керек?

String hash көбіне substring-терді тез салыстыру үшін қолданылады.

екі substring салыстыру O(1)
бірдей substring табу
pattern-ді text ішінде іздеу
palindrome тексеру
string задачаларды тез шешу

Hash жоқ болса, ұзындығы len екі substring салыстыру O(len). Hash болса, салыстыру O(1).

Полиномиальный hash идеясы

String-ті P негізіндегі сан сияқты қарауға болады.

Мысалы:

s = "abc"

Hash былай көрінеді:

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

Кодта көбіне солдан оңға қарай санаймыз:

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

Неге P = 13331?

P — hash базасы. Көбіне үлкен тақ сан алады.

const ull P = 13331;

13331 string hash үшін жиі қолданылатын база. Практикада жақсы жұмыс істейді.

Басқа базалар да болады: 31, 37, 911382323, 1000003. Бірақ қарапайым ULL hash үшін P = 13331 жақсы вариант.

ULL деген не?

ULL деген:

unsigned long long

C++ ішінде қысқа жазу үшін:

using ull = unsigned long long;

Енді былай жазуға болады:

ull x = 0;
unsigned long long 0-ден 2^64 - 1-ге дейін сақтайды. Егер overflow болса, C++ автоматты түрде 2^64 бойынша қалдық алады. Сондықтан бөлек mod жазбаймыз.

Негізгі формула

Prefix hash санаймыз:

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

Мұнда:

h[i] — алғашқы i символдың hash-і
P — база, мысалы 13331
value(s[i]) — символдың сандық мәні

Символ үшін универсальный вариант:

(unsigned char)s[i] + 1

+1 керек, себебі символдың мәні 0 болып кетпегені жақсы.

P дәрежелері

Substring hash-ін тез алу үшін алдын ала 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;
    }
}

Substring hash

Мына substring hash-ін алғымыз келеді:

s[l...r-1]

Яғни l кіреді, r кірмейді.

Формула:

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

pw массивімен:

ull get(int l, int r) {
    return h[r] - h[l] * pw[r - l];
}
[l, r) форматы өте ыңғайлы: l кіреді, r кірмейді.

Неге формула жұмыс істейді?

h[r] — алғашқы r символдың hash-і.

Оның ішінде бар:

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) string-тің s[l...r-1] substring hash-ін қайтарады.

Екі substring қалай салыстырылады?

Бір string ішінде екі substring берілсін:

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

Егер ұзындықтары тең және hash-тері тең болса, substring-терді бірдей деп санаймыз.

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

Substring салыстыру мысалы

String:

abacaba

Substring-тер:

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

Олардың hash-тері тең, демек substring-тер бірдей.

Толық программа: substring салыстыру

#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] inclusive керек болса

Кейде мына формат ыңғайлы:

s[l...r]

Онда функция былай болады:

ull get_inclusive(int l, int r) {
    return h[r + 1] - h[l] * pw[r - l + 1];
}
Ең маңыздысы — форматтарды араластырмау. Не барлық жерде [l, r), не барлық жерде [l, r].

Hash арқылы pattern іздеу

pattern string-і text ішінде қай позицияларда кездесетінін табу керек.

Идея:

pattern hash санаймыз
text hash санаймыз
text ішіндегі ұзындығы m барлық substring қараймыз
олардың hash-ін pattern hash-пен салыстырамыз

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

Кіші латын әріптері үшін hash

Егер string тек a...z әріптерінен тұрса, былай алуға болады:

s[i] - 'a' + 1

Сонда build ішінде:

h[i + 1] = h[i] * P + (s[i] - 'a' + 1);
Бірақ универсальный вариант — (unsigned char)s[i] + 1. Ол тек кіші ағылшын әріптері үшін ғана емес, басқа символдарға да жұмыс істейді.

Palindrome hash

Hash арқылы substring palindrome екенін тексеруге болады.

Идея:

original string hash санаймыз
reversed string hash санаймыз
substring palindrome, егер оның hash-і reverse substring hash-іне тең болса

Palindrome тексеру коды

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) форматы қолданылады. Мысалы, бүкіл string — [0, n).

Double hash

Бір ULL hash көбіне жақсы жұмыс істейді, бірақ теориялық collision мүмкін. Collision шансын азайту үшін екі түрлі hash қолдануға болады.

hash1 база P1 арқылы
hash2 база P2 арқылы
string-тер тең, егер екі hash те тең болса

Мысалы:

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

Қарапайым double ULL 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(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 hash жеткілікті болады, бірақ double hash қауіпсіздеу.

Hash mod арқылы ма, әлде ULL ма?

Вариант Қалай жұмыс істейді? Плюс
ULL overflow арқылы 2^64 бойынша қарапайым және тез
mod жай сан mod бойынша қалдық аламыз көбірек бақылауға болады
double hash екі тәуелсіз hash collision шансын азайтады

Негізгі шаблон: 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")

Асимптотика

Операция Уақыт Память
String hash құру O(n) O(n)
Substring hash алу O(1) O(1)
Екі substring салыстыру O(1) O(1)
Pattern-ді text ішінде табу O(n + m) O(n + m)

String hash қашан қолданылады?

substring-терді көп рет салыстыру керек
бірдей substring табу керек
pattern-ді text ішінде тез іздеу керек
palindrome тексеру керек
substring query үшін O(1) керек

Типтік қателер

  • [l, r) және [l, r] форматтарын шатастыру
  • pw массивін санауды ұмыту
  • unsigned long long орнына int қолдану
  • Hash collision болуы мүмкін екенін ұмыту
  • Ұзындығы әртүрлі substring-терді тек hash бойынша салыстыру
  • Символды 0 қылып алу, мысалы s[i] - 'a', мұнда a үшін 0 болады
  • Бір string үшін әртүрлі 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) string s[l...r-1] hash-ін береді