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最短的状态的标号,之前写成了最长的。
1/* ***********************************************
2Author :111qqz
3Created Time :2017年11月08日 星期三 18时50分18秒
4File Name :nsubstr.cpp
5************************************************ */
6
7#include <bits/stdc++.h>
8#define PB push_back
9#define fst first
10#define sec second
11#define lson l,m,rt<<1
12#define rson m+1,r,rt<<1|1
13#define ms(a,x) memset(a,x,sizeof(a))
14typedef long long LL;
15#define pi pair < int ,int >
16#define MP make_pair
17
18using namespace std;
19const double eps = 1E-8;
20const int dx4[4]={1,0,0,-1};
21const int dy4[4]={0,-1,1,0};
22const int inf = 0x3f3f3f3f;
23#define MAXALP 26
24
25struct state
26{
27 int len, link, nxt[MAXALP];
28};
29const int N =3E5+7;
30state st[N*2];
31int Right[2*N]; //right[i]表示第i个状态的right集合大小
32int sz, last,rt;
33char s[N];
34int cnt[2*N],a[2*N];//for radix sort
35void sa_init()
36{
37 sz = 0;
38 last = rt = ++sz;
39 st[1].len = 0;
40 st[1].link=-1;
41 ms(st[1].nxt,-1);
42 ms(cnt,0);
43}
44void sa_extend(int c)
45{
46 int cur = ++sz;
47 st[cur].len = st[last].len + 1;
48 memset(st[cur].nxt, -1, sizeof(st[cur].nxt));
49 int p;
50 for (p = last; p != -1 && st[p].nxt[c] == -1; p = st[p].link)
51 st[p].nxt[c] = cur;
52 if (p == -1) {
53 st[cur].link = rt;
54 } else {
55 int q = st[p].nxt[c];
56 if (st[p].len + 1 == st[q].len) {
57 st[cur].link = q;
58 } else {
59 int clone = ++sz ;
60 st[clone].len = st[p].len + 1;
61 st[clone].link = st[q].link;
62 memcpy(st[clone].nxt, st[q].nxt, sizeof(st[q].nxt));
63 for (; p != -1 && st[p].nxt[c] == q; p = st[p].link)
64 st[p].nxt[c] = clone;
65 st[q].link = st[cur].link = clone;
66 }
67 }
68 last = cur;
69}
70int f[N]; //答案
71int main()
72{
73 #ifndef ONLINE_JUDGE
74// freopen("./in.txt","r",stdin);
75 #endif
76 sa_init();
77 ms(f,0);
78 ms(Right,0);
79 scanf("%s",s+1);
80 int len = strlen(s+1);
81 for ( int i = 1 ; i <= len ; i++)
82 {
83 Right[sz+1] = 1; //初始化right集合的大小
84 sa_extend(s[i]-'a');
85 }
86 // a simple radix sort
87 for (int i = 1 ; i <= sz ; i++) cnt[st[i].len]++;
88 for ( int i = 1 ; i <= len ; i++) cnt[i]+=cnt[i-1];
89 for (int i = 1 ; i <= sz ;i++) a[cnt[st[i].len]--] = i;
90 //将len按照从大到小的顺序存入临时数组a,a[1]表示len最*短*的状态的标号。
91
92 for ( int i = sz ; i >= 1 ; i--) Right[st[a[i]].link]+=Right[a[i]];
93 //按照len从大到小的顺序更新right集合。
94 //考虑一棵parent树,某个状态的right集合就是其就是其所有儿子节点的并集。
95 //我们对parent树自底向上更新,由于儿子的len一定比父亲的len长,所以自底向上更新也就是按照len从大到小更新
96 //
97 //
98 for ( int i = rt ; i <= sz ; i++) f[st[i].len] = max(f[st[i].len],Right[i]) ;
99 //长度为st[i].len的字符串出现了right[i]次
100
101 for ( int i = len ; i >= 1 ; i--) f[i] = max(f[i],f[i+1]);
102 //由于我们之前对于一个状态,只是更新了其表示的子串中长度最长的那个(也就是st[i].len)
103 //但实际上每个状态表示的子串的长度是在区间[st[st[i].link]+1,st[i].len]
104 //这个状态表示的每一个长度在该区间中的子串实际上都出现了right[i]次
105 //我们只更新了最大,所以最后要倒序更新一下。
106 //这样做的正确性在于,长度i-1的子串出现的次数一定大于等于长度为i的子串出现的次数。
107 for ( int i = 1 ; i <= len ; i++) printf("%d\n",f[i]);
108
109
110 #ifndef ONLINE_JUDGE
111 fclose(stdin);
112 #endif
113 return 0;
114}