FAST PATTERN MATCHING IN STRINGS*(KMP)(译)

    FAST PATTERN MATCHING IN STRINGS*

 
 
摘要:
  提出了一个运行时间与字符串长度成比例,在字符串中找到所有给定字符串的算法。这个比例常数足够小使得这个算法能在实际中使用,这个程序也可以进行拓展去解决一些更一般的模式匹配问题。这个算法的一个理论应用表明:串联的偶数长的回文,即,语言{ a a R }*能够在线性时间被识别。其他在平均情况下运行的更快的算法也被考虑进去了。
关键字:模式,文本编辑,模式匹配,单词查找树存储器,搜索,字符串周期,回文,优化算法,斐波纳契串,正则表达式。
  文本编辑程序经常被要求去从头到尾搜索一个字符串的字符,以寻找一个给定的“模式”串的实例。我们希望找到所有的(或者仅仅是最左边的)那个模式作为文本一个连续的子串出现的位置。比如,c a t e n a r y 包含模式 t e n ,但是我们不将c a n a r y 视作其的一个子串。
  搜索一个匹配的模式的显然方式是在文本每个可能的位置尝试进行搜索。只要发现错误的字符,就放弃在该位置的搜索。但是这个过程有可能是效率低下的,比如当我们在 a a a a a a a a a a a a a a b 搜索a a a a a a a b.当模式是 a n b ,文本是 a 2 n b 时,我们会发现我们做了 n + 1 2 次字符比较。而且,当我们检查它时,传统的过程会牵涉到“回退”输入文本,这会增加恼人的困难(当我们考虑经常包含的缓冲操作时).
  在这篇论文中,我们描述一个没有“回退”,在 O ( m + n ) 单位时间内,在长度为n的文本中找到模式长为m的模式匹配算法。如果输入是从外部文件中读取,那么这个算法仅仅需要 O ( m ) 的额外内存空间,在输入连续单字符之间仅仅需要花费 O ( l o g m ) 单位时间。这些“O”公式所隐含的所有比例常数都与字母表大小无关
  我们应当首先考虑算法概念简单但低效的形式。这篇论文的第三、四小节讨论提高效率和使该算法适应其他问题的一些方法。第5节建立基本理论(注:略去一部分倾向于数学的内容),第6节用这个算法去证明一个猜测(一个特定的上下文无关语言不能在线性时间内被识别)是虚假的。第7节讨论最初的算法和它与其它近期工作的联系。最后的第8节讨论更多在模式匹配方面近期的工作。(注:略去7、8节)

1.Informal development

  如果我们把模式放在文本上,并以某种方式把它移到右边,那么这种模式匹配方法背后的想法可能是最容易掌握的。比如考虑在文本 b a b c b a b c a b c a a b c a b c a b c a c a b c 中搜索模式 a b c a b c a c a b .开始我们将模式放在最左边,准备去扫描输入文本最左边的字符:
    a b c a b c a c a b
    b a b c b a b c a b c a a b c a b c a b c a c a b c
   
  箭头指示当前文本字符。由于这里指向 b 与 a 不匹配,我们向右移动一个间隔,然后移动到下一个输入字符:
    a b c a b c a c a b
   b a b c b a b c a b c a a b c a b c a b c a c a b c
    
  现在我们有了一次匹配,因此当我们扫描接下来的几个字符时,模式保持不变。很快,我们得到了另一次不匹配:
     a b c a b c a c a b
    b a b c b a b c a b c a a b c a b c a b c a c a b c
        
  在这里,我们已经匹配了前三个模式字符但是第四个不匹配。因此,我们知道了,到目前为止输入已扫描部分最后的的四个字符是a b c x ( x != a ).我们没有必要记住已经扫描过的字符,因为我们在模式中的位置产生了足够的信息去重现它们。在这种情况下,无论 x 是什么(只要不是a),我们推断出模式能够立即向右移动4位。移动一位、二位、三位我们不可能得到一次匹配。
  移动后,我们得到一个局部匹配,这次我们会在模式第八位上得到一次不匹配。 
         a b c a b c a c a b
    b a b c b a b c a b c a a b c a b c a b c a c a b c
               -↑
  到目前为止输入已扫描部分最后的的八个字符是a b c ab c a x ( x != c ).因此模式应该向右移动三位。
          a b c a b c a c a b
  b a b c b a b c a b c a a b c a b c a b c a c a b c 
            
   我们尝试使(文本当前字符b)与新的模式字符(c)匹配,但是我们还是得到”不匹配“。因此我们将模式移动四位(不是三位、不是五位)。然后,产生一次匹配,继续扫描直到在模式第八个字符处产生另一次不匹配:
              a b c a b c a c a b
  b a b c b a b c a b c a a b c a b c a b c a c a b c 
                   
  我们再一次向右移动三位,产生一次匹配,最终我们发现完整的匹配。
                 a b c a b c a c a b
  b a b c b a b c a b c a a b c a b c a b c a c a b c 
                       
                       
  这个例子详细的描述表明,如果我们有一个辅助的表(精准的告诉,当在模式第j个字符pattern[ j ]产生不匹配时,我们要将模式移动多少位),模式匹配过程将会运行的更有效率。让 n e x t [ j ] 储存,(在产生不匹配时)我们要下一步要比较模式字符的位置,以便我们相对文本移动 j n e x t [ j ] .这个例子的辅助表如下所示:
       j   = 1 2 3  4  5 6 7 8 9 10 
   pattern[ j ]  = a b c a b c a c a b
    next[ j ]    = 0 1 1 0 1 1 0 5 0 1
