Description
兔子们在玩字符串的游戏。首先,它们拿出了一个字符串集合S,然后它们定义一个字符串为“好”的,当且仅当它可以被分成非空的两段,其中每一段都是字符串集合S中某个字符串的前缀。
比如对于字符串集合{"abc","bca"},字符串"abb","abab"是“好”的"abb"="ab"+"b",abab="ab"+"ab"),而字符串“bc”不是“好”的。兔子们想知道,一共有多少不同的“好”的字符串。
Input
第一行一个整数n,表示字符串集合中字符串的个数
接下来每行一个字符串
Output
一个整数,表示有多少不同的“好”的字符串
Sample Input
2
ab
ac
ab
ac
Sample Output
9
HINT
1<=n<=10000,每个字符串非空且长度不超过30,均为小写字母组成。
题解:
题解根本看不懂……昨天满机房问大佬问了好几个小时,大佬的办法跟题解上的不一样啊。其实弄懂以后也挺简单的。
总共的组成方案有前缀的平方种,然后我们减去重复的划分方案。对于一个答案串,我们规定最右边的划分方案是合法的。考虑图中绿色的那个串,红色串是它右边第一个符合条件的划分方式。显然红色串是它的一个后缀。又红、绿串都是原字符串集中的前缀,红串又是满足上述条件中最长的一个,这就是“最长前缀匹配后缀”,恰好对应了AC自动机上fail指针的指向。此时红串会对绿串产生答案减一的贡献,每当有一个以图中蓝色串结尾的前缀。于是问题就转化为:对于每一个串和它fail指针指向的串,求有多少个以两串相差部分为后缀的前缀。AC自动机上,根到每个节点的路径都对应一个前缀。而以一个串为后缀的串的个数,就是它fail树上子树大小减一(本身不算)。于是暴枚每个串即可。注意一个串的fail指针如果指向根,则不存在一个串是它的后缀,那它一定是所在答案串中最靠右的划分方式,就不应减去了。
附上昨天下午刚学的AC自动机
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=1e4+10;
int n,m;
char s[50];
namespace AC_Automaton {
int tot=0,root=0;
struct Node {
int fa,siz,fail,ch[26];
}tree[N*30];
queue<int> Q;
void Insert(char *s) {
int len=strlen(s+1),now=root;
for (int i=1;i<=len;i++) {
if (!tree[now].ch[s[i]-'a']) {
tree[now].ch[s[i]-'a']=++tot;
tree[tot].fa=now;
}
now=tree[now].ch[s[i]-'a'];
}
}
void GetFail() {
for (int i=0;i<26;i++)
if (tree[root].ch[i]) {
tree[tree[root].ch[i]].fail=root;
Q.push(tree[root].ch[i]);
}
while (!Q.empty()) {
int u=Q.front(); Q.pop();
for (int i=0;i<26;i++) {
if (tree[u].ch[i]) {
tree[tree[u].ch[i]].fail=tree[tree[u].fail].ch[i];
Q.push(tree[u].ch[i]);
}
else tree[u].ch[i]=tree[tree[u].fail].ch[i];
}
}
}
void Solve() {
for (int i=1;i<=tot;i++) {
for (int j=tree[i].fail;j;j=tree[j].fail) tree[j].siz++;
}
LL ans=1ll*tot*tot;
for (int i=1;i<=tot;i++) {
int j=i,k=tree[i].fail;
if (!k) continue;
while (k) j=tree[j].fa,k=tree[k].fa;
ans-=tree[j].siz;
}
printf("%lld",ans);
}
}
using namespace AC_Automaton;
int main() {
scanf("%d",&n);
for (int i=1;i<=n;i++) {
scanf("%s",s+1);
Insert(s);
}
tree[root].fail=root;
GetFail(); Solve();
return 0;
}