Contents

字符串专题(1)-字符串Hash

字符串hash 是指将一个字符串S 映射为一个整数,使得该整数可以尽可能唯一地代表字符串 S。那么在一定程度上,如果两个字符串转换成的整数相等,就可以认为这两个字符串相同。

此前, 对于一个只有大写字母的字符串, 是把它的每一个字符当作一个26进制的数位, 整个字符串形成一个26进制数, 那么Hash就是把它转为10进制数即可. 然而, 当字符串太长时, 这个数字会大到难以承受. 一个常用方法是利用模数, 也即取某个素数 mod 的模.

H[i]=H[i-1]x26+index(str[i]) // (i: from 0 to H.length-1)
H[i]=(H[i-1]x26+index(str[i])) % mod // (i: from 0 to H.length-1)

但这又会产生另外的问题,也就是可能有多个字符串的 hash 值相同,导致冲突。不过幸运的是,在实践中发现,在 int 数据范围内,如果把进制数设置为一个 $10^7$ 级别的素数p(例如10000019),同时把mod 设置为一个 $10^9$ 级别的素数(例如1000000007),如下所示,那么冲突的概率将会变得非常小 H[i] = (H[i-1] x p +index(str[i])) % mod 来看一个问题:给出N个只有小写字母的字符串,求其中不同的字符串的个数。对这个问题,如果只用字符串 hash 来做,那么只需要将N个字符串使用字符串 hash函数转换为N个整数,然后将它们排序去重即可,代码如下(当然也可以用 set 或者 map 直接一步实现,但是速度比字符串 hash 会慢一点点):

long long hashFunc(string str){
    long long H = 0;    // in case of excessing
    for(char c: str){
        H = (H*P+(c-'a'))%MOD;
    }
    return H;
}

最长公共子串

来看一个问题:输入两个长度均不超过1000的字符串,求它们的 最长公共子串 的长度。例如字符串"ILoveYou"与"YouDontLoveMe"的最长公共子串是"Love"而不是"You",因此输出4。(注意:子串必须连续)

子串Hash值

在经过上述推导后, 我们得到了一个冲突概率小的字符串哈希函数, 我们依次得到了 H[0], H[1], … , H[n-1]. 其中 H[n-1] 即为最后的字符串 str 的哈希值, 实际工程并未使用数组存储中间值, 而是使用不断覆盖的方法. 现考虑一个问题, 如何求 str 的子串 str[i ... j] 的Hash值, 一个方法就是不考虑已经得到的 H[] 当作新的字符串再调用函数求解. 然而这样势必失去了Hash的时间优势. 是否可以仅凭借 H[] 得到 str[i ... j] 的 Hash 值呢?

考察 H[i], 他表示了字符串 str[0 ... i] 的 Hash 值, 假如不考虑MOD操作, 回到原始的进制思想, 则有 $H[i]=H[i-1]\times p+index(str[i])$ 而 $H[i … j]$ 就表示 str[i … j] 从 p进制转为10进制的结果 $ H[i, j] = str[i] \times p^{(j-i)} +…+ str[i] \times p^{(j-j)}$ 又有: $H[j] = H[j-1]*p+str[j] $ 继续迭代到第i步, 就得到 $H[j]=H[i-1]*p^{(j-i+1)}+H[i, j]$ 于是就有: $H[i, j] = H[j]-H[i-1]*p^{j-i+1}$ 最后再加上取模操作,同时为了保证 H[i, j] 为正数, 就得到:

$$H[i,j] = (H[j]-H[i-1]p^{j-i+1}) % mod$$ $$H[i,j] = H[j]% mod-(H[i-1]p^{j-i+1}) % mod$$ $$H[i,j] = H[j]% mod-(H[i-1]% mod)(p^{j-i+1}% mod) % mod$$ $$H[i,j] = H[j]-H[i-1](p^{j-i+1}% mod)% mod$$ $$H[i,j] = (H[j]-H[i-1]*(p^{j-i+1}% mod) % mod+mod)% mod$$

于是, str 的 H[] 通过 hashFunc() 可以得到, 而 (p^{j-i+1}% mod) 也可以单独计算得到, 设为 P[], 易知它独立于字符串, 在 P 和 mod 确定时是一个常数数组.

题解

以上讨论了计算字符串Hash值后, 利用中间值得到子串哈希值的方法. 接下来, 就是问题求解, 我们遍历两个字符串的每一个子串, 计算哈希值并存储, 然后比较它们之间是否有相同的值。

// @FileName:     substrmxl.cpp
// @CreateTime:   2023/04/08 14:01:16
// @Author:       Rainbow River

#include <iostream>
#include <string>
#include <vector>

using namespace std;
const int MOD = 1e9+7;
const int P = 1e7+19;
const int MAXLEN = 1010;    // 字符串最大长度
typedef long long LL;
LL powerP[MAXLEN];         // 计算P^[i]%MOD的值
struct hashnode{
    LL hashval;     // 子串哈希值
    int i, j;       // 子串下标值
    hashnode(int i, int j, int v){
        this->i = i;
        this->j = j;
        this->hashval = v;
    }
};

void hashFunc(LL H[], string str){
    H[0] = str[0];  // 自定义一个值
    for(int i=1; i<str.length(); i++){
        H[i] = (H[i-1]*P+str[i]) % MOD;
    }
}

void fillPowerP(){
    powerP[0] = 1;
    for(int i=1; i<MAXLEN; i++)
        powerP[i] = (powerP[i-1]*P)%MOD;
}

// 计算 H[i, j]
LL substrHash(LL H[], int i, int j){
    if(i == 0) return H[j];
    else return (H[j]-(H[i-1]*powerP[j-i+1])%MOD+MOD)%MOD;
}

// 计算所有子串的的 Hash 值, 并保存 hash-value, i, j
void hashStrCal(LL H[], int n, vector<hashnode>& res){
    for(int i=0; i<n; i++){
        for(int j=i; j<n; j++){
            res.push_back(hashnode(i, j, substrHash(H, i, j)));
        }
    }
}

int main(){
    // 1. 输入两个字符串
    string str1, str2;
    cin >> str1 >> str2;
    // 2. 计算 str1 和 str2 的 Hash 值数组
    int n1 = str1.length(), n2 = str2.length();
    LL *H1 = new LL[n1], *H2 = new LL[n2];
    fillPowerP();
    hashFunc(H1, str1);
    hashFunc(H2, str2);
    // 3. 计算 str1 和 str2 的 所有子串的Hash
    vector<hashnode> res1, res2;
    hashStrCal(H1, n1, res1);
    hashStrCal(H2, n2, res2);
    // 4. 比较 hash 值, 迭代得到最长的公共字符串
    int mxl=0;
    string mxsubstr;
    for(int i=0; i<res1.size(); i++){
        for(int j=0; j<res2.size(); j++){
            int l1 = res1[i].j - res1[i].i + 1;
            int l2 = res2[j].j - res2[j].i + 1;
            if(res1[i].hashval == res2[j].hashval && l1==l2 && l1 > mxl){
                mxl = l1;
                mxsubstr = str1.substr(res1[i].i, l1);
            }
        }
    }
    cout << mxl << endl;
    cout << mxsubstr << endl;
}

/*
a a
a ba
a b
aa ba
icanhearyou iamearyel
*/