(注意 n e x t [ j ] = 0 意味着我们要移动整个模式越过当前文本字符).我们稍后将讨论如何事先计算这个(辅助)表.幸运的是,表的计算十分简单、我们将看到它们只需要 O ( m ) 步。
  在扫描的每步,我们要么移动文本指针,要么移动模式(最多 n 次)。因此在 n e x t 表建立后,最多需要执行2*n步。当然模式自身并不是真正的移动,我们简单的维护变量 j 来执行这个不可少的操作。

2.Programming the algorithm

  模式匹配有(如下)一般形式:
       place pattern at left;
       while pattern not fully matched
          and text not exhausted do
          begin
             while pattern character differs from
               current text character
               do shift pattern appropriately
             advance to next character of text
            end;
为了方便起见,我们假定输入由数组 t e x t [ 1 : n ] 表示,模式由 p a t t e n [ 1 : m ] 表示。我们应该假设m > 0 ,即模式非空。我们声明两个整数变量 k j ,以便 t e x t [ k ] 表示当前文本当前字符, p a t t e r n [ j ] 表示对应的模式中的字符。于是,模式与文本第 p + 1 到第 p + m 位对齐( k = p + j )。那么,上面的程序采用如下简单形式:

          j := k := 1;
          while j m and k n do
             begin
              while j > 0 and text[ k ] ≠ pattern [ j ]
                 do j:= next[ j ];
              k:= k + 1; j:= j + 1;
            end;
如果在程序结束的时候 j > m ,那么最右边的那次匹配(从位置 k - m 开始到 k -1)已经被找到。但是如果 j ≤ m,那么文本已经被用尽。(上文的“and”操作这里表示“条件加”,除非 j > 0,” t e x t [ k ] p a t t e r n [ j ] ”才会进行运算。)这里的这个程序有一个古怪的特点,换句话说内循环“ j := n e x t [ j ] ”执行的次数不会经常多于外循环“ k := k + 1 ”。事实上,由于内循环通常有点不经常执行,因为模式通常比文本指针的右移次数少。
  为了严格的证明上述算法是正确的,我们可以使用下述不变关系:令 p = k j (即,在上面我们假设的对齐中,与模式第一个字符对应的文本字符前面第一个字符的位置)。那么我们得到 t e x t [ p + i ] = p a t t e r n [ i ] (1 ≤ i ≤ j ).(即,如果j > 0,我们已经匹配模式前j - 1个字符 ).但是对于 0 ≤ t ≤ p 我们会得到 t e x t [ t + i ] p a t t e r n [ i ] .(1 ≤ i ≤ m ).即,在p左边不可能存在完全匹配整个模式的情况。
 当然,如果我们能够计算 n e x t 表,(使得当我们执行 j := n e x t [ j ] 上面关系保持不变)程序当然是正确的。现在让我们来看看 n e x t 表的计算。当程序执行j:=next[ j ]时,我们能够知道:j > 0 ,(输入到目前被扫描过的部分)最后j个字符(包括text[ k ])是:
          p a t t e r n [ 1 ] . . . . . . p a t t e r n [ j 1 ] x .( x ≠ pattern[ j ]).
