写了NMB一个下午,晚饭随便磕了一块面包,一直在机房调试……
从10>40>70>100过了1个小时……
AC自动机 + 补全Trie树 + Trie树Dfs + Fail树Dfs + 树状数组优化Dfs贡献统计
首先是思路,一定会想到AC自动机(需要检查如此巨大的)
读入就是一个大问题,如果遇到一个字符串长度为1e5并且为aPaPaPaPa……,如果每一次构造一个字符串并把当前存在的字符构造进 Trie 树之中的话,那么就会导致 O(N^2)的复杂度,70分——TLE!
考虑之前已经读入的点一定在 Trie 树上了,那么再加一个字符其实就是为其添加一个儿子,考虑记录 Trie 树上每一个节点的爸爸,记录一个全局变量 Nd,Nd 初值为0,记录 lenr 表示当前输入了的字符的最后一个位置,lenl表示已经在 Trie 树中出现过的节点所代表的字符的尾部,那么显然 lenl+1 到 lenr 的后就是待处理的字符的区间,这时候 Nd 在 lenl 的字符所代表的 Trie 树的节点的某一个儿子就是即将添加的字符所代表的节点,删除(’B‘)的时候 Nd 向上跳一个就好了
解决了读入的问题之后,想想怎么解题,首先 AC 自动机的一个性质就是某一个节点 u 的 Fail 指针指向的一定是 u 所代表的串的以 u 结尾的前缀的后缀的结尾节点:
如图所示:

