//点击后出现烟花效果

字符串匹配算法

字符串匹配算法


字符串匹配就是在主串 A 中查找子串 B 。例如在  abcabc 中查找 bca 是否存在。子串的长度 主串长度


比较容易实现的字符串匹配算法:

  1. 暴力 O(mn)
  2. 字符串哈希 O(m+n)
  3. KMP算法 O(m+n)



算法一: 暴力


思路很简单:

让子串从第一个字符开始,与主串的每一个字符匹配,如果当前字符匹配上,则继续匹配下一个字符,如果匹配到子串的最后一个字符都相同,那就说明子串在主串中出现了。

例如:

主串 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



算法二:字符串哈希


整体思路:

将一个字符串通过哈希函数变成数字,比较时只需比较数字是否相同即可。

获取哈希值的基本思路:

例如一个字符串 abc,把每个字符看成一个 P 进制的数字,那么有:

a=aASCII×P0

ab=aASCII×P1+bASCII×P0

abc=aASCII×P2+bASCII×P1+cASCII×P0

再利用前缀和,就可以处理出每个子串的哈希值。

如:

bc=abca×P2



这里我们会想,要是有两个不同的字符串,对应同一个哈希值,那不就出问题了吗?

确实是这样,这个也叫做哈希冲突。所以我们为了避免哈希冲突,可以通过这种手段:

P 取值为 131 或者 13331 ,再将哈希值对 264 取模,可以有效减少哈希冲突。

在代码上,我们可以用 unsigned long long 类型来实现对 264 取模的操作。

由于 P 的次方经常用到,所以可以预处理出来。



例题:

给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1][l2,r2] 这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。


输入格式

第一行包含整数 nm,表示字符串长度和询问次数。

第二行包含一个长度为 n 的字符串,字符串中只包含大小写英文字母和数字。

接下来 m 行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。

注意,字符串的位置从 1 开始编号。


输出格式

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No

每个结果占一行。


数据范围

1n,m105


输入样例:

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;
    }
}

原题链接:




算法三:KMP


大名鼎鼎的 KMP 算法并不是很好理解,在这里仅作为思考。


首先,我们知道了暴力匹配算法,但其实在暴力匹配的过程中,我们做了很多不必要的操作。比如在字符串 ABABABCAA 中查找子串 ABABC ,如果用暴力做法,我们在第一遍匹配的时候,会发现子串的最后一个字母 C 与主串的字母 A 不相同,然后子串又从第一位,主串从第二位开始匹配。但其实在这个过程中,我们已经匹配上了 ABAB ,下一次匹配只需要从主串的第五位,子串的第三位开始匹配。
像这样:

主串 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

我们为什么可以这样做呢?


因为在第一次匹配的时候,我们发现已经匹配上的 ABAB 前后缀是有公共部分 AB 的。


这里为我们提供了一个思路,如果可以知道每次匹配到不相同,我们的子串应该移动到哪个位置再继续匹配,那可以大大加快匹配的速度。这里我们发现,主串的指针其实不需要回退,只要子串的指针移动到正确的位置即可。


那我们的子串应该移动到哪个位置呢?


这里引入一个 next 数组,next[j] 代表 j 指针应该移动到的下标。


先不说 next 数组是怎么得到的,来看一个例子:

字符串 A B A B C
下标 1 2 3 4 5
next 数组 0 0 1 2 0

对照这个表,再进行一次匹配:

主串 A B A B A B C A A
指针 i
子串 A B A B C
指针 j j+1

我们发现在指针 ij+1 的位置的字符不匹配,所以让 j 变成 next[j] 的值,也就是 2 ,然后继续进行匹配:



主串 A B A B A B C A A
指针 i
子串 A B A B C
指针 j j+1

注:我们这里每次比较的是主串的 i 指向的字符和子串 j+1 指向的字符。


匹配的代码可以这样实现:

//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];
    }
}

next数组的求法比较难理解,不过多介绍。这里我们认为求next数组的过程,是子串自己与自己匹配的过程。

//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数组
}

例题:

posted @   JunieXD  阅读(154)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示