扩展kmp——神奇的字符串匹配

一、引言

一个算是冷门的算法(在竞赛上),不过其算法思想值得深究。

二、前置知识

  1. kmp的算法思想,具体可以参考这篇日报

  2. trie树(字典树)

三、经典扩展kmp模板问题:

扩展kmp的模板问题:

给你两个字符串s,t,长度分别为n,m。
请输出s的每一个后缀与t的最长公共前缀。

哈希是不可能的,这辈子都不可能的。

A C \mathcal{AC} AC自动机?好像更不可做了。

我们先定义一个: e x t e n d [ i ] extend[i] extend[i]表示 S [ i . . . n ] S[i...n] S[i...n] T T T的最长公共前缀长度,而题意就是让你求所有的 e x t e n d [ i ] extend[i] extend[i]

注:以下字符串均从1开始计位。

例子:

如果 S = a a a a a a a a a a b a a S=aaaaaaaaaabaa S=aaaaaaaaaabaa n = 13 n=13 n=13

T = a a a a a a a a a a a T=aaaaaaaaaaa T=aaaaaaaaaaa m = 11 m=11 m=11

由图可知, e x t e n d [ 1 ] = 10 extend[1]=10 extend[1]=10 e x t e n d [ 2 ] = 9 extend[2]=9 extend[2]=9

我们会发现:在求 e x t e n d [ 2 ] extend[2] extend[2]时,我们耗费了很多时间,但我们可以利用 e x t e n d [ 1 ] extend[1] extend[1]来更快速地求解:

因为已经计算出 e x t e n d [ 1 ] = 10 extend[1]=10 extend[1]=10

所以有: S [ 1...10 ] = T [ 1...10 ] S[1...10]=T[1...10] S[1...10]=T[1...10]

然后得: S [ 2...10 ] = T [ 2...10 ] S[2...10]=T[2...10] S[2...10]=T[2...10]

因为计算 e x t e n d [ 2 ] extend[2] extend[2]时,实际上是 S [ 2... n ] S[2...n] S[2...n] T T T的匹配,

又因为刚刚求出了 S [ 2...10 ] = T [ 2...10 ] S[2...10]=T[2...10] S[2...10]=T[2...10]

所以匹配的开头阶段是求 T [ 2...10 ] T[2...10] T[2...10] T T T的匹配。

这时我们可以设置辅助参数: n e x t next next n e x t [ i ] next[i] next[i]表示 T [ i , m ] T[i,m] T[i,m] T T T的最长公共前缀长度。

那么对于上述的例子: n e x t [ 2 ] = 10 next[2]=10 next[2]=10

即: T [ 2...11 ] = T [ 1...10 ] T[2...11]=T[1...10] T[2...11]=T[1...10]

然后得: T [ 2...10 ] = T [ 1...9 ] T[2...10]=T[1...9] T[2...10]=T[1...9]

∴ S [ 2...10 ] = T [ 2...10 ] = T [ 1...9 ] ∴S[2...10]=T[2...10]=T[1...9] S[2...10]=T[2...10]=T[1...9]

也就是说求 e x t e n d [ 2 ] extend[2] extend[2]的匹配的前9位已经匹配成功了,不用再匹配一遍了,可以直接从 S [ 11 ] S[11] S[11] T [ 10 ] T[10] T[10]开始匹配,这样我们就省下了很多时间。

这其实就是kmp的思想。

对于一般情况:

e x t e n d [ 1... k ] extend[1...k] extend[1...k]已经算好,并且在以前的匹配过程中在S串中的最远位置是 p p p,即 p = m a x ( i + e x t e n d [ i ] − 1 ) p=max(i+extend[i]-1) p=max(i+extend[i]1),其中 i = 1... k i=1...k i=1...k

然后我们设取这个最大值k的位置是 p 0 p0 p0

首先,根据定义, S [ p 0... p ] = T [ 1... p − p 0 + 1 ] S[p0...p]=T[1...p-p0+1] S[p0...p]=T[1...pp0+1]

我们设 T [ k − p 0 + 1 ] T[k-p0+1] T[kp0+1] T T T串中对应的位置为 a a a T [ k − p 0 + 2 ] T[k-p0+2] T[kp0+2] T T T串中所对应的位置为 b b b(仅仅是为了下面的讲解方便)

