学习笔记(11)SAM
一点(口胡)概念
· \(endpos\):一个后缀在原串中出现位置的集合,介于 \(SAM\) 的性质,两个状态的 \(endpos\) 要么不交要么有包含关系
· 节点:表示一个状态,即 \(endpos\) 相同的等价类所表示的所有子串
· 边:也称作转移,上面有字符。类似于 \(Trie\) 树,连接添加一个字符后的前后两种状态
· \(link\)/\(fail\) 边:连接一个状态的后缀(实际上是父亲),作用基本等价于失配边
· \(parent\) 树:等同反串的后缀树,注意 \(SAM\) 的转移并不在其上进行
\(SAM\) 仅存在一个初始状态,而存在多个终止状态。\(SAN\) 是一种 \(DFA\),其状态数、转移数分别可证不超过 \(2n - 1\) 与 \(3n - 4\),同时复杂度保证为线性
建树过程(增量法构造)
顺序依次添加每个字符,讨论新加入的状态与已有状态的联系——
记录 \(c\) 为加入的第 \(i\) 个字符,\(last\) 为旧串(已经插入的 \(s[1,i-1]\))对应的节点编号,\(len\) 为每一个等价类中最长的后缀的长度
首先新建一个节点 \(cur\) 表示新产生的子串 \(s[1,i]\),其 \(endpos\) 应该只包含 \(i\),并且有 \(len[cur] = len[last] + 1\)
然后考虑遍历原串的后缀找到所有可以通过加上字符 \(c\) 得到 \(cur\) 的子串,修改他们的 \(endpos\)
如果直到根也没有符合的后缀,说明所有新串后缀都已并入 \(cur\),则 \(fail[cur] = 1\)
否则对 \(v=son[u][c]\) 进行分讨,保证 \(fail\) 指向后缀:
· 1.若 \(len[v] == len[u] + 1\),则 \(v\) 中子串均为新串的后缀,直接连上 \(v\)
· 2.否则需要将 \(v\) 进行分割,建立新点 \(nu\) 存储满足 \(len[v] \leq len[u] + 1\) 的部分的子串,原 \(v\) 点中只保留剩下的部分。\(nu\) 的 \(fail\) 与 \(son\) 与 \(v\) 相同,继续跳 \(fail\) 更新 \(son[u][c] == v\) 的点为 \(nu\)
最后将 \(last\) 更新为 \(cur\),\(fail[cur] = fail[v] = nu\)
建树部分完整代码如下:
struct SAM{
int tot = 1, last = 1;
int son[N][S], fail[N], len[N], siz[N];//可以用siz维护endpos的大小,拓扑时向fail加上siz即可
void insert(int c){
int cur = ++tot, u = last;
len[cur] = len[last] + 1;//新建节点
while(u && !son[u][c]){//寻找后缀
son[u][c] = cur;
u = fail[u];
}
if(!u) fail[cur] = 1;
else{
int v = son[u][c];
if(len[v] == len[u] + 1) fail[cur] = v;
else{//分割子串为两部分
int nu = ++tot;
len[nu] = len[u] + 1, fail[nu] = fail[v];
for(int i = 0; i < S; i++) son[nu][i] = son[v][i];//继承原点
while(u && son[u][c] == v){
son[u][c] = nu;
u = fail[u];
}
fail[cur] = fail[v] = nu;
}
}
last = cur;//更新last
}
void solve(){
\\...
}
}DFA;
性质
· \(SAM\) 上维护了所有子串,其中节点 \(u\) 包含长度为 \([len[fail[u]] + 1,len[u]]\) 的子串
· 通过跳 \(fail\) 访问 \(parent\) 树上的父亲可以得到该串的所有后缀(若节点 \(A\) 为 \(B\) 的祖先,则节点 \(A\) 对应子串一定为节点 \(B\) 对应子串的后缀)
· 每个节点的 \(endpos\) 等于其子树内所有终点节点的 \(endpos\) 的并集
……(还有很多等做题时接触到再补充)
应用
\((Updating \dots)\)