复健打卡第一天

拓展kmp指的是求的一个z数组: 假设这个字符串为a , 长度为len,在这个z数组中,z[i] 表示以 a[i] 开头的子串最多可以匹配前缀的长度。就好像下面这个字符串

a b b c d e a b b a c

0 0 0 0 0 0 3 2 1 1 0

这里的 2 表示以当前字符开头的字符串最对多可以匹配长度为2的前缀字符串

这个 3 表示以当前字符开始的字符串最多可以匹配长度为3的前缀字符串

那么问题来了他用来解决什么问题呢。

比如说给你一个长度为1e6 的字符串,你需要输出以每一个位置开头的字符串最多可以匹配多少长度的前缀字符串这时exkmp(恶心kmp 也就是扩展kmp) 也就派上用处了

对于单一串, 我们定义当前位置开头的字串可以匹配的前缀字符串长度为z[i];

扩展kmp是这样优化的。

我们首先定义一个区间【l,r】 我们暂且将他称为z-box(这个盒子的意义是,表示【l,r】区间的字符串是可以对应长度为(r - l + 1) 的前缀字符串。(很重要)

首先对于每一个i,他所匹配的区间为【i,i+z[i] - 1】; 也就是说 (i+z[i] - 1) 是当前点匹配的右端点我们在这里定义最右边的(i+ z[i] -1 )为维护区间的右端点r,当更新r的位置时,将更新r位置的i置为当前区间的左右端点置为l;

对于这个盒子来说,我们可以很明确的通过定义来找到这个【l,r] 这区间所对应的之前求出来的值,对! 那就是【1,( r- l + 1)]区间的字符串,也就是说,区间【l,r】的字符串是和区间【1,(r-l) +1]区间(也即是前缀)字符串是相同的!,是不是很神奇,这样我们就可以通过前面已经求出的区间来快速更新当前区间的z[i];

比如

  i  1 2 3 4 5 6 7 8 9

     a a a b a a a  b c 

z[i] 9 2 1 0 4
             |
             |

                假设我们当前的4是求出来的那么对应的区间就是 【5,8】
                那么对应的匹配前缀字符串也就是【1,4】,此时假设我们将要求z[6], 我们可以直接借用
                 a[6] 在 a[1~4] 中对应的位置,也就是 (z[6 - 5 + 1] )z[2] 的值来快速更新z[6]的值
                 如果说我们 (6 + z[2] - 1) 的值小于当前的右区间r,我们就可以直接将
                 z[6] = (z[6 - 5 + 1])=  z[2],如果大于的话就暴力向后直接匹配即可,
                 由于每次r最多只会向后走len(字符串的长度) 所以这样匹配的时间复杂度是O(n)的
                 。这也是扩展kmp的强大之处

对于算法流程。

  1. 如果说我们的i在 盒子内(也就是 i<=r) 则我们可以很容易的知道,他已经匹配上的区间为s[i,r] 匹配 s[i-l+1,r-l+1] 其实就是区间 【l,r] 和区间 【1,r-l + 1] 区间只取其【i,r】区间和z-box对应的区间
    此时需要讨论。

    (1). 如果 i + z[i-l+1] - 1 < r ,则表明当前匹配的前缀字符串长度加上i不会超过右区间,则此时z[i] = z[i-l+1];

    (2) 如果 i + z[i-l+1] - 1 >= r ,则表明当前匹配的前缀字符串长度加上i会超过右区间,此时我们先将z[i]更新为(r - i +1)再暴力向后匹配并且更新z[i]

  2. 如果i在盒外,我们直接暴力从i开始枚举即可。

  3. 此时我们已经求出来z[i] 了,此时我们需要判断如果I + z[i] - 1 > r 的话我们就直接更新区间使得(i = I , r = i + z[i] - 1 )即可

对于一个普通的板子就是接下来的代码:

void getextend(char a[], int n)
{
    z[1] = n ;
    for(int i = 2,l ,r = 0 ; i <=n ; i ++)
    {
          //对应步骤1,如果在盒子内并且i+z[i-l+1] <  r的话答案就是z[i-l+1]
          //否者答案就是r-i+1的值,这个可以推导一下可以压缩。
        if(i <=  r)z[i] = min(z[i - l + 1],r - i + 1);
        while(a[1 + z[i]] == a[i + z[i]])z[i]++;    // 对应步骤2。
        if(i + z[i] - 1 > r)l = i ,r = i + z[i] - 1;//对应步骤3
    }
}

这是求一个串的z函数,那么就有同学问了(要是求一个字符串对于另一个字符串的的z函数怎么求) 这里其实我们只要理解了z-box的含义,就可以很容易的想到怎么改这个板子了

void get_z(char a[],int n , char b[], int m)
{
    //这里求字符串a匹配b前缀的z函数。
    for(int i = 2, l , r = 0; i <= n; i ++)
    {
        if(i <= r)z[i] = min(z[i - l + 1],r - i+1);
        //这里我们只需要将z[i]保持在有效范围之内即可。
        while(i + z[i] <= n && 1 + z[i] <= m && b[1 + z[i]] == a[1 + z[i]])z[i] ++ ;
        if(i + z[i] - 1 > r)l = i , r = i + z[i] - 1;
    }
}

(可以参考下b站董晓老师讲的算法)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值