Bootstrap

KMP算法 及时间复杂度证明

KMP

假设模式串为A,需要匹配的串为B,即在B中找一个子串A。

假设下标从1开始,B长n,A长m。

失配: A i 、 B j A_{i}、B_{j} AiBj发生失配,即 A i A_{i} Ai之前的位置全部匹配成功,但 A i ≠ B j A_{i}\neq B_{j} Ai=Bj匹配失败。

暴力解法:

枚举子串在B中的起始位置,逐位匹配 A 1 A_{1} A1 A n A_{n} An,如果失配,则枚举B的下一个位置重新匹配。

在每个起始位置,逐位匹配的一种方法是,维护两个指针 i , j i,j ij i i i指向 A 1 A_{1} A1 j j j指向 B B B中的起始位置。比较 A i 、 B j A_{i}、B_{j} AiBj,如果相等 i , j i,j i,j 1 1 1;如果不相等,B的起始位置+1,重新初始化。

最坏时间复杂度O(mn)

KMP:

尝试在暴力算法的基础上优化,跳过一些多余的步骤。

考虑 A i 、 B j A_{i}、B_{j} AiBj发生失配,分为两种情况:

  • i = 1 i=1 i=1,在A的最开始发生失配,起始位置后移到 B j + 1 B_{j+1} Bj+1 B j + 1 B_{j+1} Bj+1 A 1 A_{1} A1比较,即 i = 1 , j = j + 1 i=1,j=j+1 i=1,j=j+1
    由于目前还不知道 B j B_{j} Bj之后除了长度之外的信息,所以长度足够的情况下, B j + 1 B_{j+1} Bj+1为起始位置有匹配成功的可能性,不能跳过。

  • i ≠ 1 i\neq1 i=1,此时有 i − 1 i-1 i1位匹配, A 1 A_{1} A1~ A i − 1 A_{i-1} Ai1= B j + 1 − i B_{j+1-i} Bj+1i~ B j − 1 B_{j-1} Bj1
    B j + 1 − i B_{j+1-i} Bj+1i开头的位置匹配失败,接下来应该以 B j − i + 2 B_{j-i+2} Bji+2开头重新匹配。我们考虑能否将其跳过。如果这个位置能够匹配成功,要求 A 1 A_{1} A1~ A n A_{n} An= B j − i + 2 B_{j-i+2} Bji+2~ B j − i + n + 1 B_{j-i+n+1} Bji+n+1

    注意两个匹配等式中存在共同的子串 B j + 1 − i B_{j+1-i} Bj+1i~ B j − 1 ∪ B_{j-1}\cup Bj1 B j − i + 2 B_{j-i+2} Bji+2~ B j − i + n + 1 = B_{j-i+n+1}= Bji+n+1= B j − i + 2 B_{j-i+2} Bji+2~ B j − 1 B_{j-1} Bj1(i<=n,所以j-i+n+1>=j+1>j-1)
    通过它联立两式,得到 A 2 A_{2} A2~ A i − 1 A_{i-1} Ai1 = B j − i + 2 =B_{j-i+2} =Bji+2~ B j − 1 B_{j-1} Bj1 = A 1 =A_{1} =A1~ A i − 2 A_{i-2} Ai2,其中关于 A A A的部分恰好是当前匹配成功串 A 1 A_{1} A1~ A i − 1 A_{i-1} Ai1的长度确定的前缀和后缀,它们相等。这是匹配成功的必要条件。

    当然,通过图形的方式理解也可以:
    在这里插入图片描述

    如果接着向后枚举 B j − i + 3 B_{j-i+3} Bji+3~ B j − 1 B_{j-1} Bj1,同理可得到类似的必要条件:匹配成功串 A 1 A_{1} A1~ A i − 1 A_{i-1} Ai1存在长度确定的相等前后缀。这个确定的长度是逐渐减少的。(我们不需要知道具体的起始位置,见下一段末尾。所以这里没有求明确的长度和起始位置的关系表达式,只要知道它是确定的就好。)因此,如果匹配成功串 A 1 A_{1} A1~ A i − 1 A_{i-1} Ai1不包含特定长度的相等前后缀,就可以跳过对应的位置,如果包含,就有可能匹配成功,需要考虑。并且最长的一组前后缀对应最靠前的无法跳过的位置。如果不存在相等前后缀,则下一个可能的位置是 B j B_{j} Bj(注意前提, i ≠ 1 i\neq 1 i=1)。

    并且,根据前面的联立等式,我们已经知道从开始位置到 B j − 1 B_{j-1} Bj1这一段已经匹配上了,所以指针 i i i不需要初始化为1, j j j不需要初始化为起始位置,而是从 i = l e n + 1 , j = j i=len+1,j=j i=len+1,j=j不变接着比较。对于不存在相等前后缀的情况,下一次是 B j 、 A 1 B_{j}、A_{1} BjA1比较,可以把 l e n len len视为0,则表达式统一。注意 i , j i,j i,j的新值只与最长前后缀的长度直接相关,不需要明确求出起始位置。

至此我们得出结论,对于失配位置 ( i , j ≠ 1 ) (i,j\neq1) (i,j=1),下一组比较的位置是 ( l e n + 1 , j ) (len+1,j) (len+1,j)

现在的问题是如何快速求 l e n + 1 len+1 len+1

n e x t [ i ] = l e n + 1 next[i]=len+1 next[i]=len+1表示在 A i A_{i} Ai处失配,下一个 i i i的位置,其中 l e n len len A 1 A_{1} A1~ A i − 1 A_{i-1} Ai1的最长相等前后缀的长度。
根据前面推理,可能的最长的相等前后缀中前缀为 A 1 A_{1} A1~ A i − 2 A_{i-2} Ai2,长度为 l e n = i − 2 len=i-2 len=i2。故 0 ≤ l e n ≤ i − 2 0\leq len \leq i-2 0leni2,即不能为整个前 i − 1 i-1 i1

