单模匹配
暴力匹配
只有一个模式串机型匹配
模式串:待查找的字符串
依次性对齐,模式串的第一位和母串的每一位开始匹配,直到发现母串中的一部分跟模式串匹配
- 单模匹配问题,顾名思义,只有一个模式串
- 依次对齐模式串和文本串的每一位,直到匹配成功
- 关键:不重不漏的找到答案
int brute_force(const char *s, const char *t) {
for (int i = 0; s[i]; i++) {
int flag = 1;
for (int j = 0; t[j]; j++) {
if (s[i + j] - t[j] == 0) continue;
flag = 0;
break;
}
if (flag == 1) return i;
}
return -1;
}
KMP算法
- KMP算法中,模式串中的第三部分的重要性
- 第三部分可以帮助我们加快匹配速度的,避免掉大量无用的匹配尝试
- KMP算法保证不漏:第三部分匹配到的是模式串最长前缀
- 普通编码:获得NEXT数组,使用NEXT数组
- 高级编码:抽象化了一个状态机模型,j 所指向的就是状态机中的位置
- getNext 方法相当于根据输入字符,进行状态跳转,实际上就是改变j的值
当第一次匹配之中,
-
模式串中(分前缀部分)没有出现模式串中的前缀,并且第一次匹配到一半就不成立,
那么可以说,已经匹配过的部分对于模式串都是无用的部分
那么模式串可以从已经匹配的后一位开始下一轮的匹配。 -
模式串中(非前缀部分)出现模式串的前缀,但是匹配还是不成立
那么可以说,母串中的新的模式串的前缀是模式串的新的匹配起点
那么模式串可以从新的起点开始进行匹配
③串= ②串
②串 = ①串
①串 = ③串
③字符串性质:
9. ③为t的前缀,③不是t的前缀
10. ③是当前位置的最长前缀:③字符串越长,则t字符串跨越s字符串越短,就尽可能匹配所有出现的情况
11. ③紧挨着第一部分失配的位置
对于①③,我们可以在模式串中进行预处理:②串在文本串中,我们无法考虑
把每一个位置都当作第③串失配的位置,对于每一个位置的第③串,预处理得到了next
next数组的形成
对于j串
n
e
x
t
[
i
]
next[i]
next[i] :对于i位置可以匹配到多少的前缀是匹配成功的前缀的长度
若
②
=
④
② = ④
②=④
n
e
x
t
[
i
]
=
n
e
x
t
[
i
−
1
]
+
1
next[i] = next[i - 1] + 1
next[i]=next[i−1]+1
若
②
≠
④
② \ne ④
②=④
n
e
x
t
[
i
]
=
n
e
x
t
[
n
e
x
t
[
i
−
1
]
]
+
1
next[i] = next[next[i - 1]] + 1
next[i]=next[next[i−1]]+1
对于next[i] 和 i 位
假设串为s
则s[next[i]] = s[i],因为第
n
e
x
t
[
i
]
next[i]
next[i] 位,是
i
i
i 位在前面的相对映射,:左面布局均一样
所以next[next[i - 1]],就是回到上一个左面相对位置均一样的地方,重新比对右面的字符
//写法1
void getNext(const char *t, int *next) {
next[0] = -1;
int j = -1;//上一个next数组的值
for (int i = 1; t[i]; i++) {
while (j != -1 && t[j + 1] != t[i]) j = next[j];
if (t[j + 1] == t[i]) j += 1;
next[i] = j;
}
return ;
}
int kmp(const char *s, const char *t) {
int n = strlen(t);
int *next = (int *)malloc(sizeof(int) * n + 1);
getNext(t, next);
for (int i = 0, j = -1; s[i]; i++) {
while (j != -1 && s[i] - t[j + 1]) j = next[j];
if (s[i] == t[j + 1]) j += 1;
if (t[j + 1] == 0) return i - n + 1;//已经匹配到\0字符了
}
free(next);
return -1;
}
//写法2
int getNext(const char *t, int &j, char input, int *next) {
while (j != -1 && t[j + 1] != input) j = next[j];
if (t[j + 1] == inpput) j += 1;
return j;
}
int kmp(const *char s, const char *t) {
int n = strlen(t);
int *next = (int *)malloc(sizeof(int) * n + 1);
next[0] = -1;
for (int i = 1, j = -1; t[[i]; i++) next[i] = getNext(t, j, t[i], next);
for (int i = 0, j = -1, s[i]; i++) {
if (getNext(t, j, s[i], next) != n - 1) continue;//如果返回的j值是字符串t的最后一个位置,则查找结束
return i - n + 1;
}
free(next);
return -1;
}
Sunday 算法
- SUNDAY 算法理解的核心,在于理解黄金对齐点位
- 是文本串的匹配尾部,一定会出现在模式串中的字符
- 应该和模式串中的最后一位出现该字符的位置对齐
- 第一步:预处理每一个字符在模式串中最后一次出现的位置
- 第二部:模拟暴力匹配算法过程,失配的时候,文本串指针向后根据预处理信息向后移动若干位
时间复杂度
O
(
n
m
)
O(\frac{n}{m})
O(mn) 常在文章中找单词
假设T字符串和S字符串的最后一位匹配失败了
1:则整体向后移动1位,但是d和e不匹配,于是在模式串从后往前找最后一个e,(黄金对齐点位)
再从头到尾开始匹配
如果失败重复步骤1
在模式串的首尾加上一个虚拟字符,解决找不到黄金对齐点的问题
int sunday(const char *s, const char *t) {
int offset[256];
int n = strlen(t), m = strlen(s);
for (int i = 0; i < 256; i++) offset[i] = n + 1;//默认所有字符都是没有出现过的,都是在倒数n + 1位
for (int i = 0; t[i]; i++) offset[t[i]] = n - i;//出现过倒数n - i位
for (int i = 0; i + n <= m; i += offset[s[i + n]] ) {//文本串剩余比m长,找到第i + n位在模式串中最后一次出现的位置
int flag = 1;
for (int j = 0; t[j]; j++) {
flag = flag && (s[i + j] == t[j]);
}//如果每一位都一样,flag的值一直为1
if (flag) return i;
}
return -1;
}
哈希匹配
单模匹配问题
题1:兔子与兔子
DNA序列若干查询,查询次数特别多,暴力按位比较会出现
O
(
m
×
n
)
O(m \times n)
O(m×n) 时间复杂度,会导致TLE
如何快速比较两个字符串完全相等?
引入哈希匹配法
把字符串的每一位当成数字
a
b
d
e
f
e
c
a b d e f e c
abdefec
1245653
1 2 4 5 6 5 3
1245653:假设出来
两个字符串的比较就可以看成两个数字的比较,如果字符串过长会导致不方便,所以改为对其余数取余
这两个余数就是原字符串的哈希值
假设字符串由 c 1 c 2 c 3 c 4 c_1c_2c_3c_4 c1c2c3c4再设置一个 b a s e base base作为位权,(随机设置的一个素数)
( c 0 ∗ b a s e 0 + c 1 ∗ b a s e 1 + c 2 ∗ b a s e 2 + c 3 ∗ b a s e 3 ) (c_0 * base^0 + c_1 * base^1 + c_2 * base^2 + c_3 * base^3) % 固定值 (c0∗base0+c1∗base1+c2∗base2+c3∗base3)
H a s h = ( ∑ i = 1 n c i ∗ b a s e i ) Hash = (\sum_{i = 1}^n c_i * base^i) % 固 Hash=(∑i=1nci∗basei)
回到题目,假如DNA字符串中,求第
i
i
i 位和第
j
j
j 位的哈希值
C
i
∗
b
a
s
e
0
+
C
i
+
1
∗
b
a
s
e
1
+
…
…
+
C_i * base^0 + C_{i + 1} * base^1 + …… +
Ci∗base0+Ci+1∗base1+……+
H
a
s
h
1
=
(
∑
k
=
i
j
C
k
∗
b
a
s
e
k
)
Hash_1 = (\sum_{k = i}^j C_k * base^k) % 固定值
Hash1=(∑k=ijCk∗basek)
在取余式中时不可以直接做除法的,如果想做除法需要求逆元也就是乘以一个值
H
a
s
h
=
(
H
a
s
h
1
∗
(
b
a
s
e
k
)
−
1
)
Hash = (Hash_1 * (base^k)^{-1}) % 固定值
Hash=(Hash1∗(basek)−1)
如何快速的求 H a s h 1 Hash_1 Hash1 的值? : 区间和->前缀和优化
如何求base^k 逆元?
5 ∗ 3 = 5 ∗ 1 3 5 * 3 = 5 * \frac{1}{3} 5∗3=5∗31 : 3 和 1 3 互 为 逆 元 3 和\frac{1}{3} 互为逆元 3和31互为逆元
x 1 ∗ x 2 x_1 * x_2 % p = 1 x1∗x2 则 x 1 和 x 2 互 为 逆 元 x_1 和 x_2互为逆元 x1和x2互为逆元
( 16 / 4 ) % 7 = 4 < = > ( 16 ∗ 2 ) (16 / 4) \%7 = 4 <=> (16 * 2) % 7 = 4 (16/4)%7=4<=>(16∗2)
x × x − 1 ≡ 1 ( m o d p ) x \times x^{-1} \equiv 1 (mod p) x×x−1≡1(modp) ——x 一定小于p
令 P % x = r P \%x = r P%x=r
p = x k + r; r 小于x
xk + r \equiv 0 (mod p)
x ∗ k ∗ r − 1 + r ∗ r − 1 = 0 ( m o d p ) x * k * r^{-1} + r * r^{-1} = 0 (mod p) x∗k∗r−1+r∗r−1=0(modp)
x ∗ x − 1 ∗ k ∗ r − 1 + x − 1 ∗ r ∗ r − 1 ≡ 0 ( m o d p ) x * x^{-1}* k * r^{-1} + x^{-1} * r * r^{-1} \equiv 0 (mod p) x∗x−1∗k∗r−1+x−1∗r∗r−1≡0(modp) 等式两面同乘x的逆元乘r的逆元
k ∗ r − 1 + x − 1 = 0 ( m o d p ) k * r^{-1} + x^{-1} = 0 (mod p) k∗r−1+x−1=0(modp)
x − 1 = − k ∗ r − 1 ( m o d p ) x^{-1} = -k * r^{-1} (mod p) x−1=−k∗r−1(modp)
k = p / x k = p / x k=p/x
r = p % x r = p \% x r=p%x
#include<iostream>
using namespace std;
#define MAX_N 1000
//求 1 - 6 mod 7的逆元
int inv[7] = {0};
int main() {
inv[1] = 1;
for (int i = 2; i < 7; i++) {
inv[i] = ((-(7 / 2) * inv[7 % i]) % 7 + 7) % 7;
cout << i << " : " << inv[i] << endl;
}
return 0;
}
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <queue>
#include <stack>
#include <algorithm>
#include <string>
#include <map>
#include <set>
#include <vector>
using namespace std;
#define MAX_N 1000000
#define P 100007
#define P1 100007
#define base 13
#define base1 103
long long H[MAX_N + 5];
long long H1[MAX_N + 5];
long long K[MAX_N + 5];
long long K1[MAX_N + 5];
long long inv[P];
long long inv1[P1];
char s[MAX_N + 5];
void init() {
inv[1] = 1;
inv1[1] = 1;
for (long long i = 2; i < P; i++) {
inv[i] = ((-(P / i) * inv[P % i]) % P + P) % P;
inv1[i] = ((-(P1 / i) * inv1[P1 % i]) % P1 + P1) % P1;
}
K[0] = 1;
K1[0] = 1;
for (long long i = 1; i <= MAX_N; i++) {
K[i] = (K[i - 1] * base) % P;
K1[i] = (K1[i - 1] * base1) % P1;
}
H[0] = 0;
H1[0] = 0;
for (long long i = 1; s[i]; i++) {
H[i] = (H[i - 1] + K[i] * s[i]) % P;
H1[i] = (H1[i - 1] + K1[i] * s[i]) % P1;
}
return ;
}
long long getH(long long l, long long r) {
return ((H[r] - H[l - 1]) % P * inv[K[l]] % P + P) % P;
}
long long getH1(long long l, long long r) {
return ((H1[r] - H1[l - 1]) % P1 * inv1[K1[l]] % P1 + P1) % P1;
}
int main() {
scanf("%s", s + 1);
long long m, l1, l2, r1, r2;
init();
scanf("%lld", &m);
for (long long i = 0; i < m; i++) {
scanf("%lld%lld%lld%lld", &l1, &r1, &l2, &r2);
if (getH(l1, r1) == getH(l2, r2) && getH1(l1, r1) == getH1(l2, r2)) {
printf("Yes\n");
} else {
printf("No\n");
}
}
return 0;
}
- 可以使用哈希操作判断两个字符串是否相等
2.哈希值不同的话,两个字符串一定不相等,从而就不需要按位比较了- H = ( ∑ k = 0 n C k × b a s e k ) % p H = (\sum_{k = 0}^{n} {C_k \times base^k}) \% p H=(∑k=0nCk×basek)%p
- 在文本串上,每一位字符串哈希值的前缀和,方便一会求区间和
- H ( i , j ) = ( H S j − H S i − 1 ) × ( b a s e k ) − 1 % P H(i, j) = (HS_j - HS_{i - 1}) \times (base^k)^{-1} \% P H(i,j)=(HSj−HSi−1)×(basek)−1%P
Shift-AND 算法
时间复杂度几乎O(n)
通过模式串建立D数组
d[a] = 9
关键代码
P:二进制思想,以当前位置为结尾,能匹配成功多少位
s[i] : 代表文本串第i位字符
d[s[i]] : 代表文本串第i位字符的编码
P << 1 :如果上一个字符中匹配成功了2位,4位,又匹配成功了7位,末尾添加一个字符,可能匹配成功3位,5位,8位:成功的条件,s这个字符在3, 5, 8 位置出现过,所以进行与运算
| 1:左移一位,末尾补0,意味不管怎么取与运算,P的最低为都不可能是1,意味着我们永远不可能成功匹配一位字符
把P看成当前匹配字符串的位数,也就是当P匹配该位时,P在上一位的基础上,需要向左移动一位,可以理解位扩充一位,然后在末尾补1,意味着,可能最后一位是有可能匹配上的,因为如果是0得话,意味着,与模式串该位无法匹配,然后与上该位上的字符的值,看结果,假设第n位为1,说明,前n位对于该位置,前n位与模式串的前n位都是可以匹配的上的
P当中记录着多个状态,以该位置为结尾,可以匹配多个与该位置可能匹配的位置
代码演示:
int shift_and(const char *s, const char *t) {
int d[256] = {0}, n = 0;
for (int i = 0; t[i]; n++, i++)
d[t[i]] |= (1 << i);
int p = 0;
for (int i = 0; s[i]; i++) {
p = (p << 1 | 1) & d[s[i]];
if (p & (1 << (n - 1))) return i - n + 1;
}
return -1;
}
- 第一步对模式串做特殊处理,把每一种字符出现的位置,转换成相应的二级制编码
- 后续匹配的过程跟模式串一毛钱关系都没有
- p i = ( p i − 1 < < 1 ∣ 1 ) & d [ s i ] p_i = (p_{i - 1} << 1 | 1) \& d[s_i] pi=(pi−1<<1∣1)&d[si]
- p i p_i pi 第j位二进制为1,代表当前位置为结尾,可以匹配成功模式串的第j位