然后令 L = n e x t [ b ] L=next[b] L=next[b]

下面分两种情况讨论:

第一种情况: k + L < p k+L<p k+L<p

也就是 S [ k + L ] S[k+L] S[k+L]这个位置在 p p p前面,如图:

我们设 l 1 = 1 l1=1 l1=1 r 1 = L r1=L r1=L l 2 = b l2=b l2=b r 2 = b + L − 1 r2=b+L-1 r2=b+L1。( b b b的定义在上一张图)

此时 l 1 l1 l1 r 1 r1 r1 l 2 l2 l2 r 2 r2 r2的位置如图所示。

也就是说, T [ l 1... r 1 ] = T [ l 2... r 2 ] T[l1...r1]=T[l2...r2] T[l1...r1]=T[l2...r2]

红线 \color{red}{\text{红线}} 红线 绿线 \color{green}{\text{绿线}} 绿线 蓝线 \color{blue}{\text{蓝线}} 蓝线相等。

然后由 n e x t next next的定义可知, T [ r 1 + 1 ] ! = T [ r 2 + 1 ] T[r1+1]!=T[r2+1] T[r1+1]!=T[r2+1]

又因为 T [ r 2 + 1 ] = S [ k + L + 1 ] T[r2+1]=S[k+L+1] T[r2+1]=S[k+L+1]

所以 T [ r 1 + 1 ] ! = S [ k + L + 1 ] T[r1+1]!=S[k+L+1] T[r1+1]!=S[k+L+1],这两个字符不一样。

又因为 红线 \color{red}{\text{红线}} 红线 蓝线 \color{blue}{\text{蓝线}} 蓝线相等,这两条线已经匹配成功。

所以 e x t e n d [ k + 1 ] = L extend[k+1]=L extend[k+1]=L,也就是 n e x t [ b ] next[b] next[b]

所以这段的代码比较简单:

if(i+nxt[i-p0]<extend[p0]+p0)extend[i]=nxt[i-p0];
//i相当于k+1
//nxt[i-p0]相当于L
//extend[p0]+p0相当于p
//因为在代码里我是从0开始记字符串的,所以本应在小于号左侧减1,现在不用了。

第二种情况: k + L > = p k+L>=p k+L>=p

也就是 S [ k + L ] S[k+L] S[k+L]这个位置在p前面,如图:

图可能略丑

同样,我们设 l 1 = 1 l1=1 l1=1 r 1 = L r1=L r1=L l 2 = b l2=b l2=b r 2 = b + L − 1 r2=b+L-1 r2=b+L1

此时 l 1 l1 l1 r 1 r1 r1 l 2 l2 l2 r 2 r2 r2的位置如图所示。( r 1 r1 r1的位置可能在 p − p 0 + 1 p-p0+1 pp0+1前或后)

同理, 红线 \color{red}{\text{红线}} 红线 绿线 \color{green}{\text{绿线}} 绿线 蓝线 \color{blue}{\text{蓝线}} 蓝线相等。

那么我们设 ( k + L ) (k+L) (k+L) p p p的这段距离为 x x x

那么 S [ k + 1... ( k + L ) − x + 1 ] = S [ k + 1... p ] S[k+1...(k+L)-x+1]=S[k+1...p] S[k+1...(k+L)x+1]=S[k+1...p]

又因为:

T [ l 1... r 1 − x + 1 ] = T [ l 2... r 2 − x + 1 ] = S [ k + 1... ( k + L ) − x + 1 ] T[l1...r1-x+1]=T[l2...r2-x+1]=S[k+1...(k+L)-x+1] T[l1...r1x+1]=T[l2...r2x+1]=S[k+1...(k+L)x+1]

S1 = S2 = S3 \color{blue}{\text{S1}}\color{black}{=}\color{red}{\text{S2}}\color{black}{=}\color{green}{\text{S3}} S1=S2=S3

所以 T [ l 1... r 1 − x + 1 ] = S [ k + 1... p ] T[l1...r1-x+1]=S[k+1...p] T[l1...r1x+1]=S[k+1...p]

