【数据结构】Hash 学习笔记

Hash 表

Hash 表又称散列表,哈希表,其核心思想为映射。通常用一个整数来表示某种复杂信息。

字符串 Hash

下面介绍的方法可以将一个任意长度的字符串映射为一个非负整数:

取两个固定值 \(P\)\(M\),把字符串看作 \(P\) 进制数(每一位的值为 char 类型自动转换值即可),将其转化为十进制后对 \(M\) 取模,就可以得到一个非负整数。当 \(M\) 足够大而且 \(P\) 选择得当时,哈希冲突概率基本为零。

下面是字符串哈希的代码实现:

洛谷 P3370 【模板】字符串哈希

参考代码:

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

unsigned get_hash(string& s) {
    unsigned ret = 0;
    for (char& c : s) {
        ret = ((ret * 0x66ccff) + c); 
        // 取 P = 0x66ccff, M = unsigned int 最大值
    }
    return ret;
}

unsigned a[10005], n, ans;

signed main() {
    ios::sync_with_stdio(0);
    #ifndef ONLINE_JUDGE
    clock_t t0 = clock();
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    #endif

    // Don't stop. Don't hide. Follow the light, and you'll find tomorrow. 

    cin >> n;
    for (int i = 1;i <= n;i++) {
        string s;
        cin >> s;
        a[i] = get_hash(s);
        
    }

    sort(a + 1, a + 1 + n);
    for (int i = 1;i <= n;i++) {
        if (i == 1 || a[i] != a[i - 1]) ans++;
    } // 判断有多少不同的 hash 值
    cout << ans << endl;

    #ifndef ONLINE_JUDGE
    cerr << "Time used:" << clock() - t0 << "ms" << endl;
    #endif
    return 0;
}

前缀 hash 与区间 hash

记字符串 \(S\) 的 hash 值为 \(H(S)\),在其末尾添加一个字符 \(c\),则 hash 值会变为 \(H(S + c) = ((H(S) << 1) + c) \mod M\),其中 \(\mod M\) 是对 \(M\) 取余运算,\(<<\) 是在 \(P\) 进制下的左移运算,\(x << i\) 对应数学运算中的 \(x \times P^i\)

\(S\) 末尾添加一个字符串 \(T\),hash 值则会变为 \(H(S+T)=(H(S)<<(len(T))+H(T))\)

因此,我们算出 \(S\) 的前缀哈希值 \(S[1,i]=F(i)=(F(i-1)<<1)<<+S[i]\),然后就可以在 \(O(1)\) 时间内求出某区间内的哈希值 \(H(S[l,r])=F(r)-(F(l-1)<<(r-l+1))\)

另外,分享一个技巧,这样读入字符串可以让下标从 1 开始。

string s=" ", a;
cin >> a;
s+=a;

参考代码实现:AcWing 138. 兔子与兔子

字符串匹配

计算出模式串的 hash 值,再计算主串中每个长度等于模式串的子串的 hash 值,若相等则匹配。

参考代码:

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

typedef unsigned long long ull;

const int N = 1e6 + 5;

string s = " ";

ull get_hash(string& s) {
    ull ret = 0;
    for (char& c : s) {
        ret = ((ret * 131) + c); // 取 P = 131, M = unsigned long long 最大值
    }
    return ret;
}

ull f[N], p[N], n, m, ans;

ull get_hash(int l, int r) {
    return f[r] - f[l - 1] * p[r - l + 1];
}

