P5231 JSOI2012 玄武密码
\(P5231\) [\(JSOI2012\)]玄武密码
一、题目背景
在美丽的玄武湖畔,鸡鸣寺边,鸡笼山前,有一块富饶而秀美的土地,人们唤作进香河。相传一日,一缕紫气从天而至,只一瞬间便消失在了进香河中。老人们说,这是玄武神灵将天书藏匿在此。
很多年后,人们终于在进香河地区发现了带有玄武密码的文字。更加神奇的是,这份带有玄武密码的文字,与玄武湖南岸台城的结构有微妙的关联。于是,漫长的破译工作开始了。
题目描述
经过分析,我们可以用东南西北四个方向来描述台城城砖的摆放,不妨用一个长度为 \(n\) 的序列 \(s\) 来描述,序列中的元素分别是 \(E,S,W,N\),代表了东南西北四向,我们称之为母串。而神秘的玄武密码是由四象的图案描述而成的 \(m\) 段文字。这里的四象,分别是东之青龙,西之白虎,南之朱雀,北之玄武,对东南西北四向相对应。
现在,考古工作者遇到了一个难题。对于每一段文字 \(t\),求出其最长的前缀 \(p\),满足 \(p\) 是 \(s\) 的子串。
二、题目解析
这题虽说是板子,但也不能只用板子,是一道比较不错的\(AC\)自动机题目。首先发现多模式串,二话不说上\(AC\)自动机,但是发现只有\(E\)、\(S\)、\(W\)、\(N\)这四个字母,所以可以优化一下建立起从\(E\)、\(S\)、\(W\)、\(N\)到\(a\)、\(b\)、\(c\)、\(d\)的映射就好了。
要查询每个模式串的前缀与母串的最大匹配长度,那我们就在\(Trie\)树上标记母串所有的可以匹配到的位置,然后对于每一个模式串,我们就从它的\(Trie\)树上进行检索,当发现第一个没有被母串匹配到的结点时,当前长度就是最大的匹配长度。
另外还有一个 小小的优化,如果说对于一个节点它已经被标记过了,那么它的\(ne\)链上的所有点一定也被标记了,所以这时就可以直接\(break\)。
三、实现代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5 + 10; //模式串的个数最多是1e5个
const int M = 1e7 + 10; //母串的最长长度不会超过1e7
char s[N][110]; // 模式串的长度最长不超过100
char T[M]; // 母串
int tr[M][4], idx; // AC自动机需要的Trie树,idx:点的索引号
int n, m; // 母串的长度n,m个模式串
bool st[M]; // st[i]:标识i结点是不是母串可以到达的位置
//这种字符向数字进行映射的问题,最好的办法就是if,其它的什么Hash化都是渣渣~
int get(char x) {
if (x == 'N') return 0;
if (x == 'E') return 1;
if (x == 'S') return 2;
if (x == 'W') return 3;
}
void insert(char *s, int x) {
int p = 0;
for (int i = 0; s[i]; i++) {
int t = get(s[i]); //这里有映射关系与标准模板是有差别的 int t = s[i] - 'a';
if (!tr[p][t]) tr[p][t] = ++idx;
p = tr[p][t];
}
}
//构建AC自动机,也就是在维护失配指针。这里与标准模板一模一样
int q[M], ne[M]; //这里使用M,是因为模式串个数是1e5个,每个长度是100,所以总共的基于模式串创建Trie树,节点总个数是1e5*100=1e7,恰好与M一样而已
void bfs() {
int hh = 0, tt = -1;
for (int i = 0; i < 4; i++)
if (tr[0][i]) q[++tt] = tr[0][i];
while (hh <= tt) {
int p = q[hh++];
for (int i = 0; i < 4; i++) {
int t = tr[p][i];
if (!t)
tr[p][i] = tr[ne[p]][i];
else {
ne[t] = tr[ne[p]][i];
q[++tt] = t;
}
}
}
}
//在通过模式串构建的Trie图上,跑一遍母串,目的是标识母串走了哪些点,这些点,就是每个模式串的最长前缀所到达的位置
//在query操作中,将对于母串到达的每个位置进行标记
void query(char *s) {
int p = 0; //从根开始,根节点在模板中设置为0
for (int i = 0; s[i]; i++) { //枚举母串的每个字符,看看母串都可以到达哪些 Trie树上的节点
p = tr[p][get(s[i])]; //从根开始,通过输入的字符,映射到相应的数字0~3,找到它可以到达的点
for (int j = p; j; j = ne[j]) { // p点可以到达,那么一旦下一个位置失配时,ne[p]也可以到达的,因为它们有共同的前缀?
//另外还有一个小小的优化,如果说对于一个节点它已经被标记过了,那么它的$ne$链上的所有点一定也被标记了,所以这时就可以直接$break$。
if (st[j]) break; // 这句加上,性能会好一点; 不加也一样能AC。
st[j] = true; // 标识j点是母串可以到达的点
}
}
}
int main() {
//加快读入
ios::sync_with_stdio(false), cin.tie(0);
//第一行有两个整数,分别表示母串的长度 n 和文字段的个数 m
cin >> n >> m;
//第二行有一个长度为 n 的字符串,表示母串 T
//如果是输入一串字符串并保存到字符数组中,系统会自动在后面补\0,无需自己输入。
//也就是不需要知道到底这个字符串的真实长度,n无用
cin >> T;
// m个模式串
for (int i = 1; i <= m; i++) {
cin >> s[i];
/*
使用s[]数组记录都录入了哪些模式串,这里的s是一个二维数组,
第一维表示录入的顺序号,第二维是真正的字符串内容,最长长度不超过100
*/
insert(s[i], i);
}
//构建AC自动机
bfs();
//上母串,标记母串所有可以匹配到的位置
query(T);
//对于每一个模式串,我们就从它的$Trie$树上进行检索,当发现第一个没有被母串匹配到的结点时,当前长度就是最大的匹配长度
for (int i = 1; i <= m; i++) { //枚举每个模式串
int res = 0, p = 0;
for (int j = 0; s[i][j]; j++) { //枚举每个模式串的所有前缀
p = tr[p][get(s[i][j])]; //沿着Trie前进
if (st[p]) res = j + 1; //如果标识过此位置,那么结果更新为最长前缀的长度j+1
}
printf("%d\n", res); //输出最长长缀长度
}
return 0;
}