我们所想的是找到最少移位(使得这里的字符能够尽可能与移动后的模式匹配)。换句话说,我们想 n e x t [ j ] 成为小于j最大的i,这样输入最后的 i个字符是:
          p a t t e r n [ 1 ] . . . . . . p a t t e r n [ i 1 ] x .( pattern[ i ] ≠ pattern[ j ]).
(如果不存在这样的i,我们令 n e x t [ j ] = 0 .) n e x t [ j ] 的这一定义,很容易验证: t e x t [ t + 1 ] . . . t e x t [ k ] p a t t e r n [ 1 ] . . . p a t t e r n [ k 1 ] (k - j ≤ t < k - next[ j ]).因此上述阐述的关系的确是不变的,我们的程序是正确的。
  现在我们必须勇敢的面对前面推迟了的问题 ———事先计算 n e x t [ j ] . 如果我们在 n e x t [ j ] 定义中(先)不要求 p a t t e r n [ j ] p a t t e r n [ i ] ,这个问题会变得更简单。 我们应当先考虑简单的问题。我们令 f [ j ] 存储小于 j 最大 的 i ,使得 p a t t e r n [ 1 ] . . . p a t t e r n [ i 1 ] = p a t t e r n [ j i + 1 ] . . . p a t t e r n [ j 1 ] .由于当 i = 1 时,没有无意义,我们总是有当 j > 1 时,f[ i ] ≥ 1.我们还约定 f [ 1 ] = 0 . 在第一节的例子有如下 f 表:

      j   = 1 2 3  4  5 6 7 8 9 10 
  pattern[ j ]  = a b c a b c a c a b
    f[ j ]    = 0 1 1 1 2 3 4 5 1 2
如果 p a t t e r n [ j ] = p a t t e r n [ f [ j ] ] ,那么 f [ j + 1 ] = f [ j ] + 1 .否则,我们能使用本质上与上面相同的模式匹配算法来计算 f [ j + 1 ] .(注意求解 f [ j ] 问题与匹配算法的不变关系的相似性,我们的程序计算小于或者等于k的最大的j,使得 p a t t e r n [ 1 ] . . . p a t t e r n [ j 1 ] = t e x t [ k j + 1 ] . . . t e x t [ k 1 ] .因此我们可以把前面的”技术“运用到现在的问题上来。)
下面这段程序将计算 f [ j + 1 ] (假设 f [ j ] n e x t [ 1 ] . . . . n e x t [ j 1 ] 已经被计算出来了):
    
     t := f[ j ];
      while t > 0 and pattern[ j ] ≠ pattern[ t ]
        do t:= next[ j ];
      f[ j + 1 ] := t + 1;
   如前所示,我们证明了这个程序的正确性。我们能设想模式的两个副本,一个滑向另一方的右边.比如,我们已经确定f[ 8 ] = 5(在上面的例子中),下面我们计算f[ 9 ].此时情形如下:
     
           a b c a b c a c a b
     a b c a b c a c a b
                     
   由于 p a t t e r n [ 8 ] ≠ b,我们把上面的副本向右移动(我们知道最近扫描的字符是a b c c a b x x ≠ b))。 n e x t 表告诉我们向右移动四位,得到:
                 a b c a b c a c a b
     a b c a b c  a c a b
                  
   (这里再次不匹配)。下一次移位使t=0(next[ 1 ] = 0 ),所以f[ 9 ] = 1.
   一旦我们知道如何计算 f ,就差一小步去计算 n e x t [ j ] .如下表所示(j > 1 ):
       
 

