【字符串】总结 1:前言 & 字符串 Hash
前言
最近一直在做字符串的题,因此来写篇总结梳理一下【字符串】部分的知识点。
本文采用符号及解释
- \(\Sigma\):字符集。例如全小写字母(\(|\Sigma|=26\)),数字符集(\(|\Sigma|=10\));
- \(|s|\):字符串 \(s\) 的长度;
- \(s[i\sim j]\):字符串 \(s\) 中下标 \([i,j]\) 对应的子串;
- \(\varnothing\):空串。
其它符号及解释在文中解释。
char 数组定义字符串
在 C++ 中可采用字符数组 char s[N];
来创建一个长度为 \(N\) 的字符串。
输入可使用 scanf("%s", s);
或者 scanf("%s", s + 1);
,前者默认下标从 \(0\) 开始,后者下标则从 \(1\) 开始,具体看个人使用习惯,怎么方便怎么来。
对于使用 char 数组定义的字符串,常用的函数有 strlen()
函数。传入参数为字符数组(例如 s
或 s + 1
),返回该字符串长度。
string
string 是 C++ 标准库自带的一种存储字符串的类型,支持以下操作或函数(以下字母默认为 string 类型):
s + t
:将 \(s\) 和 \(t\) 相连接后形成的字符串;s = t
:将 \(s\) 赋值为 \(t\);s[i]
:访问 \(s\) 中下标为 \(i\) 的字符;s.size()
:返回 \(s\) 的长度;s.substr(i, len)
:截取 \(s\) 中下标从 \(i\) 开始,长度为 \(len\) 的子串;s.clear()
:清空 \(s\);- 可正常使用比较逻辑符,默认按字典序排序大小。
此外,string 还可以很方便地用各种函数支持各种操作。
字符串 Hash
对于字符串 \(s\) 及整数 \(z\),对应关系 \(f:s\to z\) 被称作 Hash 函数,该函数的作用是为我们快速比较两个字符串是否相等提供便利。
为表述方便,下文统一采用 \(H(s)\) 表示字符串 \(s\) 的 Hash 函数值。
一般在计算 \(H(s)\) 时,我们可以把 \(s\) 看作一个 \(P\) 进制数,Hash 函数值即为该 \(P\) 进制数模 \(M\) 的值,即有(默认下标从 \(1\) 开始,且其中 \(s[i]\) 是在原字符上经过一定处理得到的一个整数):
一般可取 \(P=131\) 或 \(P=13331\),而 \(M=2^{64}\),那么此时可以用 C++ 中的 unsigned long long
类型来存储 Hash 函数的值,相当于用自然溢出的特性规避了取模运算带来的时间开销。
在判断两个字符串 \(s\) 和 \(t\) 是否相等时,具体而言,如果 \(H(s)\ne H(t)\) 时,定有 \(s\ne t\);而如果 \(H(s)=H(t)\) 时,\(s\) 和 \(t\) 有很大概率相等。
这里的意思其实就是对于两个不同的字符串,它们的 Hash 函数值有很低的概率相等,这种情况被称作 Hash 冲突。
对于普通的单 Hash,往往能够找到适宜的卡 Hash 办法卡掉,此时可以采用双 Hash 来降低冲突概率。
具体操作就是用不同的 Hash 函数计算方法算出同一个字符串 \(s\) 的两个 Hash 函数值 \(H_1(s)\) 和 \(H_2(s)\)。那么对于两个字符串 \(s\) 和 \(t\),若 \(H_1(s)\ne H_1(t)\) 或 \(H_2(s)\ne H_2(t)\),则这两个字符串一定不相等;若 \(H_1(s)=H_1(t)\) 且 \(H_2(s)=H_2(t)\),则我们就认为 \(s=t\)。
具体实现可参考代码:
- 单 Hash:
const int N = _______, P = 13331;
unsigned long long H[N];
string s;
void Hash(const string& s)
{
for(int i = 1; i <= s.size(); i ++)
H[i] = H[i - 1] * P + (int)s[i];
}
- 双 Hash:
const int N = _______, P = 13331;
const int mod1 = _______, mod2 = _______;
unsigned long long H1[N], H2[N];
string s;
void Hash1(const string& s)
{
for(int i = 1; i <= s.size(); i ++)
H1[i] = (H1[i - 1] * P + (int)s[i]) % mod1;
}
void Hash2(const string& s)
{
for(int i = 1; i <= s.size(); i ++)
H2[i] = (H2[i - 1] * P + (int)s[i]) % mod2;
}
值得注意的是,上述代码中 H
、H1
及 H2
数组存储的是字符串 \(s\) 的每个前缀的 Hash 值,这样做便于我们快速计算 \(s\) 的子串的 Hash 值。
对于字符串 \(s\),我们可以在 \(O(|s|)\) 内求得其所有前缀的 Hash 函数值 \(H(i)\)(默认下标从 \(1\) 开始,其代表该前缀的最后一个字符的下标为 \(i\))。
(以下推导省略模数)那么有 \(\displaystyle H(i)=\sum^{i}_{j=1}s[j]\times P^{i-j}\)。
且有 \(\displaystyle H(s[l\sim r])=\sum^{r}_{j=l}s[j]\times P^{r-j}\)。
比较两式可推得公式:
在预处理了 \(P\) 的幂次后,我们便可以 \(O(1)\) 查询子串的 Hash 值了。