【香蕉OI】阅读(AC自动机、拓扑排序)

文章目录

  • 题意
  • 思路
  • 注意
  • 代码

题意

n ( n ≤ 1 0 5 ) n(n\le 10^5) n(n105) 个字符串, ∑ l ≤ 1 0 5 \sum l\le 10^5 l105

现在要将所有字符串的所有前缀分组,保证每组内的字符串不能有包含关系。

求最小的组数。

思路

首先考虑建出 AC 自动机,每个前缀就是 AC 自动机上的一个节点。我考虑不出来,但是好像处理字符串也就那么几个算法,挑一个用就好了。

然后考虑子串在 AC 自动机上的表示,即 t t t s s s 的子串的话,那么一定有一条路径,以 AC 自动机上边的反向边和 fail 指针为单向边,从 s s s 所在的点走到 t t t 所在的点。

上面那个略微理解一下就好了, 树边的反向边指向的是原串的前缀, fail 指针指向的是原串的后缀。

那么想要一个没有包含关系的前缀集合,就是要找一些互不连通的点组成的点集。

那么想要最小化组数,就要最大化每次能够取出的点数。那么就把图建出来,每次把入度等于 0 的点取出来。暴力计算就好了,因为长度之和也就是前缀数量很少。

注意

AC 自动机的 fail 边一定要等整棵 trie 树建完之后才建。因为不是单模匹配,所以不能像 KMP 一样加一个字符就连一条 fail 。

代码

// 10.29 T3
#include
using namespace std;
const int N = 1e6 + 10, S = 26;
int n, ans;
char s[N];
int ch[N][S], fa[N], fail[N], dgr[N], cnt[N], ncnt;
queue<int> que, que1;

void insert(int n, char *s){
	int now = 1, tr;
	for (int i = 1; i <= n; ++ i){
		tr = s[i]-'a';
		if (!ch[now][tr]){
			ch[now][tr] = ++ncnt;
			fa[ncnt] = now;
			dgr[now]++;
		}
		now = ch[now][tr];
		cnt[now]++;
	}
}

void build()
{
	fail[1] = 0;
	for (int i = 1; i <= ncnt; ++ i)
		for (int j = 0; j < S; ++ j)
			if (ch[i][j]){
				for (int k = fail[i]; k; k = fail[k])
					if (ch[k][j]){
						fail[ch[i][j]] = ch[k][j];
						break;
					}
				if (fail[ch[i][j]] == 0) fail[ch[i][j]] = 1;
				dgr[fail[ch[i][j]]]++;
			}
}

int main()
{
	scanf("%d", &n);
	ncnt = 1;
	memset(dgr, 0, sizeof dgr); dgr[1] = 1;
	memset(cnt, 0, sizeof cnt);
	for (int i = 1; i <= n; ++ i){
		scanf("%s", s+1);
		insert(strlen(s+1), s);
	}
	build();
	for (int i = 1; i <= ncnt; ++ i)
		if (dgr[i] == 0)
			que.push(i);
	ans = 0;
	while (!que.empty()){
		ans++;
		while (!que.empty()){
			int u = que.front(); que.pop();
			cnt[u]--;
			if (cnt[u]) que1.push(u);
			else{
				dgr[fail[u]]--;
				if (dgr[fail[u]] == 0) que1.push(fail[u]);
				dgr[fa[u]]--;
				if (dgr[fa[u]] == 0) que1.push(fa[u]);
			}
		}
		while (!que1.empty()){
			que.push(que1.front()); que1.pop();
		}
	}
	printf("%d\n", ans);
	return 0;
}

你可能感兴趣的:(题解)