HDU 2222 Keywords Search

题目大意:

        随着信息爆炸时代的到来所搜引擎进入我们每一个人的生活,比如向Google、百度之类的所搜引擎等。

        Wiskey也向将所搜引擎的功能加入到他的图像检索系统中,每一个图像都有自己的描述,当用户键入图像描述的关键词后,系统会检索出拥有能匹配最多关键词的描述的图像并返回给用户,为了简化问题,大致目标就是在一篇文章中(即一段描述中)检索出指定的字符串。

        现有多个测例(测例数题中给出),在每个测例中都会指定检索的字符串的数量N(N ≤ 10,000),并给出待检索的字符串,左后给出一篇文章(即描述),其中待检索字符串的长度不超过50,描述的长度1,000,000,对于每个测例都打印出在文章中出现了多少待检测的字符串(如果同一个字符串出现多次也只算一次)。

题目链接

注释代码:

/*                                          
 * Problem ID : HDU 2222 Keywords Search 
 * Author     : Lirx.t.Una                                          
 * Language   : C++                         
 * Run Time   : 812 ms                                          
 * Run Memory : 28720 KB                                          
*/ 

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>

//alphabet length
//字母表长
#define	ALPHN		26

//maximum word length
//检索的最大模式串长度
#define	MAXWDLEN	51
//maximum text length
//最大的文章长度
#define	MAXTXTLEN	1000001
//maximum state number
//自动机中状态的最大数量
#define	MAXSTATN	250001

using namespace std;

struct	Node {//trie树中的结点

	//count
	//对单词进行计数,和普通trie树中的用法一样
	//如果cnt=0则表示该状态不是模式字符串的结尾
	//如果cnt>0表示该状态是模式字符串的结尾同时该字符串出现过多次
	  //可能会有这种蛋疼的用户,他查找的关键字重复输入多次
	  //对于这种情况,如果在文章中出现该单词则由于输入多次的原因出现次数
	  //也会相应地累加
	  //因此这里用cnt而不是flag,cnt起到一举两得的作用(不仅可以表示是否为
	    //字符串结尾,也能表示该单词出现了多少次)
	int		cnt;
	//产生式的退出符号,即s --(a)--> r,即s -> ar,s和r分别为两种状态,a为终止符
	//s、r为非终止符
	//即stat[s].out[a] = r
	//因此out就相当于产生符号->
	int		out[ALPHN];//总共可以有26中产生式,弧上式不同的26个字母
	//fail指针,和KMP的next数组相似
	//当自动机中的指针(即状态s)和text中的当前字符不匹配时就会利用该指针
	//回退到和当前状态具有相同输入弧的最近状态,例如:
	//s -(a)-> s1
	//  -(b)-> s2 -(a)-> s3
	//则s3的fail指针就指向s1
	int		fail;
};

//对于KMP算法只能一次匹配一个模式串
//而对于AC自动机一次可以匹配多个模式串
//这里只不过是对检索的模式串利用trie树建立AC自动机
//然后将主串(就是所谓的文章或描述)放在AC自动机上扫描
//看看能否扫描到指定的模式串,并记录相关信息

char	w[MAXWDLEN];//word,即模式串
char	t[MAXTXTLEN];//text,即主串(文章)

//state,即自动机中的状态,形状为一trie树
//只不过就是在trie树中每个状态都加一个fail指针就变成了AC自动机
Node	stat[MAXSTATN];
int		tsn;//total state number,当前的总状态数,起始状态下标为0

void
insert(char *w) {//插入,即普通的trie树插入

	int		s;
	int		aph;

	s = 0;
	while ( *w ) {
	
		aph = *(w++) - 'a';

		if ( !stat[s].out[aph] ) {
		
			stat[s].out[aph] = ++tsn;
			s = tsn;

			continue;
		}

		s = stat[s].out[aph];
	}

	stat[s].cnt++;//注意,如果用户蛋疼的重复输入检索字符串则需要++
}

