UVA - 1597 Searching the Web

/*
  法一借鉴自bolg:http://blog.csdn.net/code4101/article/details/39587211
  
  收获:
  
  1. typedef定义数组类型,格式应为 typedef int int_array[80]; 此后int_array可作为有80个元素的int数组的简写
  http://blog.csdn.net/huang_xw/article/details/8273655
  
  2. 几个好用的define宏定义,简化代码
  
	#define rep(i,b) for(int i=0; i<(b); i++)
	#define foreach(i,a) for(__typeof((a).begin()) i=a.begin(); i!=(a).end(); ++i)
		
	***注意:__typeof函数在英文前面,是2个_,不是一个_
		
	同时学到了_typeof关键字的使用
	http://blog.csdn.net/cx132123/article/details/6641735
		
  3. 逗号表达式的返回值
  概括就是:在C++中,逗号表达式有时候是一种很有用的工具:(A,B,C),则从左到右求值,最后C的(返回)值作为整个表达式的值
  http://blog.csdn.net/songzitea/article/details/52177294
  
  4. 还有就是,由于文章输入用的是getline,getline是能读取空串(整个串只有一个回车符)的,所以务必记得,输入n、m两个数字之后,都有getchar(),否则,轻则PE,重则WA
*/


#include 
#include 
#include 
#include 
#include 
#include  //字母的判断和转换,用里的 isalpha 和 tolower,可以极大简化代码 
#include 
using namespace std;

#define rep(i, n) for (int i = 0; i < (n); i++)
#define foreach(i, a) for (__typeof( (a).begin() )i = a.begin(); i != (a).end(); i++) 
//这个头文件对STL类的,都很实用,学习了 
#define FOR for(int j = limit[i]; j < limit[i + 1]; j++) //遍历同一篇文章的所有行数
const int maxn = 1500 + 5;
typedef bool Bit[maxn]; 


int n, m, lines; // n:文档数,m:请求数,lines:文章行数
int limit[105]; //limit[i]记录第i篇文章从哪一行开始
string doc[maxn]; //储存文章
map Index; // Index 的 key位单词,Bit[i]为true,表示标记key对应的单词在第一行出现过 

//将读取的某行做处理,分割为一个个单词,以记录某个单词在哪一行出现过
void update(string s, int p)
{
	string word;
	foreach(it, s)
	{
		if (isalpha(*it)) *it = tolower(*it);
		else *it = ' '; 
	}
	stringstream ss(s);
	while (ss >> word) Index[word][p] = true;
 } 

