哈希前置知识请戳这里-> 哈希绪论
昨天我们对哈希的基础知识有了一定的了解,并已经知道了如何求子串、拼接子串的哈希值,今天我们就这两个操作分析一些基础例题,加深理解和掌握。
例题一:子串查找
LOJ #103. 子串查找
显然这是一道kmp算法的模板题
朴素的做法是枚举文本串的每一个位置作为模式串开始比较的位置。设枚举到主串的位置是
i
i
i,
N
N
N为主串(即输入的第一行)的长度,
M
M
M为模式串(即输入的第二行)的长度,
s
a
sa
sa为主串,
s
b
sb
sb为模式串,则当且仅当
s
a
sa
sa第
i
i
i到第
i
+
M
−
1
i+M-1
i+M−1与模式串一一匹配,才能使
a
n
s
+
1
ans+1
ans+1。这样在最坏情况下(比如
s
a
、
s
b
sa、sb
sa、sb所有字符均为
a
a
a),复杂度达到了
O
(
N
M
)
O(NM)
O(NM),是不可接受的。现在我们考虑用哈希处理,那么可以将第二步比较的操作复杂度降低到
O
(
1
)
O(1)
O(1),因此只要先预处理出
s
a
sa
sa的前缀哈希表,则当我们枚举每一个位置
i
i
i时,利用前缀表获得区间
[
i
,
i
+
M
−
1
]
[i,i+M-1]
[i,i+M−1]子串的哈希值,再与
s
b
sb
sb的哈希值相比较即可,总体复杂度就降低到了
O
(
N
+
M
)
O(N+M)
O(N+M),已经和kmp算法一样优秀了就是常数有点大
下面来看代码:
#include<stdio.h>
#include<string.h>
#define ll long long
#define N 1000000007
#define M 14371003
#define max 1000005
char s[max], ss[max];
int a[max], p[max];
int main()
{
scanf("%s%s", s + 1, ss + 1);
int len = strlen(ss + 1), ans = 0, cnt = 0, l = strlen(s + 1);
if (len > l) { printf("0"); return 0; }
a[1] = s[1], p[0] = 1, p[1] = M;
for (int i = 1; i <= len; i++)
ans = ((ll)ans * M + ss[i]) % N;//获得sb的哈希值
for (int i = 2; i <= l; i++)
{
a[i] = ((ll)a[i - 1] * M + s[i]) % N;
p[i] = (ll)p[i - 1] * M % N;
}
for (int i = 1; i <= l + 1 - len; i++)
if (((a[i + len - 1] - (ll)a[i - 1] * p[len]) % N + N) % N == ans)cnt++;
//查询子串哈希值公式
printf("%d", cnt);
return 0;
}
接下来我们看两道道有难度的题目
例题二:字符串的删除操作
LOJ #2823. 「BalticOI 2014 Day 1」三个朋友
这道题我们很容易想到利用逆向思维解决:枚举每一个删除的位置,然后检查删除后的字符串是否由两个完全相同的字符串构成,而判断字符串是否相同就可以利用哈希的思想了。
首先,输入的字符串长度一定要是奇数(删掉一个字符是长度是偶数),否则直接输出无解。稍微修改一下昨天拼接字符串的公式就得到了在区间
[
l
,
r
]
[l,r]
[l,r]删除位置
x
x
x对应的字符后子串的哈希值公式:
a
n
s
=
g
e
t
(
l
,
x
−
1
)
∗
b
a
s
e
r
−
x
+
g
e
t
(
x
+
1
,
r
)
ans=get(l,x-1)*base^{r-x}+get(x+1,r)
ans=get(l,x−1)∗baser−x+get(x+1,r)相当于是把
[
l
,
x
−
1
]
、
[
x
+
1
,
r
]
[l,x-1]、[x+1,r]
[l,x−1]、[x+1,r]两段字符串拼接起来。然后就是需要注意若删除的位置是在原字符串前一半位置,则比较
h
a
s
h
(
[
1
,
x
−
1
]
+
[
x
+
1
,
l
e
n
2
]
)
hash([1,x-1]+[x+1,\frac{len}{2}])
hash([1,x−1]+[x+1,2len])与
h
a
s
h
(
[
l
e
n
2
+
1
,
l
e
n
]
)
hash([\frac{len}{2}+1,len])
hash([2len+1,len])是否相等,其中
l
e
n
len
len为原字符串的长度,
x
x
x在后半段同理。(其实就是分类讨论
x
x
x的位置)
最后只要根据符合条件的
x
x
x的数量来输出多解一解还是无解(需要考虑
x
x
x不同但字符串相同的情况)
总体复杂度
O
(
N
)
O(N)
O(N)
下面是代码:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 1000000007
#define M 14371003
#define ll long long
int ans[2000020], p[2000020];
int gethash(int l, int r)
{
return ((ans[r] - (ll)ans[l - 1] * p[r - l + 1]) % N + N) % N;
}
int del(int l, int r, int x)//删除操作
{
return ((ll)gethash(l, x - 1) * p[r - x] + gethash(x + 1, r)) % N;
}
int main()
{
int n, f = 0, res, t1, t2, t3, t4; char s[2000020];//f标记是否出现过符合条件的x
scanf("%d%s", &n, s);
ans[0] = s[0], p[0] = 1;
for (int i = 1; i ^ n; i++)
{
ans[i] = ((ll)ans[i - 1] * M + s[i]) % N;
p[i] = (ll)p[i - 1] * M % N;
}
if (n & 1)//只要长度为奇数才可能有解
{
t1 = (n >> 1), t2 = n - 1, t3 = t1 - 1, t4 = t1 + 1;
for (int i = 0; i ^ t4; i++)//枚举前一半
if (!(del(0, t1, i) ^ gethash(t4, t2)))//哈希值相等
{
if (f && (del(0, t2, i) ^ del(0, t2, res)))
{//符合条件的x已经出现且与之前得到的字符串不同则输出多解
printf("NOT UNIQUE");
return 0;
}
f = 1, res = i;
}
for (int i = t4; i ^ n; i++)//枚举后一半
{
if (!(gethash(0, t3) ^ del(t1, t2, i)))
{
if (f && (del(0, t2, i) ^ del(0, t2, res)))
{
printf("NOT UNIQUE");
return 0;
}
f = 1, res = i;
}
}
}
if (!f)//未出现符合条件的位置x或者原字符串长度为偶数则输出无解
{
printf("NOT POSSIBLE");
return 0;
}
if (res <= t1)printf("%s", s + 1 + t1);//x出现在前一半
else//x出现在后一半
{
s[t1] = '\0';
printf("%s", s);
}
return 0;
}
例题三:字符串合并操作的应用
牛客 白兔的字符串
对于循环同构,我们可以先预处理出字符串
T
T
T所有循环同构字符串的哈希值,而每一个循环同构字符串都由区间
[
i
+
1
,
l
e
n
T
]
、
[
1
,
i
]
[i+1,len_T]、[1,i]
[i+1,lenT]、[1,i]拼接而成,可以利用子串拼接的公式进行求解。接着对于每一个给出的
S
S
S枚举所有起点
i
i
i,检查子串
[
i
,
i
+
l
e
n
T
−
1
]
[i,i+len_T-1]
[i,i+lenT−1]的值是否在哈希表中出现过。当然是先对哈希表进行排序,之后二分查找哈希值即可。总复杂度
O
(
n
∗
l
e
n
T
∗
l
o
g
l
e
n
T
)
O(n*len_T*loglen_T)
O(n∗lenT∗loglenT)数据比较毒瘤,换了好几个质数
代码:
#include <stdio.h>
#include <string.h>
#include <algorithm>
#define ll long long
#define N 2147483587
#define M 25165843
char s[10000005];
int hash[10000005], base[1000005], rev[1000005];
int get(int l, int r)
{
return ((hash[r] - (ll)hash[l - 1] * base[r - l + 1]) % N + N) % N;
}
int merge(int l1, int r1, int l2, int r2)//子串合并操作
{
return ((ll)get(l1, r1) * base[r2 - l2 + 1] + get(l2, r2)) % N;
}
int main()
{
scanf("%s", s + 1);
int len = strlen(s + 1), n; base[0] = 1;
for (int i = 1; i <= len; ++i)
{
hash[i] = ((ll)hash[i - 1] * M + s[i]) % N;
base[i] = ((ll)M * base[i - 1]) % N;
}
for (int i = 1; i < len; ++i)
rev[i] = merge(i + 1, len, 1, i);//合并区间[i+1,len_T]、[1,i]得到同构子串
rev[len] = hash[len];
std::sort(rev + 1, rev + len + 1);//排序,方便二分查找
scanf("%d", &n);
while (n--)
{
int ans = 0;
scanf("%s", s + 1);
int lens = strlen(s + 1);
if (lens < len) { puts("0"); continue; }
for (int i = 1; i <= lens; ++i)
hash[i] = ((ll)hash[i - 1] * M + s[i]) % N;
lens -= len - 1;
for (int i = 1; i <= lens; ++i)//枚举每一个起始位置
if (std::binary_search(rev + 1, rev + len + 1, get(i, i + len - 1)))++ans;
//在哈希表中能找到对应的哈希值,那么答案加一
printf("%d\n", ans);
}
return 0;
}
今天的例题到这里就结束了
明天更新二维哈希、自然溢出以及二分哈希表
不见不散