1. 程式人生 > 其它 >【題解】[JSOI2012]玄武密碼

【題解】[JSOI2012]玄武密碼

題目連結

題意

輸出模式串在文字串中能夠匹配上的最長字首。

輸入格式

第一行有兩個整數,\(N\)\(M\),分別表示母串的長度和文欄位的個數;

第二行是一個長度為\(N\)的字串,所有字元都滿足是 E,S,W 和 N 中的一個;

之後\(M\)行,每行有一個字串,描述了一段帶有玄武密碼的文字。依然滿足,所有字元都滿足是 E,S,W 和 N 中的一個。

輸出格式

輸出有\(M\)行,對應\(M\)段文字。

每一行輸出一個數,表示這一段文字的字首與母串的最大匹配串長度。

思路

這是一道披上辣椒衣的AC自動機模板題。難點就在這層辣椒衣。

解決方法:先打標記,最後遍歷。

程式碼實現

#include<cstdio>
#include<queue>
#include<cstring>
#include<map>
using namespace std;
const int N=1e5+5;
int n,m;
int t[N*100][5],cnt,fail[N*100];
char s[N][105],txt[N*100];
bool vis[N*100];
map <char,int> mp; 

inline int ans(int x){
	int p=0,ret=0;
	int l=strlen(s[x]);
	for(int i=0;i<l;i++){
		int c=mp[s[x][i]];
		p=t[p][c];
		if(vis[p]) ret=i+1;
	}
	return ret;
}

inline void query(char x[]) {
	int p=0;
	for(int i=0; i<n; i++) {
		p=t[p][mp[x[i]]];
		int j=p;
		while(j && (!vis[j])){
			vis[j]=true;
			j=fail[j];
		}
	}
}

inline void build_graph() {
	queue <int> q;
	for(int i=0; i<4; i++) {
		if(t[0][i]) q.push(t[0][i]);
	}
	while(!q.empty()) {
		int p=q.front();
		q.pop();
		for(int i=0; i<4; i++) {
			if(t[p][i]) {
				q.push(t[p][i]);
				fail[t[p][i]]=t[fail[p]][i];
			} else t[p][i]=t[fail[p]][i];
		}
	}
}

inline void ins(char x[]) {
	int l=strlen(x);
	int p=0;
	for(int i=0; i<l; i++) {
		int c=mp[x[i]];
		if(!t[p][c]) t[p][c]=++cnt;
		p=t[p][c];
	}
}

int main() {
	mp['E']=0; mp['S']=1; mp['W']=2; mp['N']=3;
	scanf("%d%d",&n,&m);
	scanf(" %s",txt);
	for(int i=1; i<=m; i++) {
		scanf(" %s",s[i]);
		ins(s[i]);
	}
	build_graph();
	query(txt);
	for(int i=1; i<=m; i++) printf("%d\n",ans(i));
	return 0;
}

PS. 為了體驗不能用萬能頭的感覺,沒有用萬能頭。

反思:
必須承認,看了題解才寫出看上去能算優美的程式碼。
一開始想要直接把trie的節點弄成包羅永珍的大結點,就是說,把每個結點弄成大struct,用每個結點對映它對應的模式串,包括記錄它的深度,etc。然後發現寫起來很頭大。
看了一篇題解,發現可以通過先打標記,最後遍歷的方法來確定能匹配的字首長度。
這是一種儲存資料解題(離線)的思維。
事實上,題目提示得很明顯:先輸入文字串,後輸入模式串。這和一般AC自動機輸入資料的順序不同,或許提示著一種更自由地對待模式串的方式(指最後遍歷一遍)。