基本原理
即h[i]的公式为
步骤
首先先预处理所有的前缀哈希
把字符串看做p进制的数(不能把字母映射成0),
当p = 131 或者 13331,Q = 2^64在一般情况下,假定不会出现冲突
已知h[r],h[l-1],如何求[l,r]的哈希值?
把h[l]移到与h[r]对齐的部分 h[l-1] * p^(r - l + 1)
h[r] - h[l-1]*p^(r - l + 1)
假设你已经计算了 h[r]
和 h[l-1]
,它们分别代表了以下内容:
h[r]
包含了从1
到r
的所有字符的哈希值。h[l-1]
包含了从1
到l-1
的所有字符的哈希值。
我们想得到子串 [l, r]
的哈希值,可以认为这个哈希值是从 h[r]
中扣除 h[l-1]
所代表的那部分内容后剩下的部分。
为什么需要乘以 Base^{r-l+1}
?
h[l-1]
代表的部分是从1
到l-1
的字符的哈希值。要计算从第l
个字符到第r
个字符的哈希值,需要将h[l-1]
对应的部分移到h[r]
的第l
个字符开始的地方。这就需要将h[l-1]
乘以Base^{r-l+1}
。- 乘以
Base^{r-l+1}
的目的是将原来属于前l-1
个字符的影响“移位”到从l
到r
的位置上,从而能够从h[r]
中正确扣除这一部分。
字符串前缀哈希的优势
可以快速判断请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。
题目
一:
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5+10,Base = 131;
ULL h[N],p[N];
// h[i]前i个字符的hash值
// p数组用于存储 Base 的不同次方值
int n,m;
string s;
ULL Calcular(int l, int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
int main() {
cin >> n >> m;
cin >> s;
p[0] = 1; // Base^0 = 1
h[0] = 0; // 初始哈希值为0
for(int i = 0; i < n; i++){
p[i + 1] = p[i] * Base;// 计算Base的幂次
h[i + 1] = h[i] * Base + s[i];// 计算前缀哈希值
}
while(m--){
int l1,r1,l2,r2;cin >> l1 >> r1 >> l2 >> r2;
if(Calcular(l1,r1) == Calcular(l2,r2)) cout << "Yes\n";
else cout << "No\n";
}
return 0;
}
二:
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 5e3+10,Base = 131;
ULL h[N],p[N];
map<ULL,int> mp;
// h[i]前i个字符的hash值
// p数组用于存储 Base 的不同次方值
// mp用于检测之前是否有完全相同的子串
int n,m;
string s;
void InitiaHash()
{
p[0] = 1; // Base^0 = 1
h[0] = 0; // 初始哈希值为0
for(int i = 0; i < n; i++){
p[i + 1] = p[i] * Base;// 计算Base的幂次
h[i + 1] = h[i] * Base + s[i];// 计算前缀哈希值
}
}
ULL GetHash(int l, int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
//
bool check(int mid){
mp.clear();
int l = 0, r = mid;
while(r <= n - mid){
// 根据mid分为两个自串
mp[GetHash(l + 1,l + mid)] = 1;
if(mp[GetHash(r + 1, r + mid)] == 1) return 1;
l ++, r ++;
}
return 0;
}
int main() {
cin >> n;
cin >> s;
InitiaHash();
int l = 0, r = n;
while(l < r){
// mid表示满足要求的子串的长度
int mid = (l + r + 1) >> 1;
// 找到了但是长度可以增大所以l = mid,增大mid值
if(check(mid)) l = mid;
else r = mid - 1;
}
cout << l << '\n';
return 0;
}
r <= n - mid是什么意思?
r <= n - mid的目的是确保我们在字符串中检查的每一个子串都能完整地取出,并且其长度为 mid
。这个条件是用来限制 r
指针的位置,以确保 r
指向的子串不会超出字符串的范围。
1. r
指针的含义
r
指针表示右半部分 当前正在检查的子串的起始位置。
2. 为什么 r <= n - mid
因为不能让右半部分检查的范围超过n,故r + mid <= n即r <= n - mid
二分查找的思路
二分查找的核心思想是通过缩小搜索范围逐步逼近最终答案。具体到这个问题中:
-
二分查找的初始范围:
l
是二分查找的左边界,初始时为 0(表示最短的子串长度)。r
是二分查找的右边界,初始时为n
(表示最长的子串长度,整个字符串)。
-
当
check(mid)
返回1
时:
如果 check(mid)
返回 1
,说明存在长度为 mid
的子串满足条件。但是,这并不意味着这是最长的满足条件的子串,因为可能存在更长的子串也满足条件。
因此,我们需要继续尝试更大的子串长度,也就是更新 l = mid
,并在下一次二分查找中尝试更大的 mid
值(即右侧的部分)。
为什么check函数是检查[l + 1,l + mid]以及[r + 1,r + mid]
为什么使用 l + 1
而不是 l
?
当你调用 GetHash(l + 1, l + mid)
时:
l + 1
对应的其实是字符串的第l + 1
个字符(注意这里从 1 开始算,因为哈希数组h
是基于 1 索引的)。GetHash(l + 1, l + mid)
会计算从第l + 1
个字符到第l + mid
个字符的哈希值,即这个子串的哈希值。
如果你用 GetHash(l, l + mid)
:
- 由于哈希数组
h[]
是 1 索引的,而字符串s[]
是 0 索引的,GetHash(l, l + mid)
会计算从l
到l + mid - 1
的哈希值。 - 但
l
本身是 0 索引,如果l = 0
,则h[l]
对应的是空字符串的哈希值,这样就会导致计算错误,因为h[0]
并不是第 0 个字符的哈希值,而是一个特殊的初始值。