BZOJ 3473 字符串
题目大意:
给你\(n\)个字符串, 询问每个串中有多少个子串, 满足它至少是\(k\)个字符串的子串.
数据范围: 所有字符串总长度\(\ge 100000\)
Solution
我们对所有字符串建立广义后缀自动机. DFS一次, 启发式合并每个点被哪些串包含, 这样我们就可以得到一个点是否为大于等于\(k\)个字符串的子串.
考虑如何统计答案: 我们把每个字符串放入后缀自动机中跑一次, 假如当前节点不满足是至少\(k\)个字符串的子串, 则往父亲节点跳, 直到满足要求或跳到根节点; 然后在这个串的答案上加上当前节点表示的最大长度即可, 表示有这么多个以当前位置结尾的串满足要求.
#include <cstdio>
#include <cstring>
#include <vector>
#include <set>
#include <algorithm>
#define vector std::vector
#define set std::set
#define swap std::swap
const int LEN = (int)1e5;
int k;
int ans[LEN + 1];
struct suffixAutomaton
{
struct node
{
node *suc[26], *pre; vector<node*> successorOnSuffixTree;
int len, vst, tg; vector<int> bck;
inline node() {for(int i = 0; i < 26; ++ i) suc[i] = NULL; bck.clear(); successorOnSuffixTree.clear(); vst = tg = 0 ;}
}*rt;
inline suffixAutomaton() {rt = new node; rt->len = 0; rt->pre = NULL;}
inline void insert(char *str, int len, int id)
{
node *lst = rt;
for(int i = 0; i < len; ++ i)
{
int c = str[i] - 'a';
if(lst->suc[c] != NULL)
{
node *p = lst->suc[c];
if(p->len == lst->len + 1) {p->bck.push_back(id); lst = p; continue;}
node *q = new node; *q = *p; q->len = lst->len + 1; q->bck.push_back(id);
p->pre = q;
for(; lst != NULL && lst->suc[c] == p; lst = lst->pre) lst->suc[c] = q;
lst = q;
continue;
}
node *u = new node; u->len = lst->len + 1; u->bck.push_back(id);
for(; lst != NULL && lst->suc[c] == NULL; lst = lst->pre) lst->suc[c] = u;
if(lst == NULL) u->pre = rt;
else
{
node *p = lst->suc[c];
if(p->len == lst->len + 1) u->pre = p;
else
{
node *q = new node; *q = *p; q->len = lst->len + 1;
p->pre = u->pre = q;
for(; lst != NULL && lst->suc[c] == p; lst = lst->pre) lst->suc[c] = q;
}
}
lst = u;
}
}
void build(node *u)
{
if(u->pre != NULL) u->pre->successorOnSuffixTree.push_back(u), u->vst = 1;;
for(int i = 0; i < 26; ++ i) if(u->suc[i] != NULL && ! u->suc[i]->vst) build(u->suc[i]);
}
inline void buildSuffixTree() {build(rt);}
set<int> DFS(node *u)
{
set<int> st; st.clear(); for(auto id : u->bck) st.insert(id);
for(auto v : u->successorOnSuffixTree)
{
set<int> res = DFS(v);
if(st.size() < res.size()) swap(st, res);
for(auto id : res) st.insert(id);
}
if(st.size() >= k) u->tg = 1;
return st;
}
inline void work() {DFS(rt);}
inline int getAnswer(char *str, int len)
{
node *u = rt; int res = 0;
for(int i = 0; i < len; ++ i)
{
u = u->suc[str[i] - 'a'];
while(u != rt && ! u->tg) u = u->pre;
res += u->len;
}
return res;
}
}SAM;
int main()
{
#ifndef ONLINE_JUDGE
freopen("BZOJ3473.in", "r", stdin);
freopen("BZOJ3473.out", "w", stdout);
#endif
int n; scanf("%d%d", &n, &k);
static char str[LEN]; static int len[LEN]; int p = 0;
for(int i = 0; i < n; ++ i) scanf("%s", str + p), SAM.insert(str + p, len[i] = strlen(str + p), i), p += len[i];
SAM.buildSuffixTree();
memset(ans, 0, sizeof(ans));
SAM.work();
p = 0;
for(int i = 0; i < n; ++ i) printf("%d\n", SAM.getAnswer(str + p, len[i])), p += len[i];
}
这里补充一些关于广义后缀自动机的理解.
我们知道, 普通后缀自动机的每个节点表示的是结束位置相同的一些子串的集合; 而在广义后缀自动机中, 这里的结束位置应该被看作是一个二元组, 同时记录了是哪一个串的哪一个位置.
广义后缀树中每个点所表示的字符串的出现次数等于以它为根的子树中实点个数, 这与普通后缀自动机中是一样的.
广义后缀自动机的构造分为在线和离线两种; 这里只讲在线的: 我们在插入的时候要分类讨论, 假如已经有当前字母对应的子节点, 则判断这个子节点的长度是否上一个点的长度加1, 假如是则直接跳到这个子节点即可; 否则新建一个节点, 同时跳parent, 修改路径.