神奇的后缀自动机!!!
它可以解决以下问题:
问题1:判断子串
直接在后缀自动机上跑边,跑完串还未跑到NULL则为原串子串。
后缀数组:跑出sasasa,然后从最小的后缀开始,一个个往后枚举,记录下当前匹配到的位置,如果匹配不上就下一个后缀,否则位置向后移一位。如果枚举完了后缀还没有完全匹配则不是原串子串。
问题2:不同子串个数
DAG上DP。对于一个节点i,f[i]表示从iii出发的子串个数(不含空串)。那么,f[i]就等于
∑
(
i
,
j
)
∈
E
d
g
e
(
f
[
j
]
+
1
)
\sum_{(i,j)\in Edge}(f[j]+1)
∑(i,j)∈Edge(f[j]+1) 。f[1]即是答案。
后缀数组:每一个后缀的长度减去其height之和。
问题3:在原串所有子串中(相同的不算一个)字典序第i大的是哪个(洛谷P3975)
首先处理出每个节点的endpos大小,即每个类中的串在原串中出现的次数。考虑dp,f[i]代表i的大小。对于不包含任意一个个前缀的节点,f[i]= ∑ ( i , j ) ∈ E d g e f [ j ] \sum _{(i,j)\in Edge}f[j] ∑(i,j)∈Edgef[j]然后DP出g[i],表示从iii出发的子串个数(不含空集且计算重复),则g[i]= ∑ ( i , j ) ∈ E d g e ( g [ j ] + f [ j ] ) \sum_{(i,j)\in Edge}(g[j]+f[j]) ∑(i,j)∈Edge(g[j]+f[j])。
最后,在后缀自动机上dfs,按字典序遍历出边,排名还够的减去这边的所有子串,不够的跑到这条出边连向的节点,重复以上步骤。当排名变为非正数时输出跑过的路径形成的字符串。具体做法还请参考本题题解。
问题4:判断两个串的最长公共子串
把两个串拼起来,中间加特殊字符,跑后缀自动机。然后用类似于上面处理出现次数的方法,跑出一个子串在拼起来的串前半部分出现的次数和后半部分出现的次数。然后遍历节点,找len最大的前后出现次数都不为0的节点。以上思路还可以处理多个字符串的最长公共子串。
后缀数组:同样是拼起来,然后处理sa和height,对于每个后缀,找到其之后第一个属于另一半部分的后缀(可以O(n)做到,具体做法请读者思考),求它们的lcp,最后取最大值。
以及:
以上很多功能还有待我去实现… (太菜了)
下面上代码:
#include <iostream>
#include <queue>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <stack>
#include <map>
#define MAX 10010
#define ll long long
#define mod 1000000007
using namespace std;
struct node {
int tree[MAX][26];
int fa[MAX];
int len[MAX];
int num[MAX];
int size[MAX];
int tot;
int last;
node()
{
memset(tree, 0, sizeof(tree));
tot = 1;
last = 1;
fa[1] = 0;
len[1] = 0;
}
void add(int c)
{
int p = last;
int now = last = ++tot;
size[now] = 1;
len[now] = len[p] + 1;
for (; p && tree[p][c] == 0; p = fa[p])
tree[p][c] = now;
if (!p)
fa[now] = 1;
else
{
int l = tree[p][c];
if (len[l] == len[p] + 1)
fa[now] = l;
else
{
int neww = ++tot;
len[neww] = len[p] + 1;
fa[neww] = fa[l];
for (int i = 0; i < 26; i++)
tree[neww][i] = tree[l][i];
fa[now] = fa[l] = neww;
for (; p && tree[p][c] == l; p = fa[p])
tree[p][c] = neww;
}
}
}
int getnum(int now = 1)
{
if (num[now] != 0)
return num[now];
num[now] = 1;
for (int i = 0; i < 26; i++)
{
if (tree[now][i])
{
int ans = getnum(tree[now][i]);
num[now] += ans;
}
}
return num[now];
}
void getsize()
{
int A[MAX] = { 0 };
int t[MAX] = { 0 };
for (int i = 1; i <= tot; i++) t[len[i]]++;
for (int i = 1; i <= tot; i++) t[i] += t[i - 1];
for (int i = 1; i <= tot; i++) A[t[len[i]]--] = i;//神奇的桶排
for (int i = tot; i >= 1; i--) size[fa[A[i]]] += size[A[i]];
}
void getkth(int k, string& ans,int now=1)
{
if (now == 1)
ans = "";
if (k <= 1)
return;
k--;
for (int i = 0; i < 26; i++)
{
int to = tree[now][i];
if (to != 0)
{
if (k > num[to])
k -= num[to];
else
{
ans += (i + 'a');
getkth(k,ans,to);
return;
}
}
}
ans = "-1";
}
}SAM;
int main()
{
string a;
cin >> a;
for (int k = 0; k < a.size(); k++)
SAM.add(a[k] - 'a');
SAM.getnum();
SAM.getsize();
string ans="yes";
int len = 1;
while (ans != "-1")
{
SAM.getkth(len++, ans);
cout << len - 1 << ":" << ans << endl;
}
return 0;
}
目前只实现了两个比较基础的功能 getnum() 和 getsize()
其中 getnum() 的功能是求出文本串中一共有几个不同的子串(含空串!)
getsize() 的功能是求出每个节点所代表的子串在文本串中一共出现了几次,也就是|endpos|.
这个实现很重要,值得学习
通过推(guan)理(cha)就可以不(xuan)难(xue)发现
s
i
z
e
[
i
]
=
∑
(
i
,
j
)
∈
E
d
g
e
s
i
z
e
[
j
]
size[i]=\sum _{(i,j)\in Edge}size[j]
size[i]=∑(i,j)∈Edgesize[j]
对于除了第三种情况中neww出来的替代品来说每一个节点的size初始值为1
但是我们只有从下向上的fa关系所以我们需要一个拓扑排序,但是正常的拓扑排许肯定是不合适的,因为SAM的复杂度为O(n*logn) 傻愣愣的拓扑会让复杂度退化…这样不好…
再次通过推(guan)理(cha)就可以不(xuan)难(xue)发现:可以通过len的值的大小对SAM进行划分层次.
然后以len为关键字进行排序就可以得到SAM的一个拓扑排序
然后就有了桶排求完前缀和的t数组可以这样想
t[k]表示长度小于等于k的节点个数为t[k]个
那么长度为k的节点的排名不就为t[k]了吗? (这里绕了我好久…)
至于第三行的t[len]–是为了让相同的值的排名不一样 (仅此而已)
通过getnum()和getszie()的配合,我们就可以解决文本串中第k小子串的问题啦!
至于其它的功能,我再学习学习叭
去重版的第k小已经实现啦… (并不需要size数组的辅助)明天搞不去重的…
注意一个点…一定要搞清是在parent树上跑还是在SAM上跑…
其中fa数组存的是自下向上连接的parent树,tree数组存的是自上而下的SAM…
len数组存的是每一个状态的最长字符串的长度也就是longest(s)
不去重的也写完了(和不去重的几乎没有什么不一样的地方就不贴了…)
然后是两个串找最长公共序列
(随时更新)