【洛谷】P2414 [NOI2011] 阿狸的打字机

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

写了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 ;
}

注意几点注释上的小细节

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值