【算法讲解】字符串 hash 之逆元
作者: | 更新日期:
算法比赛中一个基础的知识点。
本文首发于公众号:天空的代码世界,微信号:tiankonguse
零、背景
Leetcode 算法比赛的题解中,我经常提到一个字符串 hash 这个算法。
上面文章《【算法讲解】字符串 hash》介绍了最初级的 hash 算法。
这篇文章稍微介绍一个高级的字符串 hash, 逆元。
一、模运算
介绍 hash 算法之前,需要先介绍两个模运算的规则。
逆元:如果存在一个 x, 使得 (a * x) % p = 1,则 x 称为 a % p 的逆元,记作a^-1
大整数除法:假设 a/b 可以整除,但是 a 和 b 都是大整数,如何求 (a/b)%p的答案呢?
 (a / b) % p
= (a / b) % p * 1
= (a / b) % p * (b * b^-1) % p
= ((a / b) *( b * b^-1)) % p
= (a * b^-1) % p
由此可以推导出下面的公式。
(a / b) % p = (a * b^-1)%p
一、前缀 hash
正常的 hash 左边是高位,右边是低位,如下:
int pre = 0;
for(int i=1;i<=n;i++){
    int vi = Val[i] - '0;
    pre = (pre * 10 + vi) % mod;
    preHash[i] = pre;
}
如果左边是低位,右边是高危,则写法如下
int pre = 0;
for(int i=0;i<n;i++){
    int vi = Val[i] - '0;
    pre = (pre + vi * pow(10, i, mod)) % mod;
    preHash[i] = pre;
}
二、区间 hash
如果我们想要计算字符串区间val[3,5]的 hash 值,根据上面的算法,可以推论出
pre[5] = v0*10^0 +v1*10^1 + v2*10^2 + v3*10^3+ v4*10^4 + v5*10^5
preHash[5] = pre[5] % mod
pre[2] = v0*10^0 +v1*10^1 + v2*10^2
preHash[2] = pre[2] % mod
目标 = (v3*10^0+ v4*10^1 + v5*10^2) % mod
    = (pre[5] - pre[2]) / 10^3 % mod
    = ((pre[5] % mod) - (pre[2] % mod)) / 10^3 % mod
    = (preHash[5] - prehash[2]) / 10^3 % mod
从上面的公式中可以看到,只需要两个 hash 前缀可以直接相减,之后还需要进行左移若干次。
PS:上面公式正确性很容易证明,先把 10^3 转化为逆元即可证明。
假设一个数字的逆元记为inv(a),则区间子串的 hash 算法如下
ll RangeHash(int l, int r) { // [l, r]
  l--; //(l, r]
  const int lr = r - l;
  const ll R = preHash[r];
  const ll L = preHash[l];
  return (R - L ) * inv(10^lr) % mod;
}
三、逆元计算
根据费马小定理,可以知道 a^(p-1) % p = 1。
对 a^(p-1) 拆出一个 a,即可得出结论 a * a^(p-2) % p = 1。
即 a 的逆元是 a^(p-2)。
故我们可以使用快速幂来计算逆元。
ll qpow(ll x, ll v, ll mod) {
  x = x % mod;
  ll y = 1;
  while (v) {
    if (v & 1) y = y * x % mod;
    x = x * x % mod;
    v >>= 1;
  }
  return y;
}
ll inv(ll x, ll mod) { return qpow(x, mod - 2, mod); }
四、最后
好了,字符串的两种区间 hash 都介绍完了。
两种区间 hash 优化后时间复杂度其实是等价的,大家可以根据比赛的实际情况来选择。
《完》
-EOF-
本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号ID:tiankonguse-code
            本文首发于公众号:天空的代码世界,微信号:tiankonguse
            
            如果你想留言,可以在微信里面关注公众号进行留言。 
        