int main()
{
	cin >> n;
	getchar();//注意要有一个getchar()吃掉多余的回车,否则后面的那些串,都会恰好错位一行,最终PE
	rep(i, n)
	{
		limit[i] = lines;
		while (getline(cin, doc[lines]) && doc[lines] != "**********")
		{
			update(doc[lines], lines);
			lines++;
		}
	}
	limit[n] = lines; // 之所以此处要对limit[n]做处理,是因为 rep(i, n) 的宏定义中,i最多能取到(n-1),但 对FOR的宏定义中,有 j < limit[i + 1]这句,i+1的最大可能值,其实是可以取到 n的,所以如果不提前初始化好 limit[n](应初始化为第n篇文章的最后一行的行数+1,如程序中那样初始化),则必定WA 
	 
	string s; //存储请求
	Bit mark; //记录哪些行应输出(为了处理每篇文档输出后,还需输出的10个'-')
	
	bool *A, *B;
	cin >> m;
	getchar(); //注意要有一个getchar()吃掉多余的回车,否则后面的那些串,都会恰好错位一行,最终WA 
	for (int ii = 0; ii < m; ii++ )
	{
		getline(cin, s);
		
		if (s[0] == 'N')
		{
			A = Index[s.substr(4)];
			rep(i, n)
			{
				bool flag = true;
				FOR if (A[j]) //FOR代表遍历当前文章的所有行,找是否出现指定关键字,若出现,则不输出该文章 
				{
					flag = false;
					break;
				}
				FOR mark[j] = flag; //如果flag仍为true,说明整篇文章都没有给定的关键字,按照题目要求,该文章的所有行都要输出;否则,该篇文章全部不输出 
			}
		}
		else if (s.find("AND") != string::npos)
		{
			int pos = s.find(" AND "); //分离出所要找的两个关键词
			A = Index[s.substr(0, pos)];
			B = Index[s.substr(pos + 5)];
			memset(mark, 0, sizeof(mark));
			
			bool hasA, hasB; //分别在同一文章中,两个关键词是否都出现,必须两者都为true时,该行才可被标记为可输出
			rep(i, n)
			{
				hasA = hasB = false; //默认没出现
				FOR if (A[j]) 
				{
					hasA = true; break; // 遍历doc[i]这篇文章的所有行数,确定这篇文章中有无关键词A,有只需有一个(故可以用 break),但没有需要所有行全没有,对B同理 
				}
				FOR if (B[j])
				{
					hasB = true; break;
				}
				
				if (!(hasA && hasB)) continue; //若要输出,首先这篇文章必须同时包含A和B
				FOR mark[j] = (A[j] || B[j]); //按照题意,输出是输出至少包含一个关键字的,所以对于文章中已经同时出现A、B的情况以后,对于这篇文章要输出哪些行,只要是有至少一个关键词的行,都满足输出条件 
			 } 	 
		}
		else if ( s.find("OR") != string::npos )
		{
			int pos = s.find(" OR ");
			A = Index[s.substr(0, pos)];
			B = Index[s.substr(pos + 4)];
			rep(i, lines) mark[i] = (A[i] || B[i]);
			
			//注意对OR的处理,就并不需要按照文章遍历这步了,因为最终输出的,也就是出现至少一个关键词的行,所以,可以直接标记这些行,从这个角度,OR比AND和NOT这两种情况,可谓简单许多 
		}
		else
		{
			memcpy(mark, Index[s], sizeof(mark));
		}
		
		bool nowOut = false, hasOut = false; //nowOut标记当前是否需要输出10个连续-(此后简称“分隔符”),如果上一篇文章的查询有结果,即hasOut为true,自然应该输出;但是也要注意,分隔符只输出一次(在下一篇文章待输出的查询结果之前输出),所以要用两个bool变量做好控制
		//nowOut表示当前是否需要输出分隔符
		//hasOut表示当前(/上篇)文章有无查询结果输出,无输出则有一次输出分隔符的机会,在输出下一篇有输出结果的文章,的查询结果前输出(如果接下来几篇都无查询结果,分隔符就一直不输出)
		//一旦输出分隔符后,nowOut自然也应置false
		
		//同时,这种设定下,可以发现的是,如果n篇文章全都没有查询结果,那么hasOut和nowOut都为false,而只要某篇文章中有本次查询需要的结果,则两者必定不会都为false,可用这点判断是否输出 "Sorry, I found nothing."
		
		rep(i, n)
		{
			if (hasOut) nowOut = true; 
			 hasOut = false; 
			FOR if (mark[j])
			{
				if (nowOut)
				{
					cout << "----------" << endl;
					nowOut = false;
				}
				cout << doc[j] << endl;
				hasOut = true; //表示这篇文章有输出结果
			}
		}
		if (!hasOut && !nowOut) cout << "Sorry, I found nothing." << endl;
		
        cout << "==========" << endl;
    }
	 
	return 0;
}