void
bd_AC_auto(void) {//build AC automator
	//建立AC自动机
	//其实建立AC自动机的第一步已经有insert完成了,即构造一棵
	  //初始的trie树
	//这个函数完成的是为状态添加fail指针

	queue<int>		q;//queue队列

	//state,即自动机中的状态,形状为一trie树
	//只不过就是在trie树中每个状态都加一个fail指针就变成了AC自动机
	int		s, ss, r;
	int		aph;//alpha,即当前检查的字母

	//初始化
	//将其实状态所能推出的状态入队
	//并将这些推出的第一层状态的fail指针指向初状态0
	//因为调用此函数之前已经memset(stat, 0, sizeof(stat))过了
	//因此将fail=0的语句可以省略
	for ( aph = 0; aph < ALPHN; aph++ )
		if ( ss = stat[0].out[aph] )
			q.push(ss);

	//建立自动关机的核心算法
	while ( !q.empty() ) {
	
		s = q.front();//从队列中获取一个当前状态
		q.pop();

		for ( aph = 0; aph < ALPHN; aph++ )//检查可从当前状态推出的所有子状态
			if ( ss = stat[s].out[aph] ) {
			
				r = s;
				//方法是先找到当前状态最近的并且可以推出ss状态的fail状态
				while ( r = stat[r].fail )
					if ( stat[r].out[aph] ) {
					
						//如果能找到则直接将该fail状态的-(aph)->状态赋给ss的fail
						stat[ss].fail = stat[r].out[aph];
						break;
					}

				if ( !r )//否则就代表fail一直回溯到了初始状态
					if ( stat[0].out[aph] )//但是有可能初始状态可以推出ss(这种情况很容易忽略!!!)
						stat[ss].fail = stat[0].out[aph];
				else//如果初始状态也推不出ss则只能让ss的fail指向初始状态了
					//可以这样理解,初始状态可以由任意字母推出,因此如果任意状态fail都可以回溯到初始状态
					stat[ss].fail = 0;

				q.push(ss);//由于s->ss存在所以要将ss入队待下次扩展
			}
	}//如此一来,trie树中所有状态都建立的fail指针,因此自动机建立完毕
}

int
query(char *t) {//查询,即在文章t中查询模式字符串

	int		cnt;
	int		s, r;//当前状态和回溯状态(即fail指向的状态)
	int		aph;//同上以个函数

	cnt = 0;//匹配到的数量
	s 	= 0;

	while ( *t ) {
	
		aph = *(t++) - 'a';

		//如果可从当前状态通过当前检查的字母aph推不出任何状态
		//则只能通过fail回退,直到能推出状态为止
		while ( !stat[s].out[aph] && s )
			s = stat[s].fail;

		//两种可能
		//一种是找到可通过aph推出状态的s(有可能回溯到了初始状态)
		//另一种是回溯到初始状态但不能推出任何状态,这种情况s就只能停留在0了
		  //就和KMP中j回溯到0也无法使mod[j + 1] == pri[i]的情形一样
		if ( !( s = stat[s].out[aph] ) )
			continue;//直接检查下一个主串字符

		//表示当前主串字符匹配成功,并且找到了匹配的状态s
		//现在对s以及s的fail状态进行检查
		//因为s的fail状态都和s一样具有相同的输入弧,因此都需要检查
		r = s;//r为回溯状态
		while ( r ) {
		
			if ( stat[r].cnt ) {//如果r为单词结尾
			
				cnt += stat[r].cnt;//则累加
				//由于只记录出没出现并不记录出现几次
				//因此记录完后就得清零,否则可能会重复计算
				stat[r].cnt = 0;
			}

			r = stat[r].fail;//当前状态检查好后再往前回溯检查上一级fail状态
		}
	}

	return cnt;
}

int
main() {

	int		tc;//test case,测例数
	int		n;//模式串数量

	scanf("%d", &tc);
	while ( tc-- ) {
	
		tsn = 0;

		memset(stat, 0, sizeof(stat));
		scanf("%d", &n);
		while ( n-- ) {

			scanf("%s", w);
			insert(w);
		}
		bd_AC_auto();
		scanf("%s", t);

		printf("%d\n", query(t));
	}

	return 0;
}

无注释代码:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>

#define	ALPHN		26

#define	MAXWDLEN	51
#define	MAXTXTLEN	1000001
#define	MAXSTATN	250001

using namespace std;

struct	Node {

	int		cnt;
	int		out[ALPHN];
	int		fail;
};

char	w[MAXWDLEN];
char	t[MAXTXTLEN];

Node	stat[MAXSTATN];
int		tsn;

void
insert(char *w) {

	int		s;
	int		aph;

	s = 0;
	while ( *w ) {
	
		aph = *(w++) - 'a';

		if ( !stat[s].out[aph] ) {
		
			stat[s].out[aph] = ++tsn;
			s = tsn;

			continue;
		}

		s = stat[s].out[aph];
	}

	stat[s].cnt++;
}

