谈谈字符哈希
哈希是一种映射,通过一丢丢计算得到的就叫哈希值。
通过对两个数的哈希值比较,如果哈希值相等视为两个数相等。
运用在字符串匹配问题上可大幅度降低时间复杂度,将逐个逐个字符比较的时间复杂度给“吃掉”,比较是否相同的时间复杂度从
-
Q:字符串不是数字,如何计算哈希值?
-
A:通常我们采用的是多项式 Hash 的方法:
代码可参考下面的:
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
// 双哈希类,使用两种不同的哈希函数来减少冲突概率
class DH {
vector<ull> h1, h2; // 存储两种哈希的前缀值
vector<ull> p1, p2; // 存储基数的幂次,用于快速计算子串哈希
const ull b1 = 131, b2 = 13331; // 两种不同的基数
const ull m1 = 1e9+7, m2 = 998244353; // 两种不同的模数
public:
// 初始化哈希预处理数组
void init(string s) {
int n = s.size();
h1.resize(n+1); h2.resize(n+1);
p1.resize(n+1); p2.resize(n+1);
// 初始化基数幂次数组
p1[0] = p2[0] = 1;
// 预处理前缀哈希和基数幂次
for(int i = 1; i <= n; i++) {
// 计算第一种哈希的前缀值
h1[i] = (h1[i-1] * b1 + s[i-1]) % m1;
// 计算第二种哈希的前缀值
h2[i] = (h2[i-1] * b2 + s[i-1]) % m2;
// 计算基数的i次幂
p1[i] = (p1[i-1] * b1) % m1;
p2[i] = (p2[i-1] * b2) % m2;
}
}
// 获取子串s[l..r)的双哈希值
pair<ull, ull> get(int l, int r) {
// 计算第一种哈希的子串哈希值
ull v1 = (h1[r] - h1[l] * p1[r-l] % m1 + m1) % m1;
// 计算第二种哈希的子串哈希值
ull v2 = (h2[r] - h2[l] * p2[r-l] % m2 + m2) % m2;
return {v1, v2};
}
};
// 在字符串s中查找模式串p的所有出现位置
vector<int> find(string s, string p) {
DH ds, dp; // 创建两个双哈希对象,分别用于s和p
// 预处理两个字符串的哈希
ds.init(s);
dp.init(p);
// 获取模式串p的完整哈希值
auto t = dp.get(0, p.size());
vector<int> res; // 存储所有匹配位置的起始索引
// 遍历s中所有可能的起始位置
for(int i = 0; i <= s.size() - p.size(); i++) {
// 比较s的子串和p的哈希值
if(ds.get(i, i + p.size()) == t) {
res.push_back(i); // 如果哈希匹配,记录位置
}
}
return res;
}
前缀优化
如果题目要多次求子串,前面的代码还是不够看,还是会 TLE。
可以用前缀和的思想来优化。
为什么能用前缀优化呢?
我们的哈希公式是一个一个字符从头到尾来计算,对于字符串
任意子串
通过数学变换可得:
代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
// 双哈希类,使用两种不同的哈希函数来减少冲突概率
class DoubleHash {
vector<ull> h1, h2; // 存储两种哈希的前缀值
vector<ull> p1, p2; // 存储基数的幂次,用于快速计算子串哈希
const ull b1 = 131, b2 = 13331; // 两种不同的基数
const ull m1 = 1e9+7, m2 = 998244353; // 两种不同的模数
int n; // 字符串长度
public:
// 构造函数,初始化时直接预处理哈希
DoubleHash(const string& s) {
n = s.size();
h1.resize(n+1); h2.resize(n+1);
p1.resize(n+1); p2.resize(n+1);
// 初始化基数幂次数组
p1[0] = p2[0] = 1;
// 预处理前缀哈希和基数幂次(前缀优化)
for(int i = 1; i <= n; i++) {
// 计算第一种哈希的前缀值(使用模运算优化)
h1[i] = (h1[i-1] * b1 + s[i-1]) % m1;
// 计算第二种哈希的前缀值
h2[i] = (h2[i-1] * b2 + s[i-1]) % m2;
// 预计算基数的幂次(避免重复计算)
p1[i] = (p1[i-1] * b1) % m1;
p2[i] = (p2[i-1] * b2) % m2;
}
}
// 获取子串s[l..r)的双哈希值(带边界检查)
pair<ull, ull> getHash(int l, int r) const {
// 边界检查(前缀优化)
if(l < 0 || r > n || l >= r) return {0, 0};
// 计算第一种哈希的子串哈希值(使用预计算的幂次优化)
ull v1 = (h1[r] - h1[l] * p1[r-l] % m1 + m1) % m1;
// 计算第二种哈希的子串哈希值
ull v2 = (h2[r] - h2[l] * p2[r-l] % m2 + m2) % m2;
return {v1, v2};
}
// 获取字符串长度
int size() const { return n; }
};
// 在字符串s中查找模式串p的所有出现位置(带前缀优化)
vector<int> findPattern(const string& s, const string& p) {
// 边界条件检查(前缀优化)
if(p.empty()) return {};
if(s.size() < p.size()) return {};
// 使用构造函数初始化(避免额外的init调用)
DoubleHash ds(s), dp(p);
// 获取模式串p的完整哈希值
auto targetHash = dp.getHash(0, p.size());
vector<int> res; // 存储所有匹配位置的起始索引
res.reserve(s.size() / p.size()); // 预分配空间(前缀优化)
// 遍历s中所有可能的起始位置
int patternLen = p.size();
for(int i = 0; i <= s.size() - patternLen; i++) {
// 比较s的子串和p的哈希值
if(ds.getHash(i, i + patternLen) == targetHash) {
res.push_back(i); // 如果哈希匹配,记录位置
}
}
return res;
}
// 示例
int main() {
string text = "ababcabcabababd";
string pattern = "ababd";
auto positions = findPattern(text, pattern);
cout << "Pattern found at positions: ";
for(int pos : positions) {
cout << pos << " ";
}
cout << endl;
return 0;
}
通过前缀我们能更好的解决字符串匹配问题。
THE END
我第一篇文章,纪念我的夏令营。