spoj nsubstr Substrings (后缀自动机 模板题)

http://www.spoj.com/problems/NSUBSTR/en/

题意:

f[i]指长度为i的串出现次数的最大值。这里的不同出现指,可以有重复串,只要起始位置不同就视为不同的出现。

求f[1]..f[lenth]。

思路:

我们知道,SAM上的节点是right集合的等价类。

一个子串的right集合是指在一个母串中,某个子串所有出现位置的右端点的集合。

如果两个子串的right集合完全相同,那么就可以归结为同一个节点,也就是说这两个子串对于SAM是等价的。

因为在SAM上,这两个子串后,添加若干字符得到的后缀都是一样的。

所以一个SAM节点的个数,就是right集合等价类的个数(如果不考虑初始状态)

对于SAM某一个节点,其能表示的子串有一个范围,设为[MIN,MAX]

SAM的一个节点能表示的子串的含义是说,这些子串的right集合相同。

而一个子串出现的次数就是其right集合的大小。

考虑SAM上的某个节点,其表示的长度区间在[MIN,MAX]的子串都出现了|right|次

如果我们直接从MIN更新到MAX有点太暴力了,实际上这里我们可以只更新f[MAX]

由于长度i-1的子串出现的次数一定大于等于长度为i的子串出现的次数,所以最后长度从大到小更新f即可。

现在的问题就变成了如何求right集合。

实际上我们不需要知道right集合的具体组成,只需要知道right集合的大小。

考虑一棵parent树,树上某个节点的right集合就是该节点的所有儿子节点的right集合的并集

因此我们只需要在parent上自低向上更新right集合的大小就可以了。

如何保证在parent树上是自底向上更新呢?

我们只需要按照len,也就是SAM中所有节点能表示的子串的MAX值从大到小的顺序更新right集合。

原因是parent树上,儿子的len一定比父亲的len长。

注意到,对于parent树上的叶子节点,其right集合是1,这也是我们的初始状态。

部分实现细节见代码注释。

关于SAM的详细学习笔记日后补

20171109Update:

注释写错了一处,a[1]应该是len最短的状态的标号,之前写成了最长的。

/* ***********************************************
Author :111qqz
Created Time :2017年11月08日 星期三 18时50分18秒
File Name :nsubstr.cpp
************************************************ */

#include <bits/stdc++.h>
#define PB push_back
#define fst first
#define sec second
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define ms(a,x) memset(a,x,sizeof(a))
typedef long long LL;
#define pi pair < int ,int >
#define MP make_pair

using namespace std;
const double eps = 1E-8;
const int dx4[4]={1,0,0,-1};
const int dy4[4]={0,-1,1,0};
const int inf = 0x3f3f3f3f;
#define MAXALP 26

struct state
{
    int len, link, nxt[MAXALP];
};
const int N =3E5+7;
state st[N*2];
int Right[2*N]; //right[i]表示第i个状态的right集合大小
int sz, last,rt;
char s[N];
int cnt[2*N],a[2*N];//for radix sort
void sa_init()
{
    sz = 0;
    last = rt = ++sz;
    st[1].len = 0;
    st[1].link=-1;
    ms(st[1].nxt,-1);
    ms(cnt,0);
}
void sa_extend(int c)
{
    int cur = ++sz;
    st[cur].len = st[last].len + 1;
    memset(st[cur].nxt, -1, sizeof(st[cur].nxt));
    int p;
    for (p = last; p != -1 && st[p].nxt[c] == -1; p = st[p].link)
        st[p].nxt[c] = cur;
    if (p == -1) {
        st[cur].link = rt;
    } else {
        int q = st[p].nxt[c];
        if (st[p].len + 1 == st[q].len) {
            st[cur].link = q;
        } else {
            int clone = ++sz ;
            st[clone].len = st[p].len + 1;
            st[clone].link = st[q].link;
            memcpy(st[clone].nxt, st[q].nxt, sizeof(st[q].nxt));
            for (; p != -1 && st[p].nxt[c] == q; p = st[p].link)
                st[p].nxt[c] = clone;
            st[q].link = st[cur].link = clone;
        }
    }
    last = cur;
}
int f[N]; //答案
int main()
{
    #ifndef  ONLINE_JUDGE 
//  freopen("./in.txt","r",stdin);
  #endif
    sa_init();
    ms(f,0);
    ms(Right,0);
    scanf("%s",s+1);
    int len = strlen(s+1);
    for ( int i = 1 ; i <= len ; i++)
    {
        Right[sz+1] = 1; //初始化right集合的大小
        sa_extend(s[i]-'a');
    }
    //  a simple radix sort
    for (int i = 1 ; i <= sz ; i++) cnt[st[i].len]++;
    for ( int i = 1 ; i <= len ; i++) cnt[i]+=cnt[i-1];
    for (int i = 1 ; i <= sz  ;i++) a[cnt[st[i].len]--] = i;
    //将len按照从大到小的顺序存入临时数组a,a[1]表示len最*短*的状态的标号。

    for ( int i = sz ; i >= 1 ; i--) Right[st[a[i]].link]+=Right[a[i]]; 
    //按照len从大到小的顺序更新right集合。
    //考虑一棵parent树,某个状态的right集合就是其就是其所有儿子节点的并集。
    //我们对parent树自底向上更新,由于儿子的len一定比父亲的len长,所以自底向上更新也就是按照len从大到小更新
    //
    //
    for ( int i = rt ; i <= sz ; i++) f[st[i].len] = max(f[st[i].len],Right[i]) ; 
    //长度为st[i].len的字符串出现了right[i]次
    
    for ( int i = len ; i >= 1 ; i--) f[i] = max(f[i],f[i+1]);
    //由于我们之前对于一个状态,只是更新了其表示的子串中长度最长的那个(也就是st[i].len)
    //但实际上每个状态表示的子串的长度是在区间[st[st[i].link]+1,st[i].len]
    //这个状态表示的每一个长度在该区间中的子串实际上都出现了right[i]次
    //我们只更新了最大,所以最后要倒序更新一下。
    //这样做的正确性在于,长度i-1的子串出现的次数一定大于等于长度为i的子串出现的次数。
    for ( int i = 1 ; i <= len ; i++) printf("%d\n",f[i]);


  #ifndef ONLINE_JUDGE  
  fclose(stdin);
  #endif
    return 0;
}