题解 P2444 【[POI2000]病毒】
Sooke
2017-08-22 22:02:15
## 思路概述
我们知道,AC自动机是一种多模字符串匹配算法。构造 Trie 树 后,在模式串末尾一位的结点作上标记。平常的 AC自动机 是尽量能多接触到这些标记,使总值最大。本题倒是有点奇葩,要构造一个可行的无限长文本串,使没有任何子串为给出模式串中的一个。也就是说,我们需要让平常 AC自动机 的查询操作,尽量避免标记,能用失配指针跳转就跳转。
因为要有无限长的可行串,根据 AC自动机 的原理,我们可以将结点连接到儿子的边当作一条单向边,同时失配指针也当作一条单向边,如果存在一个环,且环上没有任何危险标记(即病毒代码段末尾一位的结点专门作的编号),此时 AC自动机 能一直在环上匹配,并且永远也不会得到为模式串的一个子串,就像程序中的死循环一样。这个找环我们可以通过 dfs 来实现。
## 注意事项
- 1 . 我们需要建立两个布尔数组,其中一个布尔数组记录每个节点在当前 dfs 走的路径上有没有被选中,另一个布尔数组记录每个节点历史上有没有被访问过。如果当前路径形成回路,就找到环了,应该还是比较好实现的。
- 2 . 避免危险标记,也就是说如果下一个结点拥有危险标记,就不走那个结点。
- 3 . 在构造失配指针时,一个很明显的优化是:如果一个结点拥有了失配指针,它指向的结点如果有危险标记,自己必然也危险,因为它到根结点形成的串是自己到根节点的后缀。
## 代码实现
代码和注释都认认真真打了,如果上面的概述有点难懂的话……请结合代码和该行的注释进行理解吧。
```cpp
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <queue>
using namespace std;
queue < int > Q; // 标失配结点编号所需的队列
struct Node{
int t[2] , f; // t 数组为下个二进制位结点编号,f 为失配结点编号
bool c; // c 变量为该结点是否为危险代码段的最后一位
}; // 定义字典树结点结构体
struct AcAutomaton{
Node N[30001]; // 字典树的结点
int S = 0; // 字典树的当前非根节点总数
inline void Insert(char c[] , int l){
int nd = 0; // 定义当前点,当然从根节点 0 开始
for(int i = 0 ; i < l ; i++)
if(N[nd].t[c[i] - 48] == 0)
N[nd].t[c[i] - 48] = ++S , nd = S; // 没有目标节点,新建个
else
nd = N[nd].t[c[i] - 48]; // 有目标节点,直接转移当前点
N[nd].c = true; // 标上病毒段末尾的危险标记
} // 函数:向字典树内插入长度为 l 的病毒串
inline void PutFail(){
if(N[0].t[0] > 0)
Q.push(N[0].t[0]); // 根节点有没有 0 儿子,如果有推入队列
if(N[0].t[1] > 0)
Q.push(N[0].t[1]); // 根节点有没有 1 儿子,如果有推入队列
while(!Q.empty()){
int nd = Q.front() ; Q.pop(); // 得到队列首
for(int i = 0 ; i <= 1 ; i++)
if(N[nd].t[i] > 0){
Q.push(N[nd].t[i]);
int td = N[nd].f;
while(td > 0 && N[td].t[i] <= 0)
td = N[td].f; // 要不到根节点,要不找到最长匹配后缀段
if(N[td].t[i] <= 0)
N[N[nd].t[i]].f = 0; // 失配指针转移到根节点
else{
N[N[nd].t[i]].f = N[td].t[i];
if(N[N[td].t[i]].c)
N[N[nd].t[i]].c = true;
// 既然自己后缀都行不通,自己也危险
}
}else
N[nd].t[i] = N[N[nd].f].t[i];
}
} // 函数:设置失配指针
}; // 定义 AC 自动机结构体
AcAutomaton A; // 新建 AC 自动机
bool v[30001]; // v 为结点是否是当前搜索路径上的一点
bool f[30001]; // f 为结点是否是否被现在或过往搜索过
char c[30001];
int n;
void Dfs(int d){ // 通过搜索寻找有没有环
v[d] = true; // 作下路径标记
for(int i = 0 ; i <= 1 ; i++)
if(v[A.N[d].t[i]]){ // 根据路径标记判断是否拥有环
printf("TAK");
exit(0); // 找到了环,输出并退出程序
}else if(!A.N[A.N[d].t[i]].c && !f[A.N[d].t[i]]){
// 只有下一位不为危险结点并且有可能成环,才递归搜索
f[A.N[d].t[i]] = true; // 下一个结点已经被搜索过了
Dfs(A.N[d].t[i]);
}
v[d] = false; // 抹除路径标记
}
int main(){
scanf("%d" , &n); // 输入病毒串数量(这个不用我打注释了吧……)
for(int i = 1 ; i <= n ; i++)
scanf("%s" , c) , A.Insert(c , strlen(c)); // 输入并插入病毒串;
A.PutFail(); // 设置每个结点的失配指针
Dfs(0); // 从字典树的根结点开始找环
printf("NIE"); // 既然程序没有退出,就输出无解
return 0;
}
```