/*
  法二借鉴自:
  http://blog.csdn.net/XieNaoban/article/details/52628860
  和
  https://github.com/morris821028/UVa/blob/master/temp/1597%20-%20Searching%20the%20Web.cpp
  
  
  收获:
  1. 如果对map的value,将其引用赋值给value的同类型,会比赋value值(map[key])的耗时少。
  因为如果只是赋值,此后每次对value的操作,都需要从map里面重新查找。而如果赋引用,相当于是一劳永逸,只用查找一次,减少了STL的使用对程序速度的影响
  
  详细的说法(本来写在注释中,现在单独剪切到收获中)
  
			set &t = word_line[com[0]];

			  按照blog: http://blog.csdn.net/XieNaoban/article/details/52628860
			  中的分析,此处的引用,可谓是这个方法的神来之笔,使得运行速度大大减少
			  
			  为什么能提高效率?
			  
			  我的猜测:
			  按照RLJ曾在《入门经典》P123提到的:虽然map每次插入、查找和删除时间,和元素个数的对数呈线性关系;虽然map每次插入、查找和删除时间,和元素个数的对数呈线性关系;
			  但是,在一些对时间要求非常高的题目中,STL是有性能方面的瓶颈
			  而用LRJ的这两句总结,来分析这题,如果t前面不用引用,那么每次调用t时,都要从map中找到对应的单词应对应的集合,虽然map的查找尚算高效的了,但是,该代码在后来多次用到了t,只有将t设置为引用,才可以真正减少每次查找的时间消耗
			  
			  实在巧妙!应该好好学习下这个小技巧,可在卡TLE的时候思索,怎么降低STL对速度的影响
  
  2. 有关set的详细总结:
  http://blog.csdn.net/howardemily/article/details/53573427
  
  3. 十分保险的清楚缓存的方式
  while (getchar() != '\n');
  这种情况下,不仅清除掉多余的空格,甚至如果空格之前有多余的空白字符,也一并清除掉,使得不影响下一次的有效输入
  
  4. 利用位运算的与和或,代替逻辑运算的与和或
  这点不详细解释了,注意观察代码中out的定义方式,和更改方式(就是有out的地方,都重点关注一下,就能发现是怎么用位运算代替逻辑运算的)
  
  但是要注意:我猜博主之所以要这样代替,是因为,有 |= 和 &= 运算符,但是,&&= 和 ||= 运算符是不存在的,所以是为了书写的简化了
  
  换成逻辑运算我试过了,也没问题,但是此时,就必须把 out放到有值中,一起进行逻辑与/逻辑或了
*/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define foreach(i, a) for (__typeof( (a).begin() )i = a.begin(); i != (a).end(); i++) 
#define rep(i, n) for(int i = 0; i < (n); i++)
const int INF = 0x3f3f3f3f;
using namespace std;
map > word_line; //表示特定单词,在哪些行数中出现过,行数存入一个集合,作为value键
vector doc; 
int limit[105];
int n, m, line; //分别依次为:文章数、指令数、当前所输入的行的行序号 

void storeInfo(string s, int line)
{
	string word;
	foreach(i, s)
	{
		if (isalpha(*i)) *i = tolower(*i);
		else *i = ' ';
	}
	stringstream ss(s);
	while (ss >> word)
	word_line[word].insert(line);
}

vector _command(string s)
{
	string word;
	vector temp;
	foreach(i, s)
	{
		if (isalpha(*i)) *i = tolower(*i);
		else *i = ' ';
	}
	stringstream ss(s);
	while (ss >> word)
	temp.push_back(word);
	return temp;
}

