AC自动机
AC 自动机
·赘述与前置
让大家失望の是, \(AC\) 自动机不是让你自动 \(\color{green}Accepted\) 的机器。
其实可以将 \(AC\) 自动机理解成一个在 \(Trie\) 树上跑的 \(KMP\)
此处的 \(KMP\)指的是一种算法思想,即利用失配时的有限信息来缩小时间复杂度,并不是真正的 \(KMP\).
\(\bf{前置知识:}\)
· \(First\) --用途与 \(Fail\) 指针
·用途
当给你一个模式串和一个目标串求解模式串的出现次数时,你会选择 \(KMP\) ;
但如果给你一堆模式串和一个目标串让你求解其总出现次数时, \(KMP\) 会 \(T\) .
这时,\(AC自动机,启动!\)
此为\(AC\)自动机求解的东西。
· \(Fail\) 指针
首要任务,是将这些模式串一股脑的塞进一棵 \(Trie\) 树里。
但在树里单个匹配,还是很慢。
此时仿照 \(KMP\) 的思想 , 使用失配指针 ( \(Fail\) )。
· \(Fail\) 含义
若 \(i\) の失配指针指到 \(j\) , 则从树根到 \(j\) 的这一段是从树根到 \(i\) 这一段的 \(最长后缀\)
为什么要存这个呢?
当我们匹配完 \(i\) 后, \(i\) 前面的任何一个连续的段都在这一目标串内被匹配。
如果存后缀的话,那么正好能接上 \(j\) 的儿子节点。
最长的话,那可以考虑,后面的还会接上此处,使之变短。
当然 \(fail\) 的作用是使 \(Tire\) 树变为有向图 ,方便以后的操作。
· \(Fail\) 的特性
-
\(root\) 的 \(fail\) 指向他自己。
-
\(root\) 的儿子的 \(fail\) 指针是 \(root\)
-
\(i\) 的 \(fail_i\) 的深度一定小于 \(i\) (显然的)
· \(Fail\) の求法
使用函数 \(Get\) _ \(Fail\)
首先我们将 \(0\) 的所有儿子全部指向 \(1\) 。(我们定义 $ 1 $ 是根 )
再将 \(1\) 放入队列之中 。
暴力枚举队首元素的各个儿子节点(此编号没儿子也枚举)。
-
如果他没有这个编号的儿子,那么他的这个编号的儿子是他的 \(fail\) 指针指向的点的这个编号的儿子。如果也没有,没关系,会指向根 ( $ 1 $ ) ;
-
如果他有这么一个儿子,那么这个儿子的 \(fail\) 指针指向:他父亲的的 \(fail\) 指针指向的点的这个编号的儿子
建议消化吸收一下。
自己手画几个图,就能明白什么意思了。
· \(Code\)
void Get_Fail( )
{
queue < int > q ;
for( int i = 0 ; i < 26 ; ++ i )
{
trie[ 0 ].son[ i ] = 1 ;
}
q.push( 1 ) ;
while ( !q.empty( ) )
{
int k = q.front( ) , failed = trie[ k ].fail ; q.pop( ) ;
for( int i = 0 ; i < 26 ; ++ i ) //暴力枚举队首元素的各个儿子节点。
{
int y = trie[ k ].son[ i ] ;
if( !y ) //1.
{
trie[ k ].son[ i ] = trie[ failed ].son[ i ] ;
continue ;
}
trie[ y ].fail = trie[ failed ].son[ i ] ; // 2.
q.push( y ) ;
}
}
}
· \(Second\) -- 查询操作
既然已知 \(Fail\) 了,那如何查询呢?(-_-)3 (挠头)
我们可以将目标串直接扔进 \(AC\) 自动机中匹配去
将经历过的点的 \(back\) 值标成 \(-1\),意为不可再进。
然后就暴力跳 $ fail $ ,具体看代码。
· \(Code\)
int query( char s[ ] )
{
int u = 1 , ans = 0 , len = strlen( s ) ;
for(int i = 0 ; i < len ; i ++ )
{
int v = s[ i ] - 'a' ;
int k = trie[ u ].son[ v ] ; //跳Fail
while( k > 1 && trie[ k ].back != - 1 ) //经过就不统计了
{
ans += trie[ k ].back , trie[ k ].back = - 1 ; //累加上这个位置模式串个数,标记已经过
k = trie[ k ].fail ; //继续跳Fail
}
u = trie[ u ].son[ v ] ;
}
return ans ;
}
· \(Third\) -- 优化 - > 拓扑排序
·如何优
上述代码的时间复杂度是有点小高的, \(Query\) 中暴力跳 \(Fail\) 的复杂度是 \(O( \ 模式串长度 \times 目标串长度 \ )\)
考虑优化。
我们发现其实大多数时间消耗在了跳 \(Fail\) 那里。
那如果我们从每一条由 \(Fail\) 指针连成的链,链头开始往下跳,能优化。
那么如何改变目标串的插入模式而得到优化呢?
我们可以还是让他在 \(AC\) 自动机上跑,在他每个经过的点增加标记,这样:
以他开始的,即他后面的(跳 \(Fail\) ) \(+=\) 这里的标记,因为比后面的长的都能匹配,那他也能匹配。而这个位置以前的标记数是跳到他自己的或其他的跑到他的,与新加的无关。
那么代码就清晰了。
· $Code \ of \ GetFail $
就在 \(y\) 定义出 \(Fail\) 时后增加 $ y の Fail $ 的入度:
in[ trie[ y ].fail ] ++ ;
· $Code \ of \ Query $
精简很多了。
inline void query( char Checkee[ ] , int length )
{
int x = 1 ;
for ( int i = 1 ; i <= length ; ++ i )
{
int j = Checkee[ i ] - 'a' ;
x = trie[ x ].son[ j ] ;
trie[ x ].ans ++ ;
}
}
· $Code \ of \ Topu $
严格按照上面的走
inline void Topu( )
{
queue < int > q ;
for ( int i = 1 ; i <= numbol ; ++ i )
{
if( !in[ i ] )q.push( i ) ;
}
while ( !q.empty( ) )
{
int k = q.front( ) ; q.pop( ) ;
vis[ trie[ k ].back ] = trie[ k ].ans ;
int y = trie[ k ].fail ; in[ y ] -- ;
trie[ y ].ans += trie[ k ].ans ;
if( !in[ y ] ) q.push( y ) ;
}
}
· 整体の代码
$ Keywords \ Search \ (HZOI) $
$ Keywords \ Search \ (vjudge) $
给定 $ n $ 个长度不超过 $ 50 $ 的由小写英文字母组成的单词准备查询,以及一篇长为 $ m $ 的文章,问:文中出现了多少个待查询的单词。多组数据。
对于全部数据,$ 1≤n≤10^4 $ , $ 1≤m≤10^6 $ 。
$ code : (Topu) $
#include<bits/stdc++.h>
const int N = 2e4 + 100 ;
const int M = 1e6 + 10 ;
using namespace std ;
char s[ N ] , Che[ M ] ;
int in[ N ] , vis[ N ] ;
int t , Map[ N ] ;
bool use[ N ] ;
struct Tire_AC_
{
int numbol = 1 ;
struct One_Node
{
int son[ 27 ] , fail , back , ans ;
}trie[ N ] ;
inline void insert( char Checkee[ ] , int length , int id )
{
int x = 1 ;
for ( int i = 1 ; i <= length ; ++ i )
{
int j = Checkee[ i ] - 'a' ;
if ( !trie[ x ].son[ j ] )
{
trie[ x ].son[ j ] = ++ numbol ;
}
x = trie[ x ].son[ j ] ;
}
if( !trie[ x ].back ) trie[ x ].back = id ;
Map[ id ] = trie[ x ].back ;
}
inline void Get_fail( )
{
queue < int > q ;
for ( int i = 0 ; i < 26 ; ++ i )
{
trie[ 0 ].son[ i ] = 1 ;
}
q.push( 1 ) ;
while ( !q.empty( ) )
{
int k = q.front( ) ; q.pop( ) ;
for( int i = 0 ; i < 26 ; ++ i )
{
int y = trie[ k ].son[ i ] , failed = trie[ k ].fail ;
if( !y )
{
trie[ k ].son[ i ] = trie[ failed ].son[ i ] ;
continue ;
}
trie[ y ].fail = trie[ failed ].son[ i ] ; in[ trie[ y ].fail ] ++ ;
q.push( y ) ;
}
}
}
inline void query( char Checkee[ ] , int length )
{
int x = 1 ;
for ( int i = 1 ; i <= length ; ++ i )
{
int j = Checkee[ i ] - 'a' ;
x = trie[ x ].son[ j ] ;
trie[ x ].ans ++ ;
}
}
inline void Topu( )
{
queue < int > q ;
for ( int i = 1 ; i <= numbol ; ++ i )
{
if( !in[ i ] )q.push( i ) ;
}
while ( !q.empty( ) )
{
int k = q.front( ) ; q.pop( ) ;
vis[ trie[ k ].back ] = trie[ k ].ans ;
int y = trie[ k ].fail ; in[ y ] -- ;
trie[ y ].ans += trie[ k ].ans ;
if( !in[ y ] ) q.push( y ) ;
}
}
void Clear_Tire( )
{
numbol = 1 ;
memset( Map , 0 , sizeof( Map ) ) ;
memset( in , 0 , sizeof( in ) ) ;
memset( vis , 0 , sizeof( vis ) ) ;
memset( use , 0 , sizeof( use ) ) ;
for( int i = 1 ; i < N ; ++ i )
{
trie[ i ].fail = 0 ;
trie[ i ].back = 0 ;
trie[ i ].ans = 0 ;
for ( int j = 0 ; j < 26 ; ++ j )
{
trie[ i ].son[ j ] = 0 ;
}
}
}
} tree ;
int n ;
signed main( )
{
cin >> t ;
while ( t -- )
{
tree.Clear_Tire( ) ;
cin >> n ;
//cout << 1 << '\n' ;
int len ;
for ( int i = 1 ; i <= n ; ++ i )
{
cin >> s + 1 ;
len = strlen( s + 1 ) ;
tree.insert( s , len , i ) ;
}
tree.Get_fail( ) ;
cin >> Che + 1 ;
len = strlen( Che + 1 ) ;
tree.query( Che , len ) ;
tree.Topu( ) ;
int answer = 0 ;
for( int i = 1 ; i <= n ; ++ i )
{
if( vis[ Map[ i ] ] )answer ++ ;
}
cout << answer << '\n' ;
}
}
· \(AC\) 自动机上跑 \(DP\)
这个...看例题吧。