洛谷练习题——高级数据结构篇

目录

1.KMP算法

2.扩展KMP(Z函数)

3.manacher算法


1.KMP算法

【模板】KMP字符串匹配 - 洛谷

题目描述

 输入格式

 输出格式

样例输入 

ABABABC
ABA

样例输出

1
3
0 0 1 

数据范围

下标从1开始。

KMP算法思想

在一次不匹配时,主串不动,拉着模式串继续向后进行匹配。

(1).取最长的相等的前后缀,可以保证不漏解。

(2).通过模式串的前后缀的自我匹配的长度,计算next函数,给j一张表,失配时跳到next[j]的位置继续匹配。

next函数

next[i]:表示模式串P[1,i]中相等前后缀的最长长度。

Pa a b a a b a a a a
ne[1]=0a
ne[2]=1a a
ne[3]=0a a b
ne[4]=1a a b a
ne[5]=2a a b a a
ne[6]=3a a b a a b
ne[7]=4a a b a a b a
ne[8]=5a a b a a b a a
ne[9]=2a a b a a b a a a
ne[10]=2a a b a a b a a a a
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;
}

 思想

双指针:i扫描模式串,j扫描前缀。

初始化,ne[1]=0,i=2,j=0。

每轮for循环,i向右走一步。

(1)如果P[i]!=P[j+1],让j回跳到能匹配的位置,如果找不到匹配的位置,j回跳到0.

(2)若P[i]==P[j+1],让j+1,指向前缀的末尾。

(3).ne[i]=j.

和j+1位置进行比较,是一个预判,进可攻,退可守的思想。

时间复杂度为O(n),因为j的总次数不超过2n。

匹配过程

for(int i=1,j=0;i<=m;i++)
{
    while(j&&S[i]!=P[j+1]) j=ne[j];
    if(S[i]==P[j+1]) j++;
    if(j==m) printf("%d\n",i-n+1);
}

双指针:i扫描主串,j指针扫描模式串。

初始化,i=1,j=0.

每轮for循环,i向右走一步。

(1)如果S[i]!=P[j+1],让j回跳到能够匹配的位置,如果找不到匹配的位置,回跳到0.

(2)如果S[i]==P[j+1],让j向右走一步。

(3)如果匹配成功,输出匹配位置。

时间复杂度

j指针次数不超过2m,所以时间复杂度为O(m)

总时间复杂度为O(n+m)

#include <iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
 
const int N = 100100, M = 1000010;
 
int n, m;
int ne[N];
char s[M], p[N];
 
void get_next()
{
    for (int i = 2, j = 0; i <= n; i ++ )
    {
        while (j && p[i] != p[j + 1]) j = ne[j];//没有推到0并且不相等的话,j指针一直回退
        if (p[i] == p[j + 1]) j ++ ;
        ne[i] = j;
    }
}
 
void kmp()
{
    for (int i = 1, j = 0; i <= m; i ++ )
    {
        while (j && s[i] != p[j + 1]) j = ne[j];//直到找到相等的为止
        if (s[i] == p[j + 1]) j ++ ;
        if (j == n)
        {
            printf("%d\n", i - n+1);
            j = ne[j];
        }
    }
}
 
void print()
{
    for(int i=1;i<=n;i++) cout<<ne[i]<<" ";
}

int main()
{
    cin>>s+1>>p+1;
    n=strlen(p+1),m=strlen(s+1);
    get_next();
    kmp();
    print();
    return 0;
}

2.扩展KMP(Z函数)

【模板】扩展 KMP(Z 函数) - 洛谷

题目描述

输入格式 

两行两个字符串a,b.

输出格式

第一行一个整数,表示 z 的权值。

第二行一个整数,表示 p 的权值。

样例输入

aaaabaa
aaaaa

样例输出

6
21

解释说明

数据范围 

思路

Z函数:对于一个长度为n的字符串S。z[i]表示S中其后缀S[i,n]和前缀S[1,x]的最长匹配长度。

3.manacher算法

【模板】manacher 算法 - 洛谷

题目描述

给出一个只由小写英文字符组成的字符串 S ,求 S 中最长回文串的长度 。

字符串长度为 n。

 输入格式

一行小写英文字符组成的字符串 S。 

输出格式

一个整数表示答案。

样例输入 

aaa

样例输出 

3