n e x t [ j ]   = { f [ j ] , if pattern[ j ] ≠ pattern[ f[ j ] ]  n e x t [ f [ j ] ] , if pattern[ j ] = pattern[ f[ j ] ] 
       
         
因此我们能够用下面这个程序计算 n e x t 表(不需要在内存中储存f[ j ]的值)。
             j:= 1;t:= 0;next[ 1 ] := 0;
              while j < m do
                 begin comment t:= f[ j ];
                   while t > 0 and pattern[ j ] ≠ pattern[ t ]
                      do t := next[ t ];
                   t:= t + 1 ; j := j + 1;
                   if pattern[ j ] = pattern [ t ]
                   then next[ j ] := next[ t ];
                   else next[ j ] := t;
                 end.
这个程序花费 O ( m ) 单位时间,同样的原因,匹配程序需要 O n 单位时间:在最内层循环操作 t := n e x t [ t ] ,总是将上面的副本向右移动,因此它最多总共执行m 次。(一个(证明运行时间有界于常数m)稍微不同的方式是去观察:变量t从零开始,它增加了m - 1次(每次增加1),此外,它的值保持非负。因此,操作 t := n e x t [ t ] (总是降低t)最多能被执行 m - 1 次).
 总结我们到目前所说的:可以利用两种思路有效地扫描文本字符串。我们可以事先计算”转移”,具体说明(当在模式的第 j 个字符不匹配时)如何移动给定的模式。我们能用相同的原理事先高效的计算“转移”(模式紧靠模式自身进行移动)(注:这里指是上文一个模式的两个副本进行匹配的过程)。

3.Gaining efficiency

  我们已经以一种很容易被证明是正确的形式,提出了模式匹配算法。但是,像经常发生的一样,这种形式并不是很高效的。事实上,这个算法很可能与朴素算法在实际数据上不具有竞争力,因为尽管朴素算法在最坏的情况下时间复杂度的阶是m * n 而不是(m+ n),但是出现最坏情况的概率是相当微小的。另一方面,新算法的一个好的实现应该运行的快得多,因为新算法在局部匹配之后不需要回退。
  我们不难看到使上述新算法中效率低下的根源:当字母表较大时,将很少有一次匹配,这个程序将花费大量的时间(相当笨拙的)发现: t e x t [ k ] p a t t e r n [ 1 ] (k = 1 , 2 , 3 ….).当 j = 1 , t e x t [ k ] p a t t e r n [ 1 ] 时,算法会执行 j:= next[ 1 ] = 0 ,接着发现j = 0 ,接着k 加 1,接着又再次将 j 设为 1 ,接着测试 1是否小于或等于m,然后测试 1 是否大于 0。显然,算法最好把 j = 1的情形 当成一个特例(单独处理)。
  算法也会花费不必要的时间来测试是 j > m 还是 k > m.我们假设一个完全匹配的模式后面存在一个没有与之匹配的字符“@”,即 p a t t e r n [ m + 1 ] = ”@”(next[ m + 1 ] = -1 ).“测试j < 0”可以插入到代码中不太频繁执行的部分中去。同样,我们可以假设 t e x t [ n + 1 ] ="   ⊥" (另一个没有与之匹配的字符),和 t e x t [ n + 2 ] = p a t t e r n [ 1 ] .以便“测试k > n ”不需要被经常执行。
  下面这种形式的算法包含了上述细微的改良:
  
                    a:= pattern[ 1 ];
                    pattern[ m + 1 ]:= ‘@’;next[ m + 1 ]:= -1;
                    text[ n + 1 ]:= ‘⊥ ‘;text[ n + 2 ]:= a;
                    j:=k:=1;
                  get started :comment j = 1;
                    while text[ k ] ≠ a and do k := k + 1;
                    if k > n then go to input exhausted;
                  char matched :j:= j + 1;k:= k + 1;
                  loop :comment j > 0;
                      if text [ k ] = pattern[ j ] then go to char matched;
                      j:=next[ j ];
                      if j = 1 then go to get started;
                      if j = 0 then
                        begin
                          j:=1;k:=k + 1;
                          go to get started;
                        ends;
                      if j > 0 then go to loop;
                  comment text[ k - m ] through text[ k - 1 ] matched;
这个算法通常比朴素算法运行的更快。最坏情况发生在当我们尝试在一个全是 a 字符的长的字符串中,搜索模式 a b 时。类似地,事先准备 n e x t 表也可以加快这个程序。
在文本编辑中,模式通常较短,以便最有效的将模式直接翻译成机器码(隐含地包括 n e x t 表).
例如,在第一节的模式能够被编译成与下面等效的机器语言:
                       L 0 :   k:= k + 1;
                       L 1 : if text[ k ] ≠ a then go to L 0 ;
                           k:= k +1;
                         if k > n then go to input exhausted;
                       L 2 : if text[ k ] ≠ b then go to L 1 ;
                           k:= k + 1;
                       L 3 : if text[ k ] ≠ c then go to L 1 ;
                           k:= k + 1;
                       L 4 : if text[ k ] ≠ a then go to L 0 ;
                           k:= k + 1;
                       L 5 : if text[ k ] ≠ b then go to L 1 ;
                           k:= k + 1;
                       L 6 : if text[ k ] ≠ c then go to L 1 ;
                           k:= k + 1;
                       L 7 : if text[ k ] ≠ a then go to L 0 ;
                           k:= k + 1;
                       L 8 : if text[ k ] ≠ c then go to L 5 ;
                           k:= k + 1;
                       L 9 : if text[ k ] ≠ a then go to L 0 ;
                          k:= k + 1;
                       L 10 : if text[ k ] ≠ b then go to L 1 ;
                          k:= k + 1;

  因为它实质上(前文已经提出)是对于j的所有值建立了一个特例(单独处理),所以它将运行的稍微快一点。
  这是一个奇怪的事实——人们常常认为新的算法应该比原来朴素的算法运行的更慢。即使新算法执行更少的工作。由于起初从概念上理解新算法是很困难的,通过与其他算法的对照(相同长度输入),我们觉得计算机也会有概念上的困难——我们期望机器在如此微妙的指令下运行得更慢!

4.Extensions

  到目前为止,我们的程序仅仅是去找到最左边的”匹配”。然而,很容易看到如何修改这个程序,使得我们能依次找到所有的“匹配”:我们可以计算长度为m + 1的”扩展”模式(添加 p a t t e r n [ m + 1 ] = “@”后)的 n e x t 表。接着,在设置 n e x t [ m ] 为 -1之前,我们设置 r e s u m e := n e x t [ m + 1 ] .在找到一个匹配并执行任何处理该匹配所需的操作之后,下列操作序列将使得匹配正确地重新启动:
            j := r e s u m e ; g o t o l o o p ;
(我们假设在此期间,文本不会改变,注意 r s u m e 不能是0)。
另一个方法是保持 n e x t [ m + 1 ] 不变,不要把它改成1,而是声明两个整形数组 h e a d [ 1 : m ] l i n k [ 1 : n ] (其中元素均被初始化为0)。在“ c h a r m a t c h e d ”处,插入代码( l i n k [ k ] = h e a d [ j ] ; h e a d [ j ] := k ; ) 。而$“if j > 0 then”,可以从程序中删除出去。这种形式链接着,所有模式中(与输入文本相匹配的)前 j个 字符的地址(1 ≤ j ≤ m).
  另一个简单的修改将找到模式的最长初始匹配。即,最大的 j 使得文本中存在 p a t t e r n [ 1 ] . . . p a t t e r n [ j ] .
  在实际上,文本的字符经常被储存在字中(我们常说b个字符构成一个字)。计算机的机器体系结构经常使得机器自身获取单个字符时不怎么方便。当在这类机器上执行效率很重要时(n很大),一种做法是执行b独立搜索(一种对一个字中,模式第一个字符的每种可能对齐方式进行一次搜索的搜索方式(共b次))。这些搜索可以通用适当的“化装”,把整个字当作“supercharacters”对待(整个字当成一个整体看待),用这种方式来代替,与单独的字符一起工作方式(不需要单独读取它们)。由于上面描述的算法的效率不取决于字符表的大小,它非常适合采用这种或与之类似的做法。
有时我们会想依次匹配两个或者更多的模式,发现第一个发生,其次是第二个,…。通过连续不断地搜索,这个问题很容易解决。总运行时间阶数将是 n加各个模式总长度。
我们也可能想并行的匹配两个或者更多的模式(在其中任意一个模式完全匹配时,尽早的停下来).对于这类搜索可以用多个 n e x t p a t t e r n 表来完成(每个拥有单独的一个j)。但是这样将使得运行时间变为 k n + (k个模式)。Hopcroft和Karp 观察到(未发表),我们的模式匹配算法可以进行扩展,使得同时搜索多个模式时运行时间与 n + 字符表大小 * 模式总长度成正比。这些模式组合成一个“单词查找树”,这些节点代表所有的一个或多个模式的初始子串,而树的分支指明适当的后继节点(分支可以看作是关于输入文本下一字符的函数)。比如,如果这里有四个模式 a b c a b , a b a b c , b c a c , b b c ,对应的单词查找树如Fig. 1 所示.

node substrings if a if b if c
0 1 7 0
1 a 1 2 0
2 ab 5 10 3
3 abc 4 7 0
4 abca 1 abcab bcac
5 aba 1 6 0
6 abab 5 10 ababc
7 b 1 10 8
8 bc 9 7 0
9 bca 1 2 bcac
10 bb 1 10 bbc

                  F I G .1
  我们能够通过概括我们前面计算 n e x t [ j ] 的思想,来高效的建造这样单词查找树。Aho 和 Corasick (独立地发现了该算法)在参考文献【2】中讨论了该问题的细节和更多的细微的改进。(注意:该算法依赖字符表大小.。如果我们希望如果我们想保持 n 的系数与k无关,这种依赖性是固有的.因为k个模式每个可能包含一个 独一无二的字符。) 将此方法与将LR(0)解析算法应用于正规文法( S a S | b S | c S | a b c a b | a b a b c | b c a c | b b c )时发生的情况进行比较是非常有趣的。

5.Theoretical considerations

如果输入文件是“实时”读取的,我们可能会对连续输入之间的长延迟感到很反感。在这章我们将要证明操作“ j := n e x t [ j ] ”执行次数,(在每次k增加之前)有界于 一个近似于 l o g ϕ m函数( ϕ = (1 + 5 )/2 1.618(黄金分割比例) )。 这个界限是最好的。我们将用小写的拉丁字母表示字符,小写的希腊字母 α β 、….来表示字符串,用 ξ 来表示空串,用 | α | 表示 α 的长度。因此 | α | = 1表示所有字符; | α β | = | α | + | β | | ξ | = 0;我们也经常用 α [ k ] 表示 α 的第k个字符(1 ≤ k ≤ | α | ).
  作为理论讨论上的一个热身,让我们考虑斐波那契字符串(被证明了:对于上述算法,这是特别病态的一个模式)。斐波拉契串的定义如下:
   ( 1 )       ϕ 1  = b;    ϕ 2 = a; ϕ n = ϕ n 1 ϕ n 2   ( n ≥ 3).
 例如, ϕ = ab, ϕ 4 = aba, ϕ 5 = abaab. | ϕ n | 在数值上与斐波拉契数列中第n项 F n 相等, ϕ n 包含一个无限长的字符串 ϕ 的前 F n 个字符。
 考虑模式 ϕ 8 ,该模式的 f [ j ] n e x t [ j ] 如Table 1 所示。
                   TABLE 1
     j = 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
  n e x t [ j ] =a b a a b a b a a b  a  a b  a b a  a b a b  a
  f [ j ]   = 0 1 1 2 2 3 4 3 4 5 6  7  5 6  7 8  9 10 11 12 8
n e x t [ j ]  = 0 1 0 2 1 0 4 0 2 1 0  7  1  0 4 0  2 1  0 12 0
如果我们伸长这个模式到 ϕ ,我们可以得到关于无限长的 f [ j ] n e x t [ j ] 序列.(它们表现出相同的共性)。用归纳法我们能够证明这一共性:
( 2 )          f [ j ] = j - F k 1 .( F k ≤ j ≤ F k + 1
这一共性是缘于斐波拉契串非凡的近交换性:
(3)          ϕ n 2 ϕ n 1  = c ( ϕ n 1 ϕ n 2 ). ( n ≥ 3 )
这里 c 表示转换最右边的两个字符.比如, ϕ 6 = abaab ○ aba,对应的 c ( ϕ 6 ) = aba ○ abaab.(” ○ “表示连接符号)。当 你 n = 3时,等式3显然成立。当 n > 3 时,我们用归纳法有 c ( ϕ n 2 ϕ n 1 ) = ϕ n 2 c ( ϕ n 1 ) = ϕ n 2 ϕ n 3 ϕ n 2 = ϕ n 1 ϕ n 2 .因此, c ( ϕ n 2 ϕ n 1 )= c ( c ( ϕ n 1 ϕ n 2 ))= ϕ n 1 ϕ n 2 .
  等式3意味着 : n e x t [ F k 1 ] = F k 1 1 . (k ≥ 3).因此如果我们当 j = F 8 1 = 20 时有一次不匹配,那么我们的程序可能我们的算法可能逐次执行 j := n e x t [ j ] ,将j的值依次设置为20,12,7,4,2,1,0。 由于 F k 的值是向最近过的整数四舍五入,可能有多达   l o g ϕ m 连续的迭代操作(即执行 j := n e x t [ j ] 循环操作)。
  我们现在即将表明,(出现)斐波拉契串时实际上是这个(算法)面对的最坏的情形。即, l o g ϕ m 是(这个问题)一个上界。首先,让我们了解一个在字符串中周期的概念。如果
(5)                  α [ i ] = α [ i + p ] ,(1 ≤ i ≤ | α [ i ] | ) - p .
我们说 p α 的一个周期。  
很容易看到, p α 的一个周期当且仅当
(6)                    α  = ( α 1 α 2 ) k α 1  .(for some k ≥ 0)
( | α 1 α 2 | = p , α 2 ξ ).相当于, p α 周期当且仅当
(7)              α θ 1 = θ 2 α . (对于一些使得 | θ 1 | = | θ 2 | = p θ 1 , θ 1 ).
条件(6)说明(7)中 θ 1 = α 2 α 1 θ 2 = α 1 α 2 . 条件(7)说明在(6)中(定义 k = | α | p )我们可以观察到:如果k > 0,那么 α = θ 2 β 表明 β θ 1 = θ 2 β β p = k 1 .因此,通过归纳地推理出, α = θ 2 k α 1 (对于一些使得 | α 1 | < p α θ 1 = θ 2 α 1 α 1 ).写作 θ 2 = α 1 α 2 服从(6).
  一旦我们考虑转移模式,我们就会清楚周期性与该算法的相关性。如果 p a t t e r n [ 1 ] . . . p a t t e r n [ j 1 ] = α , p a t t e r n [ 1 ] . . . p a t t e r n [ i 1 ] = β ,那么我们有
(8)                  α = β θ 1 = θ 2 β  ( θ 1 = θ 2 = j i )
因此移动的位数j - i 是 α 的一个周期。
  在我们算法中语句 j = n e x t [ j ] 更进一步的表明: θ 1 的第一个字符与 p a t t e r n [ j ] 不等。我们假设 β 与自身匹配过程中,在其上面的副本移动后“产生”了 γ ,即:
(9)                 β = γ ψ 1 = ψ 2 γ ψ 1 的第一个字符与 θ 1 的第一个字符不同)
我们现在要证明:
(10)                | α | > | β | + | γ |
(注:下面是证明过程)我们假设 | β | + | γ | | α | ,那么在 β γ 之间会有一段重叠部分 d = | β | + | γ | | ψ | (当 α = β θ 1 = θ 2 ψ 2 γ 时 ).因此, θ 1 的第一个字符是 γ [ d + 1 ] 。类似的,在 β γ 之间会有一段重叠部分 d (当 α = θ 2 β = γ ψ 1 θ 1 时),因此, ψ 1 的第一个字符是 β [ d + 1 ] .由于上文已经提到: ψ 1 的第一个字符与 θ 1 的第一个字符不同,所以我们得到 γ [ d + 1 ] β [ d + 1 ] 。与(9)矛盾,所以(10 )成立,这直接导致了以下上文中已经宣布了的结果:
  定理:当扫描一个文本字符时,语句“ j := n e x t [ j ] ”连续执行的次数最多为 1 + l o g ϕ m
  证明:让 L r 表示在上面讨论中(使得连续的r移位序列可能存在的)最短的字符串 α 的长度。那么 L 1 = 0 , L 2 = 1 ,根据(10)我们有: | β | L r 1 , | γ | L r 2 .因此通过对r归纳,得到L2Fr+11.现在如果r移位发生,有mFr+1ϕr1
  即使用f[j]代替next[j],第二章中算法任会在线性时间内正确运行。但是上面定理的类似物却会变成错误的。例如,模式an产生f[j]=j1( 1 ≤ j ≤ m);因此如果我们将am(模式)与am1ba(文本)相匹配,用f[j]代替next[j],在k增加到m + 1之前,当出现text[ m ] ≠ pattern[ m ]不匹配时,总是跟随着m次j:=f[j]text[m]pattern[j]之间m-1次多余的比较。
  ***********************************这里略去一部分*************************************
 

6.Palindromes.

  一个计算复杂性理论中最突出的悬而未决的问题是“确定给定长度为n字符串是否属于一个给定的上下文无关语言需要多长时间”。多年以来,对于一个一般地的上下文无关语言(n → ∞),这个问题一个最好的上界是O(n3).L.G.Valiant最近已经将此降低为O(nlog27)。另一方面,对于任何特定语言来说,这个问题不需要超过n个时间单位。O(n)O(n2.81)之间的巨大差距应该被关闭(缩小)。几乎没有人认为,最终的答案是O(n).
  让 表示有限的字母表,表示在上面的字符串,让
          P= {ααR|α}
αR表示α的逆转,即,(α1α2...αn)R = αn...α2α1 .在P中的每一个字符串π是一个偶数长的回文,相反的,每一个在上的偶数的回文都在P中.人们从前普遍认为,语言P中的“ 被派任为主角的偶数回文”(即palstars(注:这里这个单词问了下度娘,还是没结果,暂定就用单词本身来表示)的集合π1...πn(每个πi都在P中))在一台随机存取计算机上不可能在O(n)步内被识别。
发现这种语言的成员并不特别容易。比如,a a b b a b b a 是一个plastar,其中偶数回文分解开来,构成的新串是否依旧是这种语言的成员却可能不能被立即辨别,读者可能需要几分钟的时间来决定
b a a b b a b b a a b a b b a a b b a b b a b a a b b a b b a b b a b b a a b a b a b b a b b a a b
是否属于P.然而我们将证明:(利用它们的代数性质)plastars能够在O(n)单位时间内被识别。
  如果一个不空的plastar不能被记为两个非空palstars的产物,我们称它是基本的plastar.一个基本的palstar 必须是一个偶数回文ααR,但是反过来却不成立。通过重复的分解,很容易看见:每一个plastarβ可视为基本palstarβ1...βt的一个产物(for some t ≥ 0 )。更不明显的是,这种分解基本因子是唯一的。“palstars 的基本定理”是下列性质的直接结论:
  引理1:一个基本的palstar 不能以另一个基本palstar开头。
  (注:证略)
  推论(左消去性)如果αβα是plastar,那么β也是plastar。
  (注:证略)
  引理2:如果α是长度为n的字符串,我们能够在O(n)步内,确定最长的偶数回文βP(α=βγ)
  (注:证略)
  定理:任意语言L(使得L具有左消去性),使得给定长度为n的字符串,我们在O(n)步能够找到一个非空的βL,(αβ开头或者不存在这样的β)。那么我们就能够决定(在O(n)时间内)是否给定的字符串在L内。
  (注:证略)
  推论:P能够在O(n)时间内被识别。
  注意对于下面的这个相关的语言
   P1 = {π|π=πRand|π|2}
不能运用上述推论来解决相关问题(这是由于它既包含a a a b b b 又包含a a a b b b b a ),plastar的这个基本定理不成立. 尽管我们怀疑“P1能够在O(n)”,但是这任是一个公开的问题。一旦读者解决了这个问题,他将被敦促去解决另一个语言是否也能在$O(n)时间内被识别(S.A.Greibach最近在【1】中提出,众所周知,后面的这个语言是非常困难的,)。没有上下文无关语言更难被识别,除非要求是一个常数因子。          
*************************这里略去一部分***************************                   
                     

附上原文:
链接:https://pan.baidu.com/s/1pISnFQ97-YsKSJDQoG96Fg 密码:3nm6

 提笔…思索了许久,却还是白纸一张。本以为自己可以洋洋洒洒写完的”前言“,却花费了许久。待到自己真正拿起笔时,才发现自己连”才疏学浅“也算不上。自己这才知道自己腹中无多少墨水。。。说来惭愧,原本早该完成的东西,一拖再拖,耽误了很久.终于再也寻不到借口了,终究没办法了,着手开始吧!又,以为自己应当得心应手的,代替的却是发现自己的捉襟见肘。就这样断断续续,最后也算是完成了。但却留下些遗憾,原打算的全文,不断的删减,终于只剩下一些零零碎碎的东西了。(最后:鄙人能力有限,如翻译有误,敬请指出)

猜你喜欢

转载自blog.csdn.net/qq_28788687/article/details/79926869