SAy my name when a tree susurrates
SA学习笔记
非常难理解的算法(大概)。
定义两个数组:\(sa_i\) 表示的是第 \(i\) 小的后缀的下标为 \(sa_i\),\(rk_i\) 表示的是后缀 \(i\) 的下标。
显然我们有 \(rk[sa[i]]=sa[rk[i]]=i\)。
暴力求 SA
显然的,直接排序就可以了。
复杂度 \(O(n^2 \log n)\)。
\(O(n \log ^2 n)\)
这里还是好理解的。考虑倍增,则我们单次排序相当于以 \(rk[i+w]\) 为第二关键字,\(rk[i]\) 为第一关键字进行排序,然后构建得到新的 \(rk\) 与 \(sa\)。
\(O(n \log n)\)
难理解的。旧的排序不善,考虑使用善的排序,考虑到值域为 \(O(字符集)\),因此直接基数排序,显然是对的。
优化
上述做法的常数很大,考虑优化。具体的,以第二关键字进行的排序,我们直接优化掉,因为旧的 \(sa\) 已经是按照第二关键字完成排序的,所以直接把最后 \(w\) 个点丢到前面,剩下的按顺序加入就好了。
值域是可以动态更新的,然后如果值域已经到 \(n\) 了,代表我们比较完成了,直接结束循环。
感性理解SA
考虑这个的构建过程,相当于我们在不断的更新 \(sa\) 和 \(rk\),每一次,都把 \(rk\) 和 \(sa\) 变成当前串长下的正确的值。然后尝试构建新的 \(sa\),也就是根据下标的 \(rk\),完成比较。
Code
for(int i=1;i<=n;i++)
cnt[rk[i]=s[i]]++;
for(int i=1;i<=m;i++)
cnt[i] += cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[rk[i]]--] = i;
memcpy(oldrk+1,rk+1,n*4);
int p = 0;
for(int i=1;i<=n;i++)
if(oldrk[sa[i]] == oldrk[sa[i-1]])
rk[sa[i]] = p;
else
rk[sa[i]] = ++p;
m = p;
for(int w=1;w<=n;w<<=1,m=p){
int cur = 0;
for(int i=n-w+1;i<=n;i++)
id[++cur] = i;
for(int i=1;i<=n;i++)
if(sa[i] > w) id[++cur] = sa[i] - w;
fill(cnt+1,cnt+1+m,0);
for(int i=1;i<=n;i++)
cnt[rk[id[i]]] ++ ;
for(int i=1;i<=m;i++)
cnt[i] += cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[rk[id[i]]] -- ] = id[i];
p =0;
memcpy(oldrk+1,rk+1,n*4);
for(int i=1;i<=n;i++)
if(oldrk[sa[i]] == oldrk[sa[i-1]] && oldrk[sa[i] + w] == oldrk[sa[i-1] + w])
rk[sa[i]] = p;
else
rk[sa[i]] = ++p;
}
应用
例题:P4051。复制一次串,显然是对的。
例题2:P2870。添加反向的串,别忘记加空字符,显然是对的。
Height数组
欢迎来到噩梦。
概念
定义:\(ht_i = |lcp(suf(sa_{i-1}),suf(sa_{i}))|\)。
结论1:如果 \(rk_i < rk_j < rk_k\),则有 \(\min(|lcp(i,j)|,|lcp(j,k)|) \ge |lcp(i,k)|\)。
证明:考虑 \(rk\) 的定义,显然 \(i,j\) 的字典序更进,因此如果 \(i,j\) 必然会包含 \(i,k\) 的 \(lcp\)。
结论2:令 \(u=sa_i+1\),我们有 \(ht(rk_u) \ge ht(rk_{u-1})-1\)。
证明:第一种, \(sa(rk_u-1)\) 恰好就是 \(u-1\) 所对应的 \(sa(rk_{u-1}-1)+1\),则上面的式子显然成立,否则根据结论1,取到的值一定是大于这个不是相邻的 \(sa\)。
// from Alex_Wei,因为我自己还没有写过
for(int i=1,k=0;i<=n;i++){
if(k) k -- ;
while(s[i+k] == s[sa[rk[i]-1] + k])
k ++ ;
ht[rk[i]] = k;
}
ht[1] = 0;
应用
快速求两个后缀的 LCP
假设后缀为 \(i,j\),则结果为 \(\min_{p=rk_i+1}^{rk_j} ht_p\),这个的证明是显然的。
然后这种区间最值的状物,我们显然可以直接 ST 表预处理然后 \(O(1)\) 查询。
求本质不同的子串个数
\(n \cdot (n-1) / 2 - \sum ht\)。
考虑子串的本质为后缀的前缀,而本质不同的子串,我们只需要减去前面出现过的就可以了,因此是这个式子。
单调栈+height数组
这里不作展开,就是用单调栈维护height数组的贡献区间,例题:P4248。
多个串的最长公共子串
考虑直接把所有的串连成一个串,中间插入分割符,然后作双指针就可以了。例题:P2463。
并查集+SA
例题P2178,考虑 height 数组的定义,我们可以 height 数组从大到小,然后把 \(ht_i\) 当成从 \(sa_i\) 连向 \(sa_{i-1}\) 的边就好了。
结语
\(SA\) 是非常强大的算法,如果在考场上遇到了子串或者是 LCP 相关的问题,不妨想想这个。
后面应当会有专门的一篇文章,用来记录各种 SA 的题目。
另外,本文的内容有很大程度上是参考的 Alex_Wei 的内容,非常优质。

浙公网安备 33010602011771号