数据范围

1<=n<=1.1*10^7.

马拉车算法思想

马拉车算法可以在O(n)的时间复杂度内求出一个字符串的最长回文串

马拉车算法的步骤如下:

(1)改造字符串

在字符串之间和串的两端插入#,改造后,都变成奇回文串,方便统一处理。

奇回文串 aba,改造后 #a#b#a#

偶回文串 abba,改造后 #a#b#b#a#

s[0]='$'是哨兵(边界).

 scanf("%s",a+1);
 int n=strlen(a+1),k=0;
 s[0]='$',s[++k]='#';
 for(int i=1;i<=n;i++)
     s[++k]=a[i],s[++k]='#';
 n=k;

(2)回文半径d[i]

d[i]表示以i为中心的最长回文串的长度的一半(上取整),叫做回文半径。

i123456789
s#a#a#b#a#
d[i]123214121

(3)加速盒子[l,r]

维护右端点最靠右的最长回文串,利用盒子,借助之前的状态来计算新的状态,盒内d[i]可以利用对称点的d值转移,盒外暴力。

i123456789
s#a#a#b#a#
d[i]123214121

(4)算法流程

计算完前i-1个d函数,维护盒子[l,r]

1.如果i<=r(在盒内),i的对称点为r-i+l,

        1.1如果d[r-i+l]<r-i+1,则d[i]=d[r-i+l].

也就是说对称点的回文半径长度小于r-l+1,那么直接可以利用对称点的回文半径进行转移,因为对称点的回文半径没有出r端点,那么此时d[i]=d[r-i+l]一定是回文串,因为没有出r区间,可以保证一定是回文串。

        1.2如果d[r-i+l]>=r-i+1,则令d[i]=r-i+1,从r+1往后进行暴力枚举。

2.如果i>r(在盒外),则从i开始进行暴力枚举。

3.求出d[i]后,如果i+d[i]-1>r,则更新盒子l=i-d[i]+1,r=i+d[i]-1。

void get(char *s,int n)
{
    d[1]=1;
    for(int i=2,l,r=1;i<=n;i++)
    {
        if(i<=r) d[i]=min(d[r-i+l],r-i+1);//在盒子内,找对称点
        while(s[i-d[i]]==s[i+d[i]]) d[i]++;//求出盒子外的回文长度
        if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;//更新区间
    }
}

(5).时间复杂度

内层while循环的总次数决定了总的执行次数,对于while循环,如果条件成立,d++,即盒子的右端点r就会后移若干位,而r<=n,故r右移的总次数不会超过n,故时间复杂度为O(n).

(6).模拟

i=2    d=2     [1,3]

i123456789
s#a#a#b#a#
d[i]12

i=3    d=2    [1,5]

i123456789
s#a#a#b#a#
d[i]123

i=4    d=2    [1,5]

i123456789
s#a#a#b#a#
d[i]1232

i=5    d=1    [1,5]

i123456789
s#a#a#b#a#
d[i]12321

i=6    d=4    [3,9]

i123456789
s#a#a#b#a#
d[i]123214

i=7    d=1    [3,9]

i123456789
s#a#a#b#a#
d[i]1232141

i=8    d=2    [3,9]

i123456789
s#a#a#b#a#
d[i]12321412

i=9    d=1    [3,9]

i123456789
s#a#a#b#a#
d[i]123214121

因为我们又添加了#,相当于添加了一半的字符,那么最长回文子串的长度=新串的最大半径-1.

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e7;
char a[N*2],s[N*2];
int d[N*2];

void get(char *s,int n)
{
    d[1]=1;
    for(int i=2,l,r=1;i<=n;i++)
    {
        if(i<=r) d[i]=min(d[r-i+l],r-i+1);//在盒子内,找对称点
        while(s[i-d[i]]==s[i+d[i]]) d[i]++;//求出盒子外的回文长度
        if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;//更新区间
    }
}

int main()
{
    scanf("%s",a+1);
    int n=strlen(a+1),k=0;
    s[0]='$',s[++k]='#';
    for(int i=1;i<=n;i++)
        s[++k]=a[i],s[++k]='#';
    n=k;
    get(s,n);
    int ans=0;
    for(int i=1;i<=n;i++) ans=max(ans,d[i]);
    cout<<ans-1 <<endl;
    return 0;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值