SPOJ LCS 最长公共子串 后缀自动机&后缀树(Ukkonen)

  终于搞清楚了这两个恶心的算法。其实后缀树也不难写嘛。

题目

  给定两个字符串a和b,求在a和b中都有出现的连续子串的最长长度。

样例输入

alsdfkjfjkdsal
fdjskalajfkdsla

样例输出

3

做法1

  使用后缀自动机。clj的课件讲得很详细了,这里不细说。主要说几件事:

  1. 后缀自动机的状态的本质是right集合(见课件),parent意味着right集合的最小扩充。时刻记着这一点可以使很多性质的证明变得很直观。
  2. 有一个没什么用但挺有意思的性质是,字符串s的后缀自动机的每个状态和a的反串的后缀树的状态是一一对应的。原因其实很简单,后缀自动机的每个状态代表一系列子串,这些子串在s中出现的结束位置(right集合)是一样的。后缀树同样如此,每个节点连同它指向父亲的边一起代表一系列子串,而边上的串能够被压缩成一条边正是因为中间不会分叉,即出现某个前缀的地方也一定会出现另一个前缀,即它们在s中出现的起始位置(可以叫left集合)是一样的。后缀自动机和后缀树的节点的本质都可以认为是“同生共死的子串”。
  3. 有关后缀自动机的线性性质。首先点和非空边的个数都是O(n)的,证明详见clj课件。其次构建自动机的效率也是O(n)的:考虑追加一个字符c需要的两个循环,第一个循环使得一些没有c儿子的点有了c儿子,第二个循环使得一些有c儿子的点更新了c儿子。首先第一个循环的总次数显然不超过边数。第二个循环的次数其实也不超过边数,因为每个点的每个儿子最多被更新一次,这是因为每次被更新的点都是之前所有有c儿子的点之中right集合最小的点,而只有当这个right集合可以更小时才会进行这种更新。有点绕,但如果能脑补出一个过程动画的话,其实挺直观的。

      现在看这道题。如果能对每个j都能求出最大的len使得b[j-len+1..j]在a中出现,那么所有的len的最大值就是答案。
      首先建立字符串a的后缀自动机,之后向其中依次输入b的每个字符。若能够输入,则++len;否则意味着当前状态代表的一系列子串后面都没有所需字符,此时需要不断尝试缩短len来扩充子串集合,直到新加入的子串后面有了所需字符为止。注意自动机的每个状态代表的是a的子串,这个串可能很长,长到超出已匹配的长度,超出的这部分和b没有关系,只有重叠的那部分(较小的那个长度)才可以用来更新答案。

// vim: set noet ts=4 sw=4 fileencoding=utf-8:
#include 
#include 
#include 
using std::max;
using std::min;
using std::copy;
using std::fill;

const int MAX_N = 250009;

struct Node {
    static Node buf[], *bufp;
    void *operator new(size_t) {
  return bufp++;}

    int lim;
    Node *ch[26], *f;
    Node() = 

你可能感兴趣的:(题解,数据结构,后缀树,后缀自动机,最长公共子串)