n e x t [ 1 ] = ∀ f l a g < = 0 next[1]=\forall flag<=0 next[1]=flag<=0作为标记位,无实际意义。对应 i = 1 i=1 i=1的情况,需要特殊处理

n e x t [ 2 ] = 0 + 1 = 1 next[2]=0+1=1 next[2]=0+1=1,因为 A 1 A_{1} A1只有一位,必然没有相等前后缀。再次注意:这里前后缀不能是串本身 。

对于 i ≥ 2 , n e x t [ i ] = k i\geq 2,next[i]=k i2,next[i]=k,则有至多 A 1 A_{1} A1~ A k − 1 = A i − k + 1 A_{k-1}=A_{i-k+1} Ak1=Aik+1~ A i − 1 A_{i-1} Ai1(注:k=1时视为空串)

  • 如果 A i = A k A_{i}=A_{k} Ai=Ak,那么 A 1 A_{1} A1~ A k = A i − k + 1 A_{k}=A_{i-k+1} Ak=Aik+1~ A i A_{i} Ai l e n = k , n e x t [ i + 1 ] = k + 1 len=k,next[i+1]=k+1 len=k,next[i+1]=k+1
  • 否则
    • 如果 k = 1 k=1 k=1,说明 A 1 A_{1} A1~ A i A_{i} Ai没有相等前后缀, n e x t [ i + 1 ] = 1 next[i+1]=1 next[i+1]=1
    • 否则要找到次长的 t < k t<k t<k,使得 A 1 A_{1} A1~ A t − 1 = A i − t + 1 A_{t-1}=A_{i-t+1} At1=Ait+1~ A i − 1 A_{i-1} Ai1,然后将 A i 与 A t A_{i}与A_{t} AiAt比较,如果相等则 n e x t [ i + 1 ] = t next[i+1]=t next[i+1]=t,否则又要再找下一个次长,注意这是一个递归的问题。
      由于这两个式子又有公共部分 A i − t + 1 A_{i-t+1} Ait+1~ A i − 1 A_{i-1} Ai1 A 1 A_{1} A1~ A t − 1 = A i − t + 1 A_{t-1}=A_{i-t+1} At1=Ait+1~ A i − 1 = A k − t + 1 A_{i-1}=A_{k-t+1} Ai1=Akt+1~ A k − 1 A_{k-1} Ak1,注意1、3项是 A 1 A_{1} A1~ A k − 1 A_{k-1} Ak1的相等前后缀。所以t是k的最长相等前后缀的长度+1,即 t = n e x t [ k ] t=next[k] t=next[k]
//预处理next数组
next[1]=-1;
next[2]=1;
int i=2;
int len = next[2];
while(i<m){
	if(A[len]==A[i]){
		next[i+1] = len + 1;
        i++;
        len = next[i];//初始可能的最大长度
        //简写:next[++i] = ++len;
	}
	else if(len == 1){//没有相等前后缀
        next[++i] = 1;
        //len不变
    }
    else{
		len = next[len];//下一个可能的长度
	}
}

//匹配
int i=1,j=1;
while(j<n){
	if(A[i]==B[j]){//匹配
		i++;j++
    }
    else if(i==1){//失配,i=1
        j=j+1;
    }
    else{//失配,i!=1
		i=next[i];
    }
}

时间复杂度

匹配

新方法的基本动作可以视为比较-移动指针,比较是O(1)的;移动方式有 ( i , j ) → ( i + 1 , j + 1 ) 或 ( l e n + 1 , j ) 或 ( 1 , j + 1 ) (i,j)\to(i+1,j+1)或(len+1,j)或(1,j+1) (i,j)(i+1,j+1)(len+1,j)(1,j+1)三种,复杂度都是O(1)。(len+1=next[]是预处理的)

对于移动的次数:

  • 第一、三种方式:由于只有第一、三种方式影响 j j j,令 j = j + 1 , j j=j+1,j j=j+1,j又是控制变量,所以合起来移动的次数为O(n)数量级。

  • 第二种方式, l e n ≤ i − 2 len\leq i-2 leni2,故 l e n + 1 < i len+1<i len+1<i会使i减少。或者也可以从图中看出。

    第三种方式也会使i减少。
    那么, i i i的变化只有第一种方式每次使 i i i加1,第二、三种方式使 i i i减少,又初始 i = 1 i=1 i=1,且 i ≥ 1 i\geq 1 i1始终成立
    那么第一种方式移动次数 M 1 = i M_{1}=i M1=i的总增加量>= i i i的总减少量>=第二种方式移动次数 M 2 M_{2} M2,所以 M 2 M_{2} M2也是O(n)数量级的。

而比较的次数等于二者之和,也是 O ( n ) O(n) O(n)

所以,匹配过程的复杂度为 O ( n ∗ 1 ) = O ( n ) O(n*1)=O(n) O(n1)=O(n)

预处理next

注意 l e n len len有两种O(1)变化:

  • i i i一起 + 1 +1 +1
  • 减小

其中i是控制变量,最多增加m次;与上面同理,len减小的次数为O(m),比较次数等于两种变化之和也是O(m)

整体复杂度 O ( m ∗ 1 ) = O ( m ) O(m*1)=O(m) O(m1)=O(m)

故整个算法的复杂度为 O ( m + n ) O(m+n) O(m+n),考虑到n<m时显然不匹配,个人认为可以写作 O ( n ) O(n) O(n)

;