题目链接:https://ac.nowcoder.com/acm/problem/19894
后缀自动机推荐博客:https://www.luogu.org/blog/Kesdiael3/hou-zhui-zi-dong-ji-yang-xie
这里我们必须要先求出给出字符串中所有后缀两两之间的最长公共前缀长度之和,想用后缀自动机做,在后缀自动机上的每一个点都代表了原串中的多个唯一子串,但是这里面最长的那个子串一定是原串的一个前缀,并且这个点所代表的其他子串一定是这个前缀的后缀。所以我们可以暂且理解为SAM上的每一个点都代表了原串的一个前缀,这个点代表的其他子串这里先不管。
然后SAM上每一个点都会有一个fa指针,fa指针指向的那个节点所代表的最长子串也是原串的一个前缀,并且这个前缀同时还是当前点所代表的前缀的后缀。我们以自动机上的每一个叶子节点为起点向它的fa方向遍历,在遍历的过程中我们就可以算出所有前缀两两之间的最长公共后缀之和了。然后我们发现我们求出的东西刚好和题目需要我们求出的东西相反,于是我们把题目给出的字符串翻转一下,用这个翻转之后的字符串来构到自动机,然后题目就变成了求翻转字符串的所有前缀两两之间的最长后缀之和,然后用总数减去这个和就行了。
代码:
#include<iostream> #include<cstring> #include<algorithm> #include<queue> #include<map> #include<stack> #include<cmath> #include<vector> #include<set> #include<cstdio> #include<string> #include<deque> using namespace std; typedef long long ll; #define eps 1e-8 #define INF 0x3f3f3f3f #define maxn 2000005 struct node{ int len,fa; int ch[26]; }trie[maxn]; int last,tot; ll len; ll sum,ans; char str[maxn]; ll size[maxn]; int c[maxn],x[maxn]; void init(){ last=tot=1; memset(size,0,sizeof(size)); sum=ans=0; } void insert(int c){ int p=last; int np=last=++tot; size[tot]=1; trie[np].len=trie[p].len+1; for(;p&&trie[p].ch[c]==0;p=trie[p].fa) trie[p].ch[c]=np; if(p==0) trie[np].fa=1; else{ int q=trie[p].ch[c]; if(trie[p].len+1==trie[q].len) trie[np].fa=q; else{ int nq=++tot; trie[nq]=trie[q]; trie[nq].len=trie[p].len+1; trie[q].fa=trie[np].fa=nq; for(;p&&trie[p].ch[c]==q;p=trie[p].fa) trie[p].ch[c]=nq; } } } void cal(){ //基数排序,按照len值从小到大排序 for(int i=0;i<=tot;i++) c[i]=0; for(int i=2;i<=tot;i++) c[trie[i].len]++; for(int i=2;i<=tot;i++) c[i]+=c[i-1]; for(int i=tot;i>=2;i--) x[c[trie[i].len]--]=i; //从叶子节点开始遍历fail树 for(int i=tot;i>=2;i--){ int c=x[i]; //(当前节点代表的字符串出现的次数)*(它fail指向的字符串(是前一个字符串的后缀)的长度)*乘以2 //这里之所以要再乘以size[trie[i].fa]是因为fail指向的那个节点的size可能为0,它可能是我们在构造sam时创建用来将一个点划分为两个点的节点 sum+=2*size[c]*(trie[trie[c].fa].len)*size[trie[c].fa]; //因为trie[c].fa代表的前缀是trie[c]代表前缀的后缀,所以我们要把trie[c]出现的次数叠加到trie[c].fa上面 size[trie[c].fa]+=size[c]; } } int main() { scanf("%s",str); init(); //我们需要求的是str所有后缀两两之间的最长公共前缀长度之和,所以我们将str翻转 //这样问题就转化成为了求翻转之后字符串的所有前缀两两之间的最长公共后缀之和了 len=strlen(str); for(int i=len-1;i>=0;i--){ insert(str[i]-'a'); } cal(); ans=len*(len+1)/2*(len-1); printf("%lld ",ans-sum); return 0; }