Sicily 1222. 单词选择 (Trie & 二分)

题意
      给定n个单词,再给出一段包含m个单词的文章,在这篇文章里找出连续的一段话,这段话要尽可能地包含上述的n个单词,在这个基础上,找出单词数最少的一段,输出其包含了多少个单词“属于上述n个单词里的”,还有这段话的单词个数。不知道我的表述会不会很难理解,具体看题目。。

数据范围的一些限定
1 <= n <= 1000
1 <= m <= 100000
单词长度不超过10个字符,貌似全是小写字母

思路
      时间限制是1s,数据还是蛮大的,暴力肯定超时到跪,要有高效一点的办法。
      暴力的做法是,输入的时候记录m个单词中有多少个是在n个单词中出现过的,记为amount,当然要注意去重,然后枚举连续段,判断当前连续段中所含的”包含在n个单词中的”单词的数目是否为amount,然后取符合题意的连续段的长度的最小值。
      从暴力的角度出发,有两个问题要解决。
      第一个要解决的问题是怎么查询一个单词是否包含在某一堆单词之中,解决这个问题,当然用Trie。
      然后是第二个问题,直接暴力枚举连续段的起点和终点当然是不够好的,于是采用二分的思想,二分枚举连续段的长度,然后再枚举起点(或者终点)。
      更具体一些的细节请自己看下面附上的代码吧,我已经做了很详细的注释了。

      以下是代码,各种多余头文件宏定义什么的请无视,在vimrc里写好了,懒得删掉了,可以视为我在装13。如果在其中发现有错误,请指出,发表此文部分原因是希望有人能够指出我可能存在的错误。 如果有人有更好的解法,请不吝赐教。
/*
 * Author:  Fiend
 * Created Time:  2013/4/7 14:02:48
 * File Name: test.cpp
 */
#include <iostream>
#include <cstdio>
#include <cstddef>
#include <cmath>
#include <algorithm>
#include <string>
#include <cstring>
#include <vector>
#include <bitset>
#include <stack>
#include <queue>
#include <set>
#include <map>
#include <cctype>

#define ST size_type
#define PB push_back
#define LL long long
#define MAXN 1005
#define MAXM 100005
#define NODE_SIZE 10005
#define SIGMA_SIZE 30

using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::bitset;
using std::vector;
using std::pair;
using std::swap;
using std::sort;
using std::max;
using std::min;

const int inf = 0x3fffffff;

typedef pair<int, int> pii;
typedef vector<int> vi;
typedef vector<int>::iterator vit;

class Trie {
	private:
		int child[NODE_SIZE][SIGMA_SIZE];//用之前搞定两个宏
		int val[NODE_SIZE];		//记录附加信息
		int cnt;	//结点总数
	public:
		//初始时只有一个根结点
		Trie () {
			cnt = 1;
			memset (child[0], 0, sizeof (child[0]));
		}

		//将trie重置为只有一个根结点
		void clear () {
			cnt = 1;
			memset (child[0], 0, sizeof (child[0]));
		}

		//返回字符ch的编号
		int getIndex (char ch) {
			return ch -'a';
		}

		/*
		 * 查询字符串target是否在trie中,是则返回其附加的信息,否则返回0
		 * target是string类型
		 * 对象做实参要加引用,不然每次一调用函数就要复制一个对象,很影响效率
		 */
		int query (string &target) {
			int parent = 0, index;
			for (string::size_type i = 0; i != target.size(); ++i) {
				index = getIndex (target[i]);
				if (child[parent][index] == 0)
					return 0;
				parent = child[parent][index];
			}
			return val[parent];
		}

		/*
		 * 插入字符串src,附加信息为v,v必须非0,0代表“本结点不是单词结点”
		 * src是string类型,插入前不用查询是否存在
		 * 对象做实参要加引用,不然每次一调用函数就要复制一个对象,很影响效率
		 */
		void insert (string &src, int v) {
			int parent = 0, index;
			for (string::size_type i = 0; i != src.size(); ++i) {
				index = getIndex (src[i]);
				if (child[parent][index] == 0) {	//结点不存在
					child[parent][index] = cnt;		//新建结点
					memset (child[cnt], 0, sizeof(child[cnt]));
					val[cnt] = 0;	//非单词结点的附加信息为0
					++cnt;
				}
				parent = child[parent][index];	//往下走
			}
			val[parent] = v;//字符串的最后一个字符即单词结点,其附加信息为v
		}
};

Trie t;		//n个单词构成的Trie
int visited[MAXN];
string atc[MAXM];	//保存m个单词

bool check (Trie &t, int m, int length, int amount) {
	// 对象做实参要加引用啊,不然每次一调用函数就要复制一个对象,很影响效率
	//count记录所检测的连续段中,含有的在Trie中的单词的数目
	int count = 0, index;
	memset (visited, 0, sizeof (visited));
	for (int i = 1; i <= length; ++i) {//从第一个单词开始的长度为length的连续段
		index = t.query(atc[i]);
		if (index != 0) {
			if (visited[index] == 0)//之前没有出现过附加信息(即编号)为index的单词
				++count;
			++visited[index];//增加当前连续段内包含的该单词的个数
		}
	}
	//从第一个单词开始的长度为length的连续段里是否含有amount个包含在Trie中的单词
	if (count == amount)
		return true;

	//枚举长度为length的连续段的结束位置,从length + 1开始到m结束
	//相当于一个固定宽度的滑动窗口向右滑动
	for (int upperbound = length + 1; upperbound <= m; ++upperbound) {
		//当前连续段相比上一个连续段多了一个单词,少了的一个单词的处理在下面一段代码
		index = t.query(atc[upperbound]);
		if (index != 0) {
			if (visited[index] == 0)
				++count;
			++visited[index];
		}

		/*
		 * 去掉上一个连续段的第一个单词
		 * 此时需要判断被去掉的单词是否出现在当前连续段里
		 * visited数组定义为int型而不是bool型的作用体现出来
		 */
		index = t.query(atc[upperbound - length]);
		if (index != 0) {
			--visited[index];
			if (visited[index] == 0)
				//如果被去掉的单词不再在当前连续段内出现,则其visited必为0
				--count;
		}
		if (count == amount)//当前连续段中是否含有amount个包含在Trie中的单词
			return true;
	}
	return false;
}

int main () {
	int n, m, index, count, low, high, length, ans;
	string tmp;
	while (cin >> n) {
		/*
		 *建立Trie,将n个单词插入Trie中
		 *不知道有没有重复的
		 *不过有重复的也不怕,因为insert函数具有去重功能
		 */
		t.clear();
		for (int i = 1; i <= n; ++i) {
			cin >> tmp;
			t.insert(tmp, i);
		}

		/*
		 * 输入m个单词并一一查询其是否在Trie中
		 * count统计这m个单词中总共有多少个是从上述n个单词中来的
		 * 利用visited数组去重
		 */
		cin >> m;
		count = 0;
		memset (visited, 0, sizeof(visited));
		for (int i = 1; i <= m; ++i) {
			cin >> atc[i];
			index = t.query(atc[i]);
			if (index != 0 && visited[index] == 0) {
				++count;
				visited[index] = 1;
			}
		}

		//二分枚举连续段的长度,求长度的下限
		high = m;
		low  = 0;
		while (low <= high) {
			length = (low + high) >> 1;
			if (check (t, m, length, count)) {
				ans = length;
				high = length - 1;
			} else
				low = length + 1;
		}
		cout << count << endl << ans << endl;
	}
	return 0;
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值