也就是说 T [ 1... r 1 − x + 1 ] = S [ k + 1... p ] T[1...r1-x+1]=S[k+1...p] T[1...r1x+1]=S[k+1...p],这一段已经匹配成功了。

S1 \color{blue}{\text{S1}} S1 S2 \color{red}{\text{S2}} S2是相等的,已经匹配成功了。

那么我们就可以从 S [ p + 1 ] S[p+1] S[p+1] T [ r 1 − x + 2 ] T[r1-x+2] T[r1x+2]开始暴力匹配了,无需再考虑前面的东西。

那么这段的代码长这样:

int now=extend[p0]+p0-i;
now=max(now,0);//这里是防止i>p
while(t[now]==s[i+now]&&now<(int)t.size()&&now+i<(int)s.size())now++;//暴力求解的过程
extend[i]=now;
p0=i;//更新p0

n e x t next next

e x t e n d extend extend的大部分过程已经完成了,现在就剩怎么求 n e x t next next了,我们先摸清一下求 n e x t next next的本质:

求T的每一个后缀与T的最长公共前缀长度

听起来好熟悉,我们再看一下题面:

求S的每一个后缀与T的最长公共前缀长度

我们发现求 n e x t next next的本质和求 e x t e n d extend extend的本质是一样的,所以我们直接复制重新打一遍就好了。

这其实和 k m p kmp kmp的思想很相似,因为 k m p kmp kmp也是自己匹配一遍自己,再匹配文本串。

要注意的一点是:求 n e x t next next时我们要从第2位(也就是代码中的第1位)开始暴力,这样能防止求 n e x t next next时引用自己 n e x t next next值的情况。

时间复杂度

因为求 n e x t next next的时间复杂度是 O ( m ) O(m) O(m),求 e x t e n d extend extend的时间复杂度是 O ( n ) O(n) O(n),所以总时间复杂度: O ( n + m ) O(n+m) O(n+m),即 S S S串与 T T T串的长度之和。

Code

#include<bits/stdc++.h>

#define N 1000010 

using namespace std;

int q,nxt[N],extend[N];
string s,t;

void getnxt()
{
    nxt[0]=t.size();//nxt[0]一定是T的长度
    int now=0;
    while(t[now]==t[1+now]&&now+1<(int)t.size())now++;//这就是从1开始暴力
    nxt[1]=now;
    int p0=1;
    for(int i=2;i<(int)t.size();i++)
    {
        if(i+nxt[i-p0]<nxt[p0]+p0)nxt[i]=nxt[i-p0];//第一种情况
        else
        {//第二种情况
            int now=nxt[p0]+p0-i;
            now=max(now,0);//这里是为了防止i>p的情况
            while(t[now]==t[i+now]&&i+now<(int)t.size())now++;//暴力
            nxt[i]=now;
            p0=i;//更新p0
        }
    }
}

void exkmp()
{
    getnxt();
    int now=0;
    while(s[now]==t[now]&&now<min((int)s.size(),(int)t.size()))now++;//暴力
    extend[0]=now;
    int p0=0;
    for(int i=1;i<(int)s.size();i++)
    {
        if(i+nxt[i-p0]<extend[p0]+p0)extend[i]=nxt[i-p0];//第一种情况
        else
        {//第二种情况
            int now=extend[p0]+p0-i;
            now=max(now,0);//这里是为了防止i>p的情况
            while(t[now]==s[i+now]&&now<(int)t.size()&&now+i<(int)s.size())now++;//暴力
            extend[i]=now;
            p0=i;//更新p0
        }
    }
}

int main()
{
    cin>>s>>t;
    exkmp();
    int len=t.size();
    for(int i=0;i<len;i++)printf("%d ",nxt[i]);//输出nxt
    puts("");
    len=s.size();
    for(int i=0;i<len;i++)printf("%d ",extend[i]);//输出extend
    return 0;
}

洛谷上有一道扩展 k m p kmp kmp的模板题:P5410【模板】扩展 KMP

b z o j bzoj bzoj h d u hdu hdu上也有几道,大家可以去看看:

hdu2594 Simpsons’ Hidden Talents

hdu4333 Revolving Digits

深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值