void
bd_AC_auto(void) {

	queue<int>		q;

	int		s, ss, r;
	int		aph;

	for ( aph = 0; aph < ALPHN; aph++ )
		if ( ss = stat[0].out[aph] )
			q.push(ss);

	while ( !q.empty() ) {
	
		s = q.front();
		q.pop();

		for ( aph = 0; aph < ALPHN; aph++ )
			if ( ss = stat[s].out[aph] ) {
			
				r = s;
				while ( r = stat[r].fail )
					if ( stat[r].out[aph] ) {
					
						stat[ss].fail = stat[r].out[aph];
						break;
					}

				if ( !r )
					if ( stat[0].out[aph] )
						stat[ss].fail = stat[0].out[aph];
				else
					stat[ss].fail = 0;

				q.push(ss);
			}
	}
}

int
query(char *t) {

	int		cnt;
	int		s, r;
	int		aph;

	cnt = 0;
	s 	= 0;

	while ( *t ) {
	
		aph = *(t++) - 'a';

		while ( !stat[s].out[aph] && s )
			s = stat[s].fail;

		if ( !( s = stat[s].out[aph] ) )
			continue;

		r = s;
		while ( r ) {
		
			if ( stat[r].cnt ) {
			
				cnt += stat[r].cnt;
				stat[r].cnt = 0;
			}

			r = stat[r].fail;
		}
	}

	return cnt;
}

int
main() {

	int		tc;
	int		n;

	scanf("%d", &tc);
	while ( tc-- ) {
	
		tsn = 0;

		memset(stat, 0, sizeof(stat));
		scanf("%d", &n);
		while ( n-- ) {

			scanf("%s", w);
			insert(w);
		}
		bd_AC_auto();
		scanf("%s", t);

		printf("%d\n", query(t));
	}

	return 0;
}

优化:

/*
 * Problem ID : HDU 2222 Keywords Search
 * Author     : Lirx.t.Una
 * Language   : C++
 * Run Time   : 890 ms
 * Run Memory : 28712 KB
 */

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>

#define	AN	26

#define	MAXM	50
#define	MAXT	1000000
#define	MAXS	250000

using namespace std;

struct	Node {

	int		out[AN];
	int		prv;
	int		type;
};

Node	stat[MAXS];
char	m[MAXM + 1];
char	t[MAXT + 1];
int		tsn;

void
insert(char *m) {

	int		s;
	int		a;

	s = 1;
	while ( *m ) {

		a = *m++ - 'a';
		if ( !stat[s].out[a] ) {
		
			stat[s].out[a] = ++tsn;
			s = tsn;

			continue;
		}

		s = stat[s].out[a];
	}

	stat[s].type++;
}

void
build(int n) {

	int		s, ss, r;
	int		a;

	queue<int>		q;

	memset(stat, 0, sizeof(stat));
	for ( a = 0; a < AN; a++ ) stat[0].out[a] = 1;
	tsn = 1;
	q.push(1);
	while ( n-- ) {
	
		scanf("%s", m);
		insert(m);
	}

	while ( !q.empty() ) {

		s = q.front();
		q.pop();

		for ( a = 0; a < AN; a++ )
			if ( ss = stat[s].out[a] ) {
			
				q.push(ss);

				r = s;
				while ( true )
					if ( stat[ r = stat[r].prv ].out[a] ) {
					
						stat[ss].prv = stat[r].out[a];
						break;
					}
			}
	}
}

int
run(char *t) {

	int		cnt;
	int		s, r;
	int		a;

	cnt = 0;
	s	= 1;

	while ( *t ) {
	
		a = *t++ - 'a';
		
		while ( !stat[s].out[a] ) s = stat[s].prv;
		if ( !s ) { s = 1; continue; }

		s = stat[s].out[a];
		r = s;
		while ( r != 1 ) {
		
			if ( stat[r].type ) {
			
				cnt += stat[r].type;
				stat[r].type = 0;
			}

			r = stat[r].prv;
		}
	}

	return cnt;
}

int
main() {

	int		tst;
	int		n;

	scanf("%d", &tst);
	while ( tst-- ) {
	
		scanf("%d", &n);
		build(n);
		scanf("%s", t);
		printf("%d\n", run(t));
	}

	return 0;
}
单词解释:

Wiskey:人名,酒名,威士忌

retrieval:n, 检索

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值