1 一句话算法
把字符串看成一个
2 问题模型
给定一个字符串
s[l1..r1] 是否等于 s[l2..r2]
朴素比较需要逐字符扫描,单次最坏
字符串哈希把每个子串映射成整数。比较两个子串时,先比较长度,再比较哈希值。哈希不同可以确定字符串不同;哈希相同时只能把它们视为可能相同。若题目不能接受碰撞风险,还需精确比较或改用确定性算法。
不同字符串可能具有相同哈希值。本文使用固定基数
3 核心直觉
十进制数可以用前缀相减截取一段数字:
12345 中截出 345 = 12345 - 12 * 1000
字符串哈希也是同一个模型。把字符当成数字,把字符串当成
如果已经知道前缀 s[1..r] 的哈希值和前缀 s[1..l-1] 的哈希值,就可以把前面的部分减掉,留下 s[l..r]。
4 算法步骤
- 选择进制基数
BASE,本文取131;所有乘加和减法都在模的意义下进行。 - 预处理:
power[i] = BASE^i;prefix[i] = prefix[i-1] * BASE + s[i]。
- 查询子串
[l,r]:
hash(l,r) = prefix[r] - prefix[l-1] * power[r-l+1]
- 判断两个子串是否相等:
- 长度不同,直接不同;
- 长度相同,比较哈希值。
5 算法证明
核心不变量:prefix[i] 表示前缀 s[1..i] 作为
预处理时:
这等价于把旧前缀整体左移一位,再放入新字符,所以不变量成立。
对子串 [l,r],设 len = r-l+1。前缀 s[1..r] 可以拆成两段:
算式推导:
所以查询公式能正确得到该子串的哈希值。它证明的是指纹的计算公式,不是「指纹相等就一定是同一个字符串」。
6 复杂度分析
设字符串长度为
- 预处理时间复杂度:
。 - 单次查询时间复杂度:
。 - 总时间复杂度:
。 - 空间复杂度:
。
7 下标、字符与溢出约定
模板的 get(l, r) 使用 1 起始闭区间。prefix[0] = 0,power[0] = 1;当 l > r 时返回空串哈希 1 <= l <= r <= n。
C++ 按无符号字节值编码字符,并通过 unsigned long long 实现模 & ((1 << 64) - 1) 模拟相同运算;字符串先编码为 UTF-8 字节,因而两个版本对同一输入有一致的哈希值。区间下标也按字节计算,ASCII 字符串中才与字符下标一致。
8 代码实现
模板输入格式:
s
q
l1 r1 l2 r2
...
每个询问判断两个 1-based 子串是否相同,输出 Yes 或 No。
#include <bits/stdc++.h>
using namespace std;
struct StringHash {
using ull = unsigned long long;
static constexpr ull BASE = 131;
vector<ull> prefix;
vector<ull> power;
StringHash(const string &s) {
int n = (int)s.size();
prefix.assign(n + 1, 0);
power.assign(n + 1, 1);
for (int i = 1; i <= n; i++) {
power[i] = power[i - 1] * BASE;
prefix[i] = prefix[i - 1] * BASE + (unsigned char)s[i - 1];
}
}
// 返回 1-based 子串 s[l..r] 的哈希值。
ull get(int l, int r) const {
if (l > r) return 0;
return prefix[r] - prefix[l - 1] * power[r - l + 1];
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
StringHash hs(s);
int q;
cin >> q;
while (q--) {
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
if (r1 - l1 != r2 - l2) {
cout << "No\n";
continue;
}
cout << (hs.get(l1, r1) == hs.get(l2, r2) ? "Yes" : "No") << '\n';
}
return 0;
}
# 字符串哈希:1-based 子串 s[l..r] 的哈希值,O(1) 查询。
# C++ 用 unsigned long long 自然溢出(等价于模 2^64),这里用 MASK 显式模拟回绕,
# 保证与 C++ 数值逐位一致;Python int 本身任意精度,不会溢出。
# get(l, r) 是 1-based 闭区间,l > r(空区间)返回 0,与 C++ 一致。
Seq = list[int] # prefix / power:长度 n + 1 的哈希序列,下标 0 为哨兵
MASK: int = (1 << 64) - 1 # 低 64 位全 1;& MASK 即截断到 64 位,等价无符号回绕
class StringHash:
"""prefix[i] = s[1..i] 的哈希,power[i] = BASE^i。"""
BASE: int = 131 # 进制;C++ 的 static constexpr,Python 放类属性
prefix: Seq
power: Seq
def __init__(self, s: str | bytes) -> None:
data = s.encode("utf-8") if isinstance(s, str) else s
n = len(data)
self.prefix = [0] * (n + 1)
self.power = [0] * (n + 1)
self.power[0] = 1 # BASE^0 = 1
for i in range(1, n + 1):
# C++ 取 (unsigned char)s[i-1],即字节值;Python 用 ord 码点,
# 对 ASCII 两者相同,字符串先转为 UTF-8 字节,非 ASCII 时区间也按字节计算。
self.power[i] = (self.power[i - 1] * self.BASE) & MASK
self.prefix[i] = (self.prefix[i - 1] * self.BASE + data[i - 1]) & MASK
def get(self, l: int, r: int) -> int:
"""子串 s[l..r] 的哈希(1-based 闭区间);空区间返回 0。"""
if l > r:
return 0
# 把前缀 s[1..l-1] 整体左移 r-l+1 位后减掉,减法同样按 64 位回绕。
return (self.prefix[r] - self.prefix[l - 1] * self.power[r - l + 1]) & MASK
if __name__ == "__main__":
import sys
data = iter(sys.stdin.buffer.read().split())
s = next(data, None)
if s is not None:
hs = StringHash(s)
q = int(next(data))
out = []
for _ in range(q):
l1, r1, l2, r2 = (int(next(data)) for _ in range(4))
same = r1 - l1 == r2 - l2 and hs.get(l1, r1) == hs.get(l2, r2)
out.append("Yes" if same else "No")
if out:
print("\n".join(out))
9 测试用例
输入:
abacaba
3
1 3 5 7
1 3 2 4
2 2 6 6
输出:
Yes
No
Yes
解释:
s[1..3] = "aba",s[5..7] = "aba"。s[1..3] = "aba",s[2..4] = "bac"。s[2..2] = "b",s[6..6] = "b"。
10 应用分类详解
字符串哈希的本质是“把一段字符串压成可快速比较的指纹”。看到大量子串比较、重复判断、模式匹配时,可以考虑它。
一、 静态子串相等判断
典型模式: 一个字符串固定,多次询问两个子串是否相同。
识别信号: 出现“多次比较子串”“判断两个区间字符串是否一样”。
核心建模: 预处理前缀哈希,每个子串用 hash(l,r) 表示。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 子串相等查询 | 本页模型 | 长度相同且哈希相同 |
| 最长公共前缀 | 后缀比较类问题 | 二分长度,每次用哈希判等 |
二、 去重与集合统计
典型模式: 给出很多字符串或很多子串,统计不同个数。
识别信号: 出现“不同字符串数量”“去重”“重复子串”。
核心建模: 把每个字符串或子串的哈希值放入 set,再统计集合大小。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 字符串去重 | luogu P3370: 【模板】字符串哈希 | 每个字符串求哈希后排序去重 |
| 重复子串判断 | 最长重复子串 | 二分长度,检查是否有重复哈希 |
三、 模式匹配
典型模式: 在文本串中查找模式串是否出现。
识别信号: 出现“模式串”“文本串”“匹配位置”。
核心建模: 先求模式串哈希,再枚举文本中的等长窗口比较哈希。
四、 回文与循环结构判断
典型模式: 判断一个子串是否回文,或判断字符串是否存在周期。
识别信号: 出现“正反相同”“循环节”“周期”。
核心建模: 回文可比较正向哈希与反向哈希;循环节可比较相邻重复块哈希。
比较不同长度的字符串或把指纹放入集合去重时,建议同时保存 (长度, 哈希值);即使长度一致也仍可能碰撞。
11 经典例题
1. luogu P3370: 【模板】字符串哈希
字符串哈希模板题。对每个字符串求哈希,排序去重后得到不同字符串数量。
2. luogu P4391: [BalticOI 2009] Radio Transmission 无线传输
循环节问题。可以用 KMP,也可以用哈希辅助判断重复块是否一致。
3. luogu P3501
回文相关问题。正反哈希可以快速判断某段是否回文,适合和二分或枚举中心结合。
12 参考
- Rabin-Karp 字符串匹配思想。
13 旧稿提纲
原稿中的基础知识提纲
13.1 旧稿状态
旧稿仅列出「定义、适用场景、复杂度分析、代码模板」四项提纲,没有独立推导或代码。上述四项已经在本页正式教程中展开;此处保留提纲说明,后续维护以正式教程为准。
14 相关题解
暂无相关题解