1. 程式人生 > 其它 >洛谷 P5357 【模板】AC自動機(二次加強版)

洛谷 P5357 【模板】AC自動機(二次加強版)

洛谷 P5357 【模板】AC自動機(二次加強版)

原題連結

Solution

演算法:\(AC自動機\)

顧名思義,這是一道 \(AC\)自動機題目。

乍一看,誒,這不跟 P3796 【模板】AC自動機(加強版) 差不多嗎?

(加強版)要求輸出出現次數最多的模式串,那這個(二次加強版)直接把每個模式串出現次數輸出出來不就完了嗎?

怎麼感覺還退化了呢QWQ。

交一發 ———— 於是成功拿到了 76 分的好成績,其它的都 \(TLE\) 了。

讓我們來分析一下原因,我們每次是暴力跳 \(fail\), 那麼如果出現這樣的串呢 \(aaaaa...aaaa\),這樣相當於要跳 \(len\) 次。

究其根本,一條 \(fail\) 鏈上的點會被多次修改,時間就浪費在這裡了。

複雜度最壞情況下會退化為 \(O(|s| * |t|)\)\(|s|\)\(|t|\) 均表示字串長度。於是我們的程式碼就會被卡掉。

既然暴力跳 \(fail\)\(T\),那麼我們如何讓它不多次修改(對於每個點只修改一次)呢?

這裡介紹兩種方法。

拓撲優化建圖

簡單來說,建 \(fail\) 指標時,更新 \(fail\) 的入度。

這樣一來,在跳 \(fail\) 時,將指向當前節點的所有點都更新完之後再更新當前節點,不就可以做到只更新一次了嗎。

由於個人習慣,程式碼中 \(S\) 是模式串,\(T\)

是文字串

完整程式碼

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>

using namespace std;

const int N = 2e6 + 10;
int trie[N][27], num[N], vis[N], id[N], fail[N];
int n, tot;
int match[N], ans[N], in[N];
string str, s;

void insert(string s, int id){
	int now = 0;
	int len = s.length();
	for(int i = 0; i < len; i++){
		int n = s[i] - 'a';
		if(!trie[now][n])
			trie[now][n] = ++tot;
		now = trie[now][n];
	}
	match[id] = now;		//標記字串結尾位置
}

void getfail(){
	queue<int> q;
	for(int i = 0; i < 26; i++)
		if (trie[0][i])
			q.push(trie[0][i]);
	while(!q.empty()){
		int now = q.front();
		q.pop();
		for(int i = 0; i < 26; i++){
			if(trie[now][i]){
				fail[trie[now][i]] = trie[fail[now]][i];
				in[fail[trie[now][i]]]++;			//更新入度
				q.push(trie[now][i]);
			}
			else trie[now][i] = trie[fail[now]][i];
		}
	}
}

void query(string s){
	int now = 0;
	for(int i = 0; i < s.size(); i++){
		now = trie[now][s[i] - 'a'];
		vis[now]++;			//只需打上標記即可
	}
}

void topu(){
	queue <int> q;
	for(int i = 1; i <= tot; i++)
		if(!in[i])
			q.push(i);
	while(!q.empty()){
		int x = q.front();
		q.pop();
		int y = fail[x];
		in[y]--;
		vis[y] += vis[x];			//向上更新
		if(!in[y])
			q.push(y);
	}
}

int main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; i++){
		cin >> str;
		insert(str, i);
	}
	getfail();
	cin >> s;
	query(s);
	topu();
	for(int i = 1; i <= n; i++)
		printf("%d\n", vis[match[i]]);
	return 0;
}

建 fail 樹

建出 \(fail\) 樹,直接在上面 \(dfs\),回溯的時候更新答案。

由於個人習慣,程式碼中 \(S\) 是模式串,\(T\) 是文字串

完整程式碼

#include <iostream>
#include <cstring>
#include <queue>
#include <vector>

using namespace std;

const int N = 2e5 + 10;
const int T = 2e6 + 10;
struct node{
	int v, nxt;
}edge[N << 1];
int head[N], cnt;
int n;
char s[N] ,t[T];
int trie[N][27], tot = 0, fail[N];
int match[N], vis[N];

void add(int x, int y){			//前向星建圖
	edge[++cnt] = (node){y, head[x]};
	head[x] = cnt;
}

void insert(char s[], int id){
	int len = strlen(s);
	int now = 0;
	for(int i = 0; i < len; i++){
		int x = s[i] - 'a';
		if(!trie[now][x]) trie[now][x] = ++tot;
		now = trie[now][x];
	}
	match[id] = now;		//標記第i個模式串結尾位置
}

//AC自動機模板
void build(){
	queue <int> q;
	for(int i = 0; i < 26; i++)
		if(trie[0][i])
			q.push(trie[0][i]);
	while(!q.empty()){
		int now = q.front();
		q.pop();
		for(int i = 0; i < 26; i++){
			if(trie[now][i]){
				fail[trie[now][i]] = trie[fail[now]][i];
				q.push(trie[now][i]);
			}else trie[now][i] = trie[fail[now]][i];
		}
	}
}

void query(char s[]){
	int now = 0;
	int len = strlen(s);
	for(int i = 0; i < len; i++){
		now = trie[now][s[i] - 'a'];
		vis[now]++;					//同理,只打上標記即可
	}
}

void dfs(int x){
	for(int i = head[x]; i; i = edge[i].nxt){
		dfs(edge[i].v);
		vis[x] += vis[edge[i].v];		//更新答案
	}
}

int main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; i++){
		scanf("%s", s);
		insert(s, i);
	}
	scanf("%s", t);
	build();
	query(t);
	for(int i = 1; i <= tot; i++)		//建fail樹
		add(fail[i], i);
	dfs(0);
	for(int i = 1; i <= n; i++)
		printf("%d\n", vis[match[i]]);	//輸出
	return 0;
}

End