带通配符的单模式匹配
平常的单模式匹配就是给出一个模式串 t,给出一个文本串 s,问 s 中能够在哪些位点完全匹配 t,这个用 KMP 可以很好地解决。但是如果在模式串和文本串中有一些通配符,也就是这些符号匹配什么都可以,那这样就解决不了了。比如说 t=a*b
,s=aebr*ob
,其中 *
表示通配符,那么 t 在 s 中可以匹配的位点为 1 5
(下标从 1 开始)。
解决办法本质上是把匹配转变为一个数学公式 G(x),表示模式串在原串的 x 处的匹配公式,并且这个 G(x) 满足 ∀ x ≥ 0 , G ( x ) ≥ 0 \forall x\ge 0,G(x)\ge 0 ∀x≥0,G(x)≥0,且当 G(x)=0 时表示可以匹配。一般 G(x) 由一些子公式组成。
对于带通配符的单模式匹配问题,我们可以设子函数 p(a, b) 表示 t[a] 和 s[b] 的匹配函数,p(a, b) 和 G 具有一样的性质,所以我们的主要目标就是如何构造函数 p,使得它满足这个性质。比如对于这个问题,我们可以把 *
设为0,然后设
p
(
a
,
b
)
=
(
t
[
a
]
−
s
[
b
]
)
2
t
[
a
]
s
[
b
]
p(a,b)=(t[a]-s[b])^2t[a]s[b]
p(a,b)=(t[a]−s[b])2t[a]s[b],那么满足
p
(
a
,
b
)
≥
0
p(a,b)\ge0
p(a,b)≥0,并且仅当 t[a]=s[b] 或两个中有通配符时才成立 p(a, b)=0 ,其余情况都大于 0 。所以有
G
(
x
)
=
∑
i
=
0
m
−
1
p
(
i
,
x
+
i
)
G(x)=\sum\limits_{i=0}^{m-1}p(i,x+i)
G(x)=i=0∑m−1p(i,x+i) ,可以证明这个 G(x) 满足上面的限制。
进一步,我们把模式串 t 翻转(这是为了凑出常数),然后公式就变为 G ( x ) = ∑ i = 0 m − 1 p ( m − i − 1 , x + i ) G(x)=\sum\limits_{i=0}^{m-1}p(m-i-1,x+i) G(x)=i=0∑m−1p(m−i−1,x+i) ,发现 m − i − 1 + x + i = m − 1 + x m-i-1+x+i=m-1+x m−i−1+x+i=m−1+x,对于特定的 x 是一个常数,那把 p 带入展开之后,这不就是一个很长的卷积和式么。用几次 FFT 加速,就可以算出所有的 G(x),然后一一判断是否等于 0 来决定是否匹配。
其实一般的单模式匹配也可以这么做,只要改一下 p 函数,改成 p ( a , b ) = ( t [ a ] − s [ b ] ) 2 p(a,b)=(t[a]-s[b])^2 p(a,b)=(t[a]−s[b])2 就可以了。
字符串哈希
双哈希一般可以很好地避免冲突,而单哈希则可以更好地处理字符串。
双哈希就是给定两个哈希值,分别计算两次单哈希,然后用一个二元组唯一表示一个字符串。
单哈希的一般方法如下:给定一个质数 P,一个模数 M,然后对于字符串 s,其 hash 数组:
h
a
s
h
[
i
]
=
(
h
a
s
h
[
i
−
1
]
∗
P
+
s
[
i
]
)
%
M
hash[i]=(hash[i-1]*P+s[i])\%M
hash[i]=(hash[i−1]∗P+s[i])%M ,这里一般把 M 设为 unsigned long long
的上限,这样就可以自然溢出,然后 P 就选择一个中等大小的质数就行了。
算出 hash 数组之后,如果要求 s 的某个子串的 hash 值,则有 h a s h s [ l , . . . , r ] = h a s h [ r ] − h a s h [ l − 1 ] ∗ P r − l + 1 hash_{s[l,...,r]}=hash[r]-hash[l-1]*P^{r-l+1} hashs[l,...,r]=hash[r]−hash[l−1]∗Pr−l+1 。
const unsigned long long P=100007;
const int maxn=1e6+5;
char s[maxn],t[maxn];
unsigned long long h[maxn],p[maxn];
void build(char *s)
{
//p[0]=1;
//REP(i,1,maxn-5) p[i]=p[i-1]*P;
for(int i=1;s[i];i++) h[i]=h[i-1]*P+s[i];
}
unsigned long long get_hash(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
有了字符串哈希之后,单模式匹配实际上就可以看成枚举文本串起始位置,然后用哈希值判断是否相同就行了(多模式匹配如果文本串×模式串数目可以接受的话,也可以用哈希)。
另外,按照这种哈希方式,一个字符串其实可以表示为 s 1 ∗ p n + s 2 ∗ p n − 1 + ⋯ + s n ∗ p s_1*p^{n}+s_2*p^{n-1}+\dots +s_n*p s1∗pn+s2∗pn−1+⋯+sn∗p ,其中 p 的指数其实是多少不重要,只要满足相邻相差 1 的关系就行了。然后我们就可以用线段树维护每一位的值,那么某个子串的哈希值实际上就是区间和除以一个系数。
一道例题 Subpalindromes :给出一个字符串和两种操作:(1)修改某一个位置的字符;(2)查询某个区间是否是回文串。
做法就是,首先如果一个字符串是回文串,那么它正反两个方向的哈希值是相等的。所以用两棵线段树维护哈希值,然后查询的时候把指数少的那个乘上相应的 p 的幂,然后判断是否相等即可。