signed main() {
    ios::sync_with_stdio(0);
    #ifndef ONLINE_JUDGE
    clock_t t0 = clock();
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    #endif

    // Don't stop. Don't hide. Follow the light, and you'll find tomorrow. 

    string a, t; // 主串和模式串
    cin >> a >> t;
    n = a.length();
    s += a;
    p[0] = 1;
    for (int i = 1;i <= n;i++) {
        f[i] = f[i - 1] * 131 + s[i]; // f[i] = get_hash(s[1,i])
        p[i] = p[i - 1] * 131;        // p[i] = pow(131,i)
    }

    ull hasht = get_hash(t); // 模式串的 hash 值
    int len_t = t.length();
    // cout << get_hash(t) << endl;
    for (int i = 1;i <= n - len_t + 1;i++) {
        if (get_hash(i, i + len_t - 1) == hasht) {
            // cout << i << ' ';
            ans++;
        }
    }

    cout << ans << endl;

    #ifndef ONLINE_JUDGE
    cerr << "Time used:" << clock() - t0 << "ms" << endl;
    #endif
    return 0;
}

例题

[ABC284F] ABCBAC

题目可以转化为:将字符串划分为三部分 \(S1+S2+S3=S\),而且 \(len(S1)+len(S3)=len(S2)=N\),且 \(S1+S3=\text{reserve}(S2)\),输出翻转后的 \(S2\)\(i=len(S1)\)

直接枚举 i,就可以得到 \(S1\)\(S3\),:\(H(S1+S3)=(H(S1)<<(n-i)+H(S3))\),用前缀哈希来计算 \(H(S1)\)\(H(S3)\)

计算翻转后的 \(S2\) 的 hash 值,直接用翻转后的 \(S\) 的前缀哈希就可以求出来。

注意:本题卡自然溢出(\(M=2^{64}\)),可以用别的模数。(数据太强了,20081009,0x66ccff,0xccfccf250都被卡掉了。)

参考代码:

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

const int N = 2e6 + 5, MOD = 998244353;

int n;
string s = " ";

int hash1[N], hash2[N], p[N];

int get_hash1(int l, int r) {
    return (hash1[r] - (hash1[l - 1] * p[r - l + 1]) % MOD + MOD) % MOD;
}

int get_hash2(int l, int r) {
    return (hash2[l] - (hash2[r + 1] * p[r - l + 1]) % MOD + MOD) % MOD;
}

signed main() {
    ios::sync_with_stdio(0);
    #ifndef ONLINE_JUDGE
    clock_t t0 = clock();
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    #endif

    // Don't stop. Don't hide. Follow the light, and you'll find tomorrow. 
    cin >> n;
    string a;
    cin >> a;
    s += a;
    p[0] = 1;
    for (int i = 1;i <= n * 2;i++) {
        hash1[i] = (hash1[i - 1] * 131 + s[i]) % MOD; // 自定义模数
        p[i] = (p[i - 1] * 131) % MOD;
    }
    for (int i = 2 * n;i--;) hash2[i] = (hash2[i + 1] * 131 + s[i]) % MOD; // 逆序前缀哈希
    // cout << p[4] << endl;
    for (int i = 0;i <= n;i++) {
        int hashs1 = get_hash1(1, i), hashs2 = get_hash2(i + 1, i + n), hashs3 = get_hash1(i + n + 1, n + n);
        if ((hashs1 * p[n - i] % MOD + hashs3 % MOD) % MOD == hashs2) {
            for (int j = i + n;j > i;j--) cout << s[j];
            cout << endl << i << endl;
            return 0;
        }
    }
    cout << -1 << endl;

    #ifndef ONLINE_JUDGE
    cerr << "Time used:" << clock() - t0 << "ms" << endl;
    #endif
    return 0;
}

AcWing 139. 回文子串的最大长度

和上一题类似,本题也需要求一遍字符串翻转后的 hash 值。
枚举每个点作为回文串的中点,二分求出构成回文串的最大半径,判断方法为左边的逆序哈希值是否和右边的哈希值相等。
枚举的复杂度为 \(O(n)\),二分的复杂度为 \(O(\log n)\),总的时间复杂度为 \(O(n \log n)\),可以通过这题。
参考代码:去 AcWing 查看

posted @ 2023-07-12 16:28  蒟蒻OIer-zaochen  阅读(25)  评论(0编辑  收藏  举报