题意
\(n\) 个串,每次可以从每一个串中取出前缀,但每一次的取出的所有前缀不满足被包含或包含关系, 求取出所有前缀的最小次数,\(\sum l \leq 10^5\)。
变图, AC自动机
能发现一个串中最多能取出一个前缀,如果取出两个前缀,那么一定其中短的前缀是长的前缀的子串。
如果一个串的前缀能包含另一个串的前缀,那么就可以连一条有向边。
对于每个串都能对应一个点,如果在图中一点能到另一点,那么它们就一定是子串关系。
这样的图刚好和 AC自动机 中的 trie 树和 fail边所构成的图对应。
最小反链覆盖
图上某个点集,满足两两点集互不相交,称为反链。
这里要求的是AC自动机上的图的最小反链覆盖。
有个定理:在有向无环图中,最小反链覆盖 = 最长链的长度。
因此建出AC自动机后连fail边,跑DP即可。
代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int MAXN = 1000010;
const int INF = 0x7fffffff;
const int mod = 998244353;
template <typename T>
void Read(T &x) {
x = 0; T f = 1; char a = getchar();
for(; a < '0' || '9' < a; a = getchar()) if (a == '-') f = -f;
for(; '0' <= a && a <= '9'; a = getchar()) x = (x * 10) + (a ^ 48);
x *= f;
}
int n, root, cnt;
int trie[MAXN][27], fail[MAXN], deg[MAXN], tot[MAXN];
void Insert(char *a) {
int cur = root, n = strlen(a);
for (int i = 0; i < n; i ++) {
if (!trie[cur][a[i] - 'a']) trie[cur][a[i] - 'a'] = ++ cnt;
cur = trie[cur][a[i] - 'a'];
tot[cur] ++;
}
}
vector <int> e[MAXN];
void build() {
queue<int> q;
for (int i = 0; i < 26; i ++)
if (trie[root][i]) {
q.push(trie[root][i]);
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < 26; i ++) {
if (trie[u][i]) fail[trie[u][i]] = trie[fail[u]][i], q.push(trie[u][i]), e[u].emplace_back(trie[u][i]), deg[trie[u][i]] ++;
else trie[u][i] = trie[fail[u]][i];
}
}
for (int i = 1; i <= cnt; i ++)
if (fail[i])
e[fail[i]].emplace_back(i), deg[i] ++;
}
int f[MAXN];
char a[MAXN];
int main() {
ios::sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; i ++) {
cin >> a;
Insert(a);
}
build();
queue<int> q;
for (int i = 1; i <= cnt; i ++)
if (!deg[i]) q.push(i), f[i] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (auto v : e[u]) {
f[v] = max(f[v], f[u] + tot[u]);
if (!-- deg[v]) {
q.push(v);
}
}
}
int ans = 0;
for (int i = 1; i <= cnt; i ++)
ans = max(ans, f[i]);
cout << ans;
return 0;
}