参考:
《算法竞赛入门经典-训练指南》
http://codeforces.com/blog/entry/14854
kuang bin blog
加上不存在的边,并压缩冗余转移
这个改进《训练指南》中说过, 本文的目的是总结并实现一个自用的模板。。
AC自动机每个节点有一个 fail 指针, 作用与KMP中相同。
假如我们的节点是这样的
// link 就是 fail 指针
const int MaxNode = 500000 + 50;
const int CharSet = 26;
int go[MaxNode][CharSet], link[MaxNode];
int val[MaxNode]; // 附加信息,例如判断是否为一个单词的结尾
int nodes;
在AC自动机中输入一个新的字符c,转移时会先沿着link指针走,遇到 go[c] not equal 0 的节点 或者到了根就会停下。
中间走的这一段都不存在c转移,在构造的时候已经知道这个事实了,所以可以把这一段多跑的路给压缩掉。
把原来不存在的转移都补上
就再也没有while了~~
void build() {
queue<int> q;
q.push(0);
while ( !q.empty() ) {
int u = q.front(); q.pop();
int fa = link[u];
for (int i = 0; i < CharSet; ++i) {
int v = go[u][i];
if ( v ) {
link[v] = u ? go[fa][i] : 0;
q.push(v);
} else {
go[u][i] = go[fa][i];
}
}
}
}
init();
ac_insert("hers");
ac_insert("she");
ac_insert("he");
ac_insert("s");
get_draw(root, "");
ac_construct();
match("she");
return 0;
改进后fail指针的指向
一开始写的是指针形式的,也是预先分配内存
struct Node {
Node *go[CharSet], *link;
int count;
};
不过指针的开销貌似会更大? 虽然看起来是一样的。。
hdu 2222 一直 MLE
所以实际的板子改成了开数组的
用last指针进一步提高转移效率
用last指针表示沿着link走,遇到的第一个为某个单词结尾的节点。
这样在match的时候,沿着 last 走而不是 link,遇到的都是单词节点
void build() {
queue<int> q;
q.push(0);
while ( !q.empty() ) {
int u = q.front(); q.pop();
int fa = link[u];
for (int i = 0; i < CharSet; ++i) {
int v = go[u][i];
if ( v ) {
link[v] = u ? go[fa][i] : 0;
last[v] = val[link[v]] ? link[v] : last[link[v]];
q.push(v);
} else {
go[u][i] = go[fa][i];
}
}
}
}
我的模板
namespace Trie {
const int MaxNode = 500000 + 50;
const int CharSet = 26;
int go[MaxNode][CharSet], link[MaxNode], last[MaxNode];
int val[MaxNode];
int nodes;
void init() {
nodes = 1;
memset(go[0], 0, sizeof(go[0]));
}
int allocate() {
memset(go[nodes], 0, sizeof(go[nodes]));
val[nodes] = 0;
last[nodes] = 0;
link[nodes ++] = 0;
return nodes - 1;
}
void add( const char* s ) {
int cur = 0;
for (; *s; ++ s) {
int id = *s - 'a';
if ( !go[cur][id] ) {
go[cur][id] = allocate();
}
cur = go[cur][id];
}
val[cur] += 1;
}
int match(const char* s) {
int cur = 0, ret = 0;
for (; *s; ++s) {
cur = go[cur][*s-'a'];
int tmp = cur;
if ( val[tmp] ) {
ret += val[tmp], val[tmp] = 0;
}
while ( last[tmp] ) {
tmp = last[tmp];
if ( val[tmp] ) {
ret += val[tmp], val[tmp] = 0;
}
}
}
return ret;
}
void build() {
queue<int> q;
q.push(0);
while ( !q.empty() ) {
int u = q.front(); q.pop();
int fa = link[u];
for (int i = 0; i < CharSet; ++i) {
int v = go[u][i];
if ( v ) {
link[v] = u ? go[fa][i] : 0;
last[v] = val[link[v]] ? link[v] : last[link[v]];
q.push(v);
} else {
go[u][i] = go[fa][i];
}
}
}
}
}
构造不包含模式的串
当 自动机 构造好后,我们有了 r 个状态。那么就存在一个 r x r的矩阵 T,T(i,j) 表示状态 i 转移到状态 j 的方法数
T 可以这样构造
// 输入 (当前状态,输入)
// 返回转移到的状态, -1 表示无效转移
int check(int st, int input) {
st= go[st][input];
// 如果当前节点或其后缀是模式串,则为无效转移
if ( tag[st] || last[st] ) return -1;
return st;
}
Mat T(r); // r 为状态总数
memset(T.m, 0, sizeof(T.m));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < CharSet; ++j) {
int st = check(i, j);
if ( st != -1 ) T.m[i][st] += 1;
}
}
称不包含模式的串为合法串
矩阵
A0=(1,0...0)
一共 r 个元素
A1=A0∗T
,构造出长度为1的合法串
An=A0∗Tn
,结合矩阵快速幂,就可以构造出长度为n的合法串
例题
POJ 2778 DNA Sequence
详解可以看这篇