字符串匹配算法基础
字符串匹配算法
字符串匹配就是在主串 中查找子串 。例如在 中查找 是否存在。子串的长度 主串长度
比较容易实现的字符串匹配算法:
- 暴力
- 字符串哈希
- KMP算法
算法一: 暴力
思路很简单:
让子串从第一个字符开始,与主串的每一个字符匹配,如果当前字符匹配上,则继续匹配下一个字符,如果匹配到子串的最后一个字符都相同,那就说明子串在主串中出现了。
例如:
| 主串 | a | b | c | a | b | c |
|---|---|---|---|---|---|---|
| 子串 | b | c | a |
| 主串 | a | b | c | a | b | c |
|---|---|---|---|---|---|---|
| 子串 | b | c | a |
| 主串 | a | b | c | a | b | c |
|---|---|---|---|---|---|---|
| 子串 | b | c | a |
| 主串 | a | b | c | a | b | c |
|---|---|---|---|---|---|---|
| 子串 | b | c | a |
算法二:字符串哈希
整体思路:
将一个字符串通过哈希函数变成数字,比较时只需比较数字是否相同即可。
获取哈希值的基本思路:
例如一个字符串 ,把每个字符看成一个 P 进制的数字,那么有:
再利用前缀和,就可以处理出每个子串的哈希值。
如:
这里我们会想,要是有两个不同的字符串,对应同一个哈希值,那不就出问题了吗?
确实是这样,这个也叫做哈希冲突。所以我们为了避免哈希冲突,可以通过这种手段:
让 取值为 或者 ,再将哈希值对 取模,可以有效减少哈希冲突。
在代码上,我们可以用 类型来实现对 取模的操作。
由于 的次方经常用到,所以可以预处理出来。
例题:
给定一个长度为 的字符串,再给定 个询问,每个询问包含四个整数 ,请你判断 和 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数 和 ,表示字符串长度和询问次数。
第二行包含一个长度为 的字符串,字符串中只包含大小写英文字母和数字。
接下来 行,每行包含四个整数 ,表示一次询问所涉及的两个区间。
注意,字符串的位置从 开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
输入样例:
8 3 aabbaabb 1 3 5 7 1 3 6 8 1 2 1 2
输出样例:
Yes No Yes
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10,P = 131;
unsigned long long p[N],h[N];
unsigned long long find(int l,int r)
{
return h[r] - h[l-1]*p[r-l+1];
}
int main()
{
int n,m;
string s;
cin >>n>>m;
cin >>s;
s = ' '+s;
p[0] = 1;
for (int i=1;i<=n;i++)
{
p[i] = p[i-1]*P;
h[i] = h[i-1]*P + s[i];
}
while (m--)
{
int l1,r1,l2,r2;
cin >>l1>>r1>>l2>>r2;
if (find(l1,r1) == find(l2,r2)) cout <<"Yes"<<endl;
else cout <<"No"<<endl;
}
}原题链接:
算法三:
大名鼎鼎的 算法并不是很好理解,在这里仅作为思考。
首先,我们知道了暴力匹配算法,但其实在暴力匹配的过程中,我们做了很多不必要的操作。比如在字符串 中查找子串 ,如果用暴力做法,我们在第一遍匹配的时候,会发现子串的最后一个字母 与主串的字母 不相同,然后子串又从第一位,主串从第二位开始匹配。但其实在这个过程中,我们已经匹配上了 ,下一次匹配只需要从主串的第五位,子串的第三位开始匹配。 像这样:
| 主串 | A | B | A | B | A | B | C | A | A |
|---|---|---|---|---|---|---|---|---|---|
| 子串 | A | B | A | B | C |
| 主串 | A | B | A | B | A | B | C | A | A |
|---|---|---|---|---|---|---|---|---|---|
| 子串 | A | B | A | B | C |
我们为什么可以这样做呢?
因为在第一次匹配的时候,我们发现已经匹配上的 前后缀是有公共部分 的。
这里为我们提供了一个思路,如果可以知道每次匹配到不相同,我们的子串应该移动到哪个位置再继续匹配,那可以大大加快匹配的速度。这里我们发现,主串的指针其实不需要回退,只要子串的指针移动到正确的位置即可。
那我们的子串应该移动到哪个位置呢?
这里引入一个 数组, 代表 指针应该移动到的下标。
先不说 数组是怎么得到的,来看一个例子:
| 字符串 | A | B | A | B | C |
|---|---|---|---|---|---|
| 下标 | 1 | 2 | 3 | 4 | 5 |
| 数组 | 0 | 0 | 1 | 2 | 0 |
对照这个表,再进行一次匹配:
| 主串 | A | B | A | B | A | B | C | A | A |
|---|---|---|---|---|---|---|---|---|---|
| 指针 | |||||||||
| 子串 | A | B | A | B | C | ||||
| 指针 |
我们发现在指针 和 的位置的字符不匹配,所以让 变成 的值,也就是 ,然后继续进行匹配:
| 主串 | A | B | A | B | A | B | C | A | A |
|---|---|---|---|---|---|---|---|---|---|
| 指针 | |||||||||
| 子串 | A | B | A | B | C | ||||
| 指针 |
注:我们这里每次比较的是主串的 指向的字符和子串 指向的字符。
匹配的代码可以这样实现:
//s是主串 p是子串
for (int i=1,j=0;i<=n;i++)
{
while (j && s[i] != p[j+1]) j = ne[j]; //如果不匹配,j一直后退到能匹配 或者一直退到起点 j=0
if (s[i] == p[j+1]) j++;
if (j == m) //这里是匹配成功了,如果要继续匹配就让 j = ne[j] 继续匹配
{
j = ne[j];
}
}数组的求法比较难理解,不过多介绍。这里我们认为求数组的过程,是子串自己与自己匹配的过程。
//ne[1] = 0
for (int i=2,j=0;i<=n;i++)
{
while (j && p[i] != p[j+1]) j = ne[j];
if (p[i] == p[j+1]) j++;
ne[i] = j; //记录下next数组
}例题: