@bzoj - 3238@ [Ahoi2013]差异
@description@
给定一个长度为 n 的字符串 S,令 Ti 表示它从第 i 个字符开始的后缀。求:
\[\sum_{1\le i < j \le n}((len(Ti) -lcp(Ti, Tj)+(len(Tj)-lcp(Ti, Tj))
\]
其中 lcp 是最长公共前缀。
input
一个长度为 n 的字符串S。2 <= n <= 500000,S由小写英文字母组成。
output
一个整数,表示所求值。
sample input
cacao
sample output
54
@solution@
那个式子我们可以分两部分求解:len 和 lcp。
len 部分:每一个后缀的 len 都会统计 n-1 次。所以它对答案的贡献为 \((1+2+...+n)*(n-1)\)。把等差数列的求和公式代进去:\(\frac{(n-1)*n*(n+1)}{2}\)
lcp 部分:我们把原串翻转,则原串中的后缀对应新串中的前缀,我们要求解原串中的最长公共前缀就是新串中的最长公共后缀。
一个结点的 fa 所表示的结点一定是这个结点的后缀。所以我们最长公共后缀所表示的结点一定是该结点的某个祖先。
那么两个结点的 lca 就能表示它们的最长公共后缀。因此我们作一个简单的树形 dp 统计有多少对点以根为 lca 即可。
实际上,后缀自动机在翻转的串上建出来的 parent 树,就是原串中的后缀树。
所以后缀树完完全全没什么用啊喂。
@accepted code@
#include<vector>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 500000;
vector<int>G[2*MAXN + 5];
struct sam{
sam *ch[26], *fa; int mx;
int tag;
}pl[2*MAXN + 5], *root, *tcnt, *lst;
void init() {
root = tcnt = lst = &pl[0];
}
sam *newnode(int x) {
tcnt++; tcnt->tag = x;
return tcnt;
}
void clone(sam *x, sam *y) {
for(int i=0;i<26;i++)
x->ch[i] = y->ch[i];
x->fa = y->fa;
}
void sam_extend(int x) {
sam *cur = newnode(1), *p = lst;
cur->mx = lst->mx + 1; lst = cur;
while( p && !p->ch[x] )
p->ch[x] = cur, p = p->fa;
if( !p )
cur->fa = root;
else {
sam *q = p->ch[x];
if( p->mx + 1 == q->mx )
cur->fa = q;
else {
sam *nq = newnode(0);
nq->mx = p->mx + 1;
clone(nq, q);
q->fa = cur->fa = nq;
while( p && p->ch[x] == q )
p->ch[x] = nq, p = p->fa;
}
}
}
int siz[2*MAXN + 5];
char s[MAXN + 5]; ll ans;
void dfs(int rt) {
siz[rt] = pl[rt].tag;
for(int i=0;i<G[rt].size();i++) {
int to = G[rt][i]; dfs(to);
ans -= 2LL*siz[rt]*siz[to]*pl[rt].mx;
siz[rt] += siz[to];
}
}
int main() {
init(); scanf("%s", s);
int lens = strlen(s);
ans = 1LL*(lens-1)*lens*(lens+1)/2;
for(int i=lens-1;i>=0;i--)
sam_extend(s[i] - 'a');
for(int i=1;i<=tcnt-pl;i++) {
G[pl[i].fa-pl].push_back(i);
}
dfs(0);
printf("%lld\n", ans);
}
@details@
所以真的想问问大家,后缀树既然可以通过后缀自动机构造出来,时间复杂度空间复杂度也不会更优秀(因为你不可能超过线性复杂度嘛)。
那么后缀树到底用处在哪里?