26HDU07A 今晚吃什么 题解 AC自动机+虚树差分
首先一个朴素的想法肯定是定义 \(f_{i,j}\) 表示说现在是第 \(i\) 条评论,然后目前的真评论总长度是 \(j\) 的方案数。那么转移方程也是比较好写的:
\(\begin{aligned}f_{i,j}=\sum_{k}f_{k,j-1}(S_k\ 是\ S_i\ 的子串)\end{aligned}\)
我们思考一下这个东西怎么优化。
Hint 1
第二维不会太大。
一个字符串必定比其子串长至少 \(1\)。再加上输入的字符串长度单调不减,那么第二维应该是 \(\sqrt{L}\) 级别的。
子串,多模式串,考虑建立 AC 自动机,那么上面那个东西就相当于第 \(i\) 个评论所走过的点,这些点在 fail 树上到根的路径的 \(f_{v,j-1}\) 的总和。但是每个点都跑一边所有 fail 树,那么时间复杂度显然是不支持的。我们就想到预处理一下 \(g_{v,i}\) 表示从 \(v\) 节点开始,走到 fail 树根路径上的所有 \(f_{u,i}\) 的和。那么我们就想到这个东西应该以 fail 树上的拓扑序来弄,用 dfn 序也一样。但是一个东西 \(f_{u,i}\) 并不是 trie 树上从根到这个评论所代表的节点路上所有节点的 \(g\) 之和,我们可以想到,如果我这个 fail 树指向了原串的祖先,那么会有重复计数。
这是虚树差分的一个 trick。我们将 trie 路径上的所有节点按照 fail 树 dfn 排序,我们只要减掉相邻的点的 LCA 就行了。这也比较好理解。
#include <bits/stdc++.h>
#define debug puts("Wait for me.");
template <typename T>void read(T& t){t=0; char ch=getchar(); int fflag=1;while(!('0'<=ch&&ch<='9')){if(ch=='-') fflag=-1;ch=getchar();}while(('0'<=ch&&ch<='9')){t=t*10+ch-'0'; ch=getchar();} t*=fflag;}
template <typename T,typename... Args>void read(T& t, Args&... args){read(t);read(args...);}
template <typename T>void write(const T &x) {if(x < 10) {putchar(x + '0');return;}write(x / 10);putchar(x % 10 + '0');}
#define pb push_back
using namespace std;
#define rep(I,J,K) for(int I=(int)J;I<=(int)K;++I)
#define per(I,J,K) for(int I=(int)J;I>=(int)K;--I)
typedef long long ll;typedef double db;typedef pair<int,int>pii;
mt19937 mrand(random_device{}());
int rnd(int x){return mrand()%x;}
// head
const int N = 1e5 + 10, S = 2e5 + 10, SQ = 650,inf = 0x3f3f3f3f, P = 998244353;
int T;
int n;
void add(int &x, int y) {x += y; if(x >= P) x -= P;}
void del(int &x, int y) {x -= y; if(x < 0) x += P;}
struct AC_automaton {
struct Node {
int son[26], fail;
}tr[S];
int cnt;
vector<int>id[N];
int _end[S], end[N], fa[S][19], dep[S], dfn[S], tot, ans[N], f[N], pre[S], _dfn[S];
vector<int>G[S], lca[S];
void init() {
rep(i, 0, cnt) {
memset(tr[i].son, 0, sizeof tr[i].son);
tr[i].fail = 0;
}
rep(i, 1, n) end[i] = ans[i] = f[i] = 0;
rep(i, 0, cnt) _end[i] = 0, G[i].clear(), dep[i] = 0, pre[i] = 0;
rep(i, 0, cnt) rep(j, 0, 18) fa[i][j] = 0;
cnt = tot = 0;
}
void insert(string st, int idx) {
int cur = 0; id[idx].clear();
rep(i, 0, st.size() - 1) {
cur = (tr[cur].son[st[i] - 'a']) ? tr[cur].son[st[i] - 'a'] : (tr[cur].son[st[i] - 'a'] = ++cnt);
id[idx].pb(cur);
}
_end[end[idx] = cur] = idx;
return;
}
void build() {
queue<int>Q;
rep(i, 0, 25) if(tr[0].son[i]) Q.push(tr[0].son[i]);
while(!Q.empty()) {
int u = Q.front(); Q.pop();
rep(i, 0, 25) {
if(tr[u].son[i]) {
tr[tr[u].son[i]].fail = tr[tr[u].fail].son[i];
Q.push(tr[u].son[i]);
} else {
tr[u].son[i] = tr[tr[u].fail].son[i];
}
}
}
}
void dfs(int u) {
_dfn[dfn[u] = ++tot] = u;
for(int v : G[u]) {
dep[v] = dep[u] + 1;
fa[v][0] = u;
dfs(v);
}
}
void ST() {
rep(i, 1, 18)
rep(j, 1, cnt)
fa[j][i] = fa[fa[j][i - 1]][i - 1];
}
int LCA(int x, int y) {
if(dep[x] < dep[y]) swap(x, y);
rep(i, 0, 18) if((dep[x] - dep[y] >> i) & 1) x = fa[x][i];
per(i, 18, 0) if(fa[x][i] ^ fa[y][i]) x = fa[x][i], y = fa[y][i];
return (x == y) ? x : fa[x][0];
}
void Do() {
rep(i, 1, cnt) G[tr[i].fail].pb(i);
dfs(0); ST();
rep(i, 1, n) {
sort(id[i].begin(), id[i].end(), [&](int x, int y){return dfn[x] < dfn[y];});
lca[i].clear();
rep(j, 1, id[i].size() - 1) lca[i].pb(LCA(id[i][j - 1], id[i][j]));
}
ans[1] = n;
rep(i, 1, n) f[i] = 1;
rep(i, 2, min(n, SQ)) {
rep(j, 1, tot) {
// 按照 fail 树 dfs 序来弄
pre[_dfn[j]] = 0;
add(pre[_dfn[j]], pre[tr[_dfn[j]].fail]);
if(_end[_dfn[j]]) add(pre[_dfn[j]], f[_end[_dfn[j]]]);
}
rep(j, 1, n) {
// 计算 f_i
int res = 0;
for(int u : id[j]) add(res, pre[u]);
for(int u : lca[j]) del(res, pre[u]);
del(res, f[j]);
f[j] = res;
}
rep(j, 1, n) add(ans[i], f[j]);
}
rep(i, 0, n - 1) cout << ans[n - i] << ' ';
cout << endl;
}
}ACAM;
void work() {
cin >> n;
ACAM.init();
rep(i, 1, n) {
string st;
cin >> st;
ACAM.insert(st, i);
}
ACAM.build();
ACAM.Do();
return;
}
int main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> T;
while(T--) work();
return 0;
}

浙公网安备 33010602011771号