int main()
{
	string str;
	line = 0;
	cin >> n;
	while (getchar() != '\n');
	
	rep(i, n)
	{
		limit[i] = line; //记录每篇文章的起始行
		while (getline(cin, str) && str != "**********")
		{
			doc.push_back(str);
			storeInfo(str, line);
			line++;
		}
	}
	limit[n] = line; //边界处理 
	
	cin >> m;
	while (getchar() != '\n');
	rep(i, m)
	{
		getline(cin, str);
		vector com = _command(str);
		
		int hasOut = 0; //分隔两篇文章中的输出
		if (com.size() == 1) // [word]
		{
			set &t = word_line[com[0]];
			set::iterator it = t.begin();
			//注意,it是t的迭代器,也就是出现了该单词的迭代器,其对应的元素,都是含有该单词的行数的集合,所以可以直接输出对应行(doc[*it]),只要注意不同文章之间要有的分隔符即可
			
			rep(j, n)
			{
				//在确认单词对应的所在行数的集合不为空的情况下,迭代器不断自增,直到找到属于该篇文章的范围
				while (it != t.end() && *it < limit[j]) it++;
				
				
				//不为空且该关键词的确在这篇文章中出现过,才会有输出
				if (it != t.end() && *it < limit[j + 1])
				{
					if (hasOut) cout << "----------" << endl;
					hasOut = 1;
					while (it != t.end() && *it < limit[j + 1])
					{
						cout << doc[*it] << endl;
						it++;
					}
					
				}
			}
		 }
		 else if (com.size() == 2) // NOT [word]
		 {
		 	set &t = word_line[com[1]];
		 	set::iterator it = t.begin();
		 	rep(j, n)
		 	{
				//首先先定位到当前文章的范围(因为t中存储的是所有有该单词的行数,可是这些行分布在不同的文章中)
		 		while (it != t.end() && *it < limit[j]) it++;
		 		
				//如果it在所有文章中都没出现过,自然在当前遍历的文章中也没出现过,这种情况下,前一个循环中,迭代器it不会自增,所以要单独提出这种情况来考虑
				//而后一种,*it >= ...的情况,则说明这个单词在别的文章出现过,但是在这篇没有;所以我们在试图定位的时候,居然定位到了后面的文章中,说明这篇文章确实无该关键词,应该全部输出
		 		if (it == t.end() || *it >= limit[j + 1])
		 		{
		 			if (hasOut) cout << "----------" << endl;
					hasOut = 1;
					for (int k = limit[j]; k < limit[j + 1]; k++)
					cout << doc[k] << endl;
				}
		 		
			}
		 }
		 else if (com.size() == 3) // [word] AND / OR [word]
		 {
		 	set &s1 = word_line[com[0]];
		 	set &s2 = word_line[com[2]];
		 	set::iterator it = s1.begin();
		 	set::iterator jt = s2.begin();
		 	
		 	rep(j, n)
		 	{
		 		while (it != s1.end() && *it < limit[j]) it++;
		 		while (jt != s2.end() && *jt < limit[j]) jt++;
		 		
		 		int out;
		 		
		 		if (com[1] == "and") //AND要求这两个关键词,都在文章中出现过
		 		{
		 			out = 1;
		 			out &= ( it != s1.end() ) && ( *it < limit[j + 1]);
		 			out &= ( jt != s2.end() ) && ( *jt < limit[j + 1]);
				}
				else//OR要求这两个关键词,只要有一个出现过即可
				{
					out = 0;
					out |= ( it != s1.end() ) && ( *it < limit[j + 1]);
					out |= ( jt != s2.end() ) && ( *jt < limit[j + 1]);
				}
				if (!out) continue;//如果这篇文章查询结果无输出,则转而判断下一篇文章
				if (hasOut) cout << "----------" << endl;
				
				hasOut = 1;
				while (1)
				{
					int a = INF, b = INF;
					if (it != s1.end() && *it < limit[j + 1]) a = *it;
					if (jt != s2.end() && *jt < limit[j + 1]) b = *jt;
					
					if (a == b && a == INF) break;
					//此句不可漏掉,否则RE;它是为了判断,何时才能退出输出的循环;
					//如果前面的两个if,都没有给a或b改值,它们两的值都还是初始化为的INF,说明剩余的行,都不满足至少有一个关键词的输出要求
					
					if (a < b) it++;//如果第一个关键词出现的行数在前,不需改a,且将遍历第一个关键词所有出现行的迭代器,it,自增
					else if (a > b) jt++, a = b;//因为最终输出的是第a行,所以赋b给a
					else it++, jt++;//两者相等就不必改a了,不过两个迭代器都有必要变动
					
					cout << doc[a] << endl;
				}	
			}
		 }
		 if (!hasOut)
				cout << "Sorry, I found nothing." << endl;
				cout << "==========" << endl;
	}
	return 0;
}


你可能感兴趣的:(UVa,oj,算法竞赛入门经典)