字符串哈希:定义一种将字符串映射到一个整型哈希值的函数 f(),我们希望这个函数可以方便地帮我们判断两个字符串是否相等。

字符串哈希函数

引例

(1001)2 这个二进制数转为十进制,按位拆分得到 (1001)2=123+022+021+120

(1234)p 这个 p 进制数转为十进制,按位拆分得到 (1234)p=1p3+2p2+3p1+4p0

根据上面的例子,我们可以将字符串看为一个 p 进制数,将它转为十进制便是它的哈希值。不过由于指数爆炸,字符串长度稍长一点,转换得到的数字大小就会变得非常巨大,因此我们还需要对结果取模。

定义

对于一个长度为 l 的字符串 s 来说,定义多项式哈希函数:

f(s)=i=1lsi×pli(modm)

这种多项式形式的哈希函数,字符串最左侧为多项式高位,最右侧为低位,相反的实现也是同样可行的,本文不讲另一种。

该哈希函数有两个参数,分别是底数 p 和模数 m. 对于 p 可以任意选择,对于 m 需要选择一个素数,且至少要比最大的字符要大。

下面给出一些常用的选择,不过也要小心出题人把常用值给卡掉:

  • p=131,13331,449,233
  • m=109+7,998244353,998244853,436522843,264

其中 264 便是自然溢出法了,选择 unsigned long long int 数据类型即可,范围 [0,264).

另外,因为字符串哈希处理冲突的开销较大,因此我们是不处理冲突的。所以用哈希处理字符串的题目,能不能过还是很玄学的。如果实在被卡掉了,可以试一下选择两个模数进行双哈希,这样的话哈希函数的值域就能扩大到两者之积,可以降低错误率。

代码

typedef long long ll;
constexpr ll P = 449, MOD = 436522843;

ll get_hash(string &s)
{
    ll res = 0;
    for (int i = 0; i < s.size(); i++)
        res = (res * P % MOD + s[i]) % MOD;
    return res;
}

子串哈希

原理

单次计算一个字符串的哈希值复杂度是 O(n),其中 n 为串长。如果需要多次询问一个字符串的子串的哈希值,每次重新计算效率非常低下。此时就要用到子串这个性质,减少重复计算。

字符串哈希具有类似前缀和的性质,如果预处理了该字符串的每个前缀子串的哈希值,那么就可以在 O(1) 内得到该字符串所有子串的哈希值。不过和前缀和不同的是,并不能直接相减,而是要乘上一个“偏移量”再减。

具体来说,将前缀哈希值储存在 hi 中,hi 代表从第 1 位到第 i 位的前缀子串的哈希值。如果要求的子串左端点为第 l 位,右端点为第 r 位,则子串的哈希值 h 的计算公式:

h=hrprl+1hl1

代码实现时,为了加快效率,将 p 的幂次预处理好保存到数组内。

演示

我们用一个可视化的例子来演示:

(abcdef)p=ap5+bp4+cp3+dp2+ep1+fp0(abc)p=ap2+bp1+cp0(abcde)p=ap4+bp3+cp2+dp1+ep0(de)p=dp1+ep0

我们已知的是:

  • 第一行:abcdef 的哈希值(仅作对比参考,实际算法是用不上的)
  • 第二行:02 子串的哈希值 h2.
  • 第三行:04 子串的哈希值 h4.

我们要求的是:

  • 第四行:34 字串的哈希值。

通过比对我们可以发现:(de)p=h4p2h2. 与上面式子的计算方式一致。

代码

struct StrHash
{
    int len, base, mod;
    vector<int> p, h;
    void init(const string &s, int base, int mod)
    {
        len = s.size();
        this->base = base;
        this->mod = mod;
        p.resize(len + 1);
        h.resize(len + 1);
        p[0] = 1;
        for (int i = 1; i <= len; i++)
            p[i] = p[i - 1] * base % mod;
        h[0] = s[0] % mod;
        for (int i = 1; i < len; i++)
            h[i] = (h[i - 1] * base % mod + s[i]) % mod;
    }
    int get(int l, int r)
    {
        if (l <= 0)
            return h[r];
        return (h[r] - h[l - 1] * p[r - l + 1] % mod + mod) % mod;
    }
};

允许 k 次失配的字符串匹配

问题

给定长为 n 的原串 s,以及长度为 m 的模式串 p,要求查找原串中有多少子串与模式串匹配。ss 匹配,当且仅当 ss 长度相同,且最多有 k 个位置字符不同。其中 1n,m1060k5.

解法

该问题无法使用 KMP 来解决,但是可用哈希 + 二分完成。思路如下:

枚举所有可能匹配的子串,假设现在枚举的子串为 s,通过哈希 + 二分可以快速找到 sp 第一个不同的位置。之后不考虑这个失配位置及之前的部分,继续查找下一个失配位置。这样的过程最多发生 k 次。

时间复杂度 O(m+nklogm)

代码

a 为模式串,b 为子串,该函数返回子串是否能匹配上模式串。

/* 依赖上文的StrHash结构体 */
bool check(StrHash &a, StrHash &b, int toler)
{
    if (a.len < b.len) // make a >= b
        return check(b, a, toler);
    int la = a.len, lb = b.len;
    for (int i = 0; i <= la - lb; i++)
    {
        int err = 0, pos = 0;
        while (err <= toler && pos < lb)
        {
            int l = pos, r = lb - 1;
            while (l < r)
            {
                int mid = (l + r) / 2;
                if (a.get(i + pos, i + mid) == b.get(pos, mid))
                    l = mid + 1;
                else
                    r = mid;
            }
            if (a.get(i + pos, i + l) != b.get(pos, l))
                err++;
            pos = l + 1;
        }
        if (err <= toler)
            return true;
    }
    return false;
}

回文判定

回文判定的相关问题,也可以用字符串哈希来解决,核心思路就是预处理出原串的哈希和逆序串的哈希。

不过这种问题 manacher 算法往往也能完成,并且不会被卡。

比较典型的例题可以是:Codeforces Global Round 7 - D2