字符串匹配算法——暴力 + KMP +SUNDAY + 哈希 + SHIFT_AND

单模匹配

暴力匹配

只有一个模式串机型匹配
模式串:待查找的字符串
依次性对齐,模式串的第一位和母串的每一位开始匹配,直到发现母串中的一部分跟模式串匹配

  1. 单模匹配问题,顾名思义,只有一个模式串
  2. 依次对齐模式串和文本串的每一位,直到匹配成功
  3. 关键:不重不漏的找到答案
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算法

  1. KMP算法中,模式串中的第三部分的重要性
  2. 第三部分可以帮助我们加快匹配速度的,避免掉大量无用的匹配尝试
  3. KMP算法保证不漏:第三部分匹配到的是模式串最长前缀
  4. 普通编码:获得NEXT数组,使用NEXT数组
  5. 高级编码:抽象化了一个状态机模型,j 所指向的就是状态机中的位置
  6. getNext 方法相当于根据输入字符,进行状态跳转,实际上就是改变j的值

当第一次匹配之中,

  1. 模式串中(分前缀部分)没有出现模式串中的前缀,并且第一次匹配到一半就不成立,
    那么可以说,已经匹配过的部分对于模式串都是无用的部分
    那么模式串可以从已经匹配的后一位开始下一轮的匹配。

  2. 模式串中(非前缀部分)出现模式串的前缀,但是匹配还是不成立
    那么可以说,母串中的新的模式串的前缀是模式串的新的匹配起点
    那么模式串可以从新的起点开始进行匹配

    ③串= ②串
    ②串 = ①串
    ①串 = ③串

③字符串性质:
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[i1]+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[i1]]+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 算法

  1. SUNDAY 算法理解的核心,在于理解黄金对齐点位
  2. 是文本串的匹配尾部,一定会出现在模式串中的字符
  3. 应该和模式串中的最后一位出现该字符的位置对齐
  4. 第一步:预处理每一个字符在模式串中最后一次出现的位置
  5. 第二部:模拟暴力匹配算法过程,失配的时候,文本串指针向后根据预处理信息向后移动若干位

时间复杂度 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) % 固定值 (c0base0+c1base1+c2base2+c3base3)
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=1ncibasei)

回到题目,假如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 + …… + Cibase0+Ci+1base1++
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=ijCkbasek)
在取余式中时不可以直接做除法的,如果想做除法需要求逆元也就是乘以一个值
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} 53=531 : 3 和 1 3 互 为 逆 元 3 和\frac{1}{3} 互为逆元 331
x 1 ∗ x 2 x_1 * x_2 % p = 1 x1x2 x 1 和 x 2 互 为 逆 元 x_1 和 x_2互为逆元 x1x2
( 16 / 4 ) % 7 = 4 < = > ( 16 ∗ 2 ) (16 / 4) \%7 = 4 <=> (16 * 2) % 7 = 4 (16/4)%7=4<=>(162)


x × x − 1 ≡ 1 ( m o d p ) x \times x^{-1} \equiv 1 (mod p) x×x11(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) xkr1+rr1=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) xx1kr1+x1rr10(modp) 等式两面同乘x的逆元乘r的逆元
k ∗ r − 1 + x − 1 = 0 ( m o d p ) k * r^{-1} + x^{-1} = 0 (mod p) kr1+x1=0(modp)
x − 1 = − k ∗ r − 1 ( m o d p ) x^{-1} = -k * r^{-1} (mod p) x1=kr1(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;
}
  1. 可以使用哈希操作判断两个字符串是否相等
    2.哈希值不同的话,两个字符串一定不相等,从而就不需要按位比较了
  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
  3. 在文本串上,每一位字符串哈希值的前缀和,方便一会求区间和
  4. 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)=(HSjHSi1)×(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;
}
  1. 第一步对模式串做特殊处理,把每一种字符出现的位置,转换成相应的二级制编码
  2. 后续匹配的过程跟模式串一毛钱关系都没有
  3. p i = ( p i − 1 < < 1 ∣ 1 ) & d [ s i ] p_i = (p_{i - 1} << 1 | 1) \& d[s_i] pi=(pi1<<11)&d[si]
  4. p i p_i pi 第j位二进制为1,代表当前位置为结尾,可以匹配成功模式串的第j位
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值