【字符串】总结 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() 函数。传入参数为字符数组(例如 ss + 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]\) 是在原字符上经过一定处理得到的一个整数):

\[\boxed{H(s)=\left(\sum^{|s|}_{i=1}s[i]\times P^{|s|-i}\right)\mod M} \]

一般可取 \(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\)

具体实现可参考代码:

  1. 单 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];
}
  1. 双 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;
}

值得注意的是,上述代码中 HH1H2 数组存储的是字符串 \(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}\)

比较两式可推得公式:

\[\boxed{H(s[l\sim r])=H(r)-H(l-1)\times P^{r-l+1}} \]

在预处理了 \(P\) 的幂次后,我们便可以 \(O(1)\) 查询子串的 Hash 值了。

posted @ 2025-07-17 15:33  cold_jelly  阅读(16)  评论(0)    收藏  举报