可以看出 b 是前缀 ab 的后缀,bc 是前缀 abc 的后缀
那么统计 X 串在其他所有串中出现的次数的话就是统计 X 串的结尾节点连出去了多少个 Fail,那么如何统计 X 在一个特定的 Y 中出现的次数呢?我们可以先处理出所有的 Fail(在补全 Trie 图上),那么所有的 Fail 可以形成一棵 Fail 树,对于某一个单词的结尾节点的子树大小,就是本串在所有单词中出现的次数
那么其实对于单个单词来说,我们可以先预处理出 Fail 树的 Dfs 序,并且吗,每次枚举每个单词所代表的点的编号在这个 Dfs 序中出现的次数+1,那么相当于在每个结尾节点,只要在 Fail 树中它在结尾节点的子树里面的话就可以产生 1 的贡献,然而这样最坏是 O(N^2)的,优化了读入也没什么卵用……
其实可以将询问离线(没有强制在线的询问基本都可以考虑一下离线做法),对于某一个 Y 来说,可以存储其需要询问的所有 X 字符串的编号,再对为一个字符串建立一个映射 tail(i),表示串 i 的结尾节点在 Trie 树上的编号是 tail(i),那么可以在原 Trie 上跑 Dfs 啦!每一次到达一个单词的节点就可以在 Dfs 序上修改+1,退出时再减1,但是如何快速统计子树和呢?随随便便写个线段树或者树状数组就好了,我太懒了就只会用树状数组……
AC代码:
# include <bits/stdc++.h>
const int N = 100000 + 5 ;
int head [ N << 1 ] , nxt [ N << 1 ] , to [ N << 1 ] , cn ;
int ch [ N ] [ 26 ] , vis [ N ] [ 26 ] , fail [ N ] , danger [ N ] ;
int in [ N ] , out [ N ] , seq [ N ] , x [ N ] , y [ N ] , lenl , lenr ;
int c [ N ] , tail [ N ] , ans [ N ] , fa [ N ] ;
int n , siz , idc , num , nd ;
char s [ N ] , s1 [ N ] ;
std :: queue < int > q ;
std :: vector < int > Number [ N ] , Timex [ N ] ;
int lowbit ( int x ) {
return x & ( - x ) ;
}
void create ( int u , int v ) { // 邻接链表
cn ++ ;
to [ cn ] = v ;
nxt [ cn ] = head [ u ] ;
head [ u ] = cn ;
}
void insert (int No ) { // Trie树上的插入
int u = nd ;
for ( int i = lenl + 1 ; i <= lenr ; i ++ ) { // ( lenl , lenr ] 左闭右开!!
int v = s1 [ i ] - 'a' ;
if ( ! ch [ u ] [ v ] ) ch [ u ] [v ] = ++ siz ;
fa [ ch [ u ] [ v ] ] = u ;
u = ch [ u ] [ v ] ;
}
danger [ u ] = 1 ; // 单词结尾节点的danger为1
tail [ No ] = u ;
nd = u ; // 疯狂优化读入
}
void get_fail_tree ( ) { // AhoCrasick
for ( int i = 0 ; i < 26 ; i ++ )
if ( ch [ 0 ] [ i ] ) {
q . push ( ch [ 0 ] [ i ] ) ;
fail [ ch [ 0 ] [ i ] ] = 0 ;
}
while ( ! q . empty ( ) ) {
int tmp = q . front ( ) ; q . pop ( ) ;
for ( int i = 0 ; i < 26 ; i ++ ) {
int v = ch [ tmp ] [ i ] ;
if ( v ) {
fail [ v ] = ch [ fail [ tmp ] ] [ i ] ; // 搞Fail指针
q . push ( v ) ;
}
else {
ch [ tmp ] [ i ] = ch [ fail [ tmp ] ] [ i ] ; //补全Trie图
}
}
}
}
void dfs ( int u ) {
int v ;
seq [ ++ idc ] = u ;
in [ u ] = idc ;
for ( int i = head [ u ] ; i ; i = nxt [ i ] ) {
v = to [ i ] ;
dfs ( v ) ;
}
out [ u ] = idc ;
}
// 树状数组
void modify ( int x , int vl ) {
for ( int i = x ; i <= idc; i += lowbit ( i ) )
c [ i ] += vl ;
}
int query ( int x ) {
int sum = 0 ;
for ( int i = x ; i >= 1 ; i -= lowbit ( i ) )
sum += c [ i ] ;
return sum ;
}
// Trie的Dfs
void call ( int u ) {
if ( u ) modify ( in [ u ] , 1 ) ;
if ( danger [ u ] ) {
for ( int i = 0 ; i < Number [ u ] . size ( ) ; i ++ ) {
int tim = Timex [ u ] [ i ] ;
int p = Number [ u ] [ i ] ;
int aa = query ( out [ tail [ Number [ u ] [ i ] ] ] ) ;
int bb = query ( in [ tail [ Number [ u ] [ i ] ] ] - 1 ) ;
ans [ tim ] = aa - bb ;
}
}
for ( int i = 0 ; i < 26 ; i ++ ) {
int v = vis [ u ] [ i ] ;
if ( ! v ) continue ;
call ( vis [ u ] [ i ] ) ;
}
modify ( in [ u ] , - 1 ) ;
}
int main ( ) {
scanf ( "%s" , s ) ;
int len = strlen ( s ) ;
for ( int i = 0 ; i < len ; i ++ ) {
if ( s [ i ] != 'P' && s [ i ] != 'B' )
{
lenr ++ ;
s1 [ lenr ] = s [ i ] ;
}
else if ( s [ i ] == 'P' ) {
++ num ;
insert ( num ) ;
lenl = lenr ;
}
else if ( s [ i ] == 'B' ) {
if ( lenl == lenr ) lenl -- ;
lenr -- ;
nd = fa [ nd ] ;
}
}
scanf ( "%d" , & n ) ;
for ( int i = 1 ; i <= n ; i ++ ) {
scanf ( "%d%d" , & x [ i ] , & y [ i ] ) ;
Number [ tail [ y [ i ] ] ] . push_back ( x [ i ] ) ;
Timex [ tail [ y [ i ] ] ] . push_back ( i ) ;
}
for ( int i = 0 ; i <= siz ; i ++ )
for ( int j = 0 ; j < 26 ; j ++ )
vis [ i ] [ j ] = ch [ i ] [ j ] ; // 保证不能在补全Trie图上跑Dfs,会跑重复
get_fail_tree ( ) ; // 所以vis数组其实是Copy了原本没有补全的Trie树的节点信息
for ( int i = 1; i <= siz ; i ++ )
create ( fail [ i ] , i ) ; // 建立Fail树,一定是Fail(i)(深度比i低)连向i
dfs ( 0 ) ;
call ( 0 ) ;
for ( int i = 1 ; i <= n ; i ++ )
printf ( "%d\n" , ans [ i ] ) ;
return 0 ;
}
注意几点注释上的小细节

本文详细解析了使用AC自动机解决复杂字符串匹配问题的过程。针对大量字符串的匹配需求,介绍了如何通过优化读取方式和利用Fail树进行深度优先搜索来高效统计字符串出现次数。

663

被折叠的 条评论
为什么被折叠?



