UVA - 1596 Bug Hunt

/*
  这题自己想的时候,想到可以用栈来匹配和拆掉括号,这个还是比较容易想到,而判断是“定义语句”,还是“赋值语句”,也可以想到用find函数,看能否找到等号。
  
  但是,具体到怎么完整实现的时候,思路就有点懵了,可见做题量还是不够T^T
  
  最后还是借用别人的思路,按照别人的框架,自己理解以后,再独立写了一遍
*/


/*
  法一:
  参考自blog:http://www.cnblogs.com/kunsoft/p/5312710.html
  (法一非常短,这个博主的功底真.相当不错)
  
  收获:
  * 逗号表达式,将逗号运算符后面的值作为表达式的返回值
  http://blog.csdn.net/fjb2080/article/details/5174171
  
  * 关于string::npos(在find函数找不到某个字符时,就会返回string::npos)
  http://blog.csdn.net/devil_pull/article/details/25478525
  
  * 可用 to_string函数和 stoi函数,实现string类型和int类型之间的相互转换
  http://www.cplusplus.com/reference/string/stoi/
  http://www.cplusplus.com/reference/string/to_string/
  
  * stoi函数 和 atoi函数的区别
  https://stackoverflow.com/questions/20583945/what-is-the-difference-between-stdatoi-and-stdstoi
  (atoi是C语言对char*字符串的函数,如果要对string使用,应该先调用c_str函数进行转化)
  
  
  * count函数,可计算某个字符在字符串中出现的次数
  也可以计算某个数组中,某个特定的值一共出现了多少次
  http://www.cplusplus.com/reference/algorithm/count/
  
  * 我觉得他代码最大的亮点,其实是他递归的处理。
  递归的写法总是有些抽象和难看懂的,看懂尚且难,能想到怎么写就更难了,所以,这道题最值得回顾的是他的递归思路。他是怎么递归的
  
  以及,还需要想明白,对赋值语句的左值和右值,他分别又是怎么用substr()函数分离出部分字符串,并传入同一个处理函数一起处理的
  
  这两点想清楚以后,这题也就差不多了,虽然他的代码不长,看懂却也不太容易,我自己看时,就加上了很多注释,以防复习时,又看不懂了
*/

#include <iostream>
#include <map>
#include <string>
#include <sstream>
#include <vector>
#include <algorithm> // count函数 
using namespace std;

int bug = -1;
vector<string> code; //存放所有代码 
map<char, int> arr; //array,存放所有已定义的数组的大小
map<string, int> val; //value,表示已经被初始化的元素,和其对应值的对应关系 

int get_val(const string& str) 
{
	if ( str.find("[") == string::npos ) return stoi(str);//说明中括号都已被去掉,字符串中只剩下数字(这种情况出现在 lval的计算调用get_val时)
	
	if ( count(str.begin(), str.end(), '[') == 1)
	{
		if (!val.count(str)) return -1;
		return val[str];
	} 
	//只有一个中括号时,不需要再成对去括号了,直接检查是否有定义该变量,有定义则返回
	if ( count(str.begin(), str.end(), '[') >= 2)
	{
		string cur = str.substr(str.find('[') + 1);
		int v = get_val(cur);
		//不断递归,每次去掉一层数组名和一个[
		if ( !val.count( str.substr(0, 2) + to_string(v) ) )
		return -1;
		return val[str.substr(0, 2) + to_string(v)];
		//补上这个数组名和[,并且确认此值之前是否初始化,若有则可直接返回,否则返回-1
	}
	
		//我自己敲的时候,由于if ( !val.count( str.substr(0, 2) + to_string(v) ) )   这句多敲了一个分号,导致WA,检查了特别久的bug(一定要细心细心再细心,不然这样的错误,真的很难发现)
		//其实讲道理,我这次本来都不可能发现的,我还是把原博主的AC代码,一段段复制过来修改,缩小范围,确定我的代码是在哪个区域由AC变为WA的,不断不断复制过来改动再缩小错误范围...折腾了很久,才发现是当初多敲了一个分号 T^T
}

void add_arr(const string& str)
{
	stringstream ss(str.substr(2));//下标为2的那一位,为数组的大小
	int v;
	ss >> v;
	arr[str[0]] = v;
}

int main()
{
	cin.tie(0);
	cin.sync_with_stdio(false);
	string str;
	
	while (code.clear(), val.clear(), bug = -1, true)
	{
		cin >> str;
		if (str == ".") break;
		while (true)
		{
			code.push_back(str);//代码都存入vector中,输入完再一起处理
			cin >> str;
			if (str == ".") break;
		}
		
	for (size_t i = 0; i != code.size(); i++)
		{
			if (code[i].find("=") == string::npos)
			add_arr(code[i]);
			else
			{
				string left = code[i].substr(0, code[i].find("="));
				string right = code[i].substr(code[i].find("=") + 1); //将字符串以 = 为界,分为两个串
				
				
				int l_val = get_val( left.substr( left.find("[") + 1,  left.find("]") - left.find("[") - 1 )  );
				
				//get_value的参数,去掉了字符串l中,所有的],还去掉了数组名和第一个[, 剩下的便只有[了
				//之所以可以这样写,是因为只有下标越界,和使用未初始化变量,这两种bug,所以默认括号都是一一匹配的,直接一次性去掉所有],再一层层去掉[,并不会影响最终结果
				
				//之所以要这样写,是为了分离出第一层[]外的部分,例如g[b[5]] = 7这句代码,b[5]是要判断,其是否有定义并初始化的;而g则是要判断,g数组是否有定义,g数组的元素个数,是不是大于其下标,也就是b[5]的值,故而要将第一层中括号里面的元素提取出来,单独传入函数处理
				
				//-------------分割线-------------
				
				//这条语句就是只删去所有末尾的]
				//因为作为右值的时候,层层[]嵌套的右值的数据,必须在拆完所有括号以后,得到一个确切的数值,只有对左值的合法性判断时,才需要先提取出第一个中括号种的内容,再进行有无初始化的函数判断
				
				int r_val = get_val( right.substr( 0, right.find("]") ) ); //此处在自己敲时,也犯了特别粗心的错误,不小心把]敲成了[,导致经历了很久不知为何的程序崩溃
				
				
				if (l_val == -1 || r_val == -1)
				{
					bug = i; break;
				}//数组没定义,或者下标越界
				
				// l_val表示的是坐值最外层中括号里包围的数,也即最外层数组的下标,对于 a[b[c[7]]]而言,则是指的b[c[7]]表示的整数值 
				if ( !arr.count(code[i][0]) || l_val >= arr[code[i][0]] )
				{
					bug = i; break;
				}
				
				string temp = code[i].substr(0, 2) + to_string(l_val); // temp是类似这样的字符串"arr[8",是不带]的
				val[temp] = r_val;//标注该元素已被初始化
			}
		}
		cout << bug + 1 << endl;
	}
	return 0;
}

/*
  < 其他看过并理解了代码的方法:(做新题遇到障碍时,可以回来用别的思路重敲一次,对这题的理解应该会更深) >
  
  法二:
  http://blog.csdn.net/majing19921103/article/details/44409711
  不太容易理解,不过这题比较难懂的地方就是递归,所以每种解法,都有一点点难以理解
  
  这个方法的优点是:getArray函数和calculateArray函数分开,前者用于实现数组名和数组大小的分离;
  后者用于计算string串的数值内容,其内部多次调用getArray将数组名分离出来并压栈,但在处理完所有括号以后,又采用LIFO的原则,不断找到某个下标对应的最靠近它的数组名,并判断下标是否超过这个数组的大小...循环往复
  
  同时,他的partition函数,用于实现等号左右两边字符串的分离。
  
  简单概括就是:这个博主的思路非常清晰,几乎每个功能都写出一个函数,看他的代码,不会有云里雾里的感觉
  
  我认为可以有改进的地方(较之法一):
  1.1 题目有说数组名从26个字母的大小写中选取,用string存数组名会影响速度,其实没有必要,map的前一键值用char就够了
  1.2 题目中列出的bug类型,没有括号不匹配这种,所以,用栈来去括号,总归还是没什么必要,而且使得左值或右值计算中,如果要算某个数组名对应的下标的整数值,需要不断对其里面的数组名压栈,直到里面没有数组名,最后却还要一一取出数组名,并结合其对应下标判断越界,使得代码复杂了些,比较容易出错。在括号配对方面,我觉得既然没说有bug,其实还是没必要用栈检查
  (虽然我一开始也想到用栈,但看完法一中博主的代码,我觉得用栈其实增加了很多不必要的代码量)
   
*/

/*
  法三:
  来自:http://blog.csdn.net/m0_37253730/article/details/69942016
  
  这个方法比前两种容易理解许多,它和法二有些类似,除了没有用栈来进行处理
  
  但是,这串代码的思路,真的十分清晰,所以我自己借鉴了他的思路以后,手敲了一次
  
  边敲也边思考了一下,为什么这个博主的代码能写的如此简洁,浅见如下:
  对map多次使用count函数,检查确定了count函数返回值不为0时,就果断地用了下标来表示map里的元素,使得这部分的代码十分简洁明了
  
  而反观法二,法二就基本没有这样,先用count判断能否取下标,再直接用下标
  
  **********最后再附上博主自己对这种解法的解释,复制自该博主的博客**********
  主要思路:
用map<string,int> 存放变量名和变量名对应的size
map<string,map<int,int> > 来存放哪几个id是被初始化过的了,只有这里面的值可以出现在[]里面。
check() 递归检查类似a[b[c[id]]] 这种类型。
递归到最里面,然后返回到上一层来检查这个值是否存在和是否被初始化过。
记得清空map, 以及substr(id,len)是从下标id开始,截取长度为len的串...而不是开始下标和结束下标。

	以下是我借鉴博主的思路,对自己代码的改进:
	****!有一点值得说明的地方是
	题目有说数组名只在26个字母的大小写间选取,所以,有些本需要用 find_first_of('[')+1 的地方,我是直接用了2,因为仔细读了题,我可以确定对去中括号的操作,找到的第一个[,必定是字符串中下标为1的字符
	
	所以,认真读题还是很重要的,这样有些细节上的处理,就可以简化了...
	
*/

#include <iostream>
#include <string>
#include <map>
#include <cctype> // isdigit函数 
#include <sstream>
using namespace std;

map<string, int> size; //标记数组大小
map<string, map<int, int> > num; //定义过的数组名,对int下标是否完成初始化
int judge; // judge的含义:在check函数中,用来标记下标数据是否合法,合法为0,否则标记为1;主要是为了标记,code中是否有bug 


int check(const string &s)
{
	if (isdigit(s[0])) //此处本来想return (int)s[0];的,后来想到不能这样,这个数字不一定只有一位,首位满足这个if里的条件,只能证明,该去的括号已经去了罢了
	{
		int temp;
		stringstream ss(s);
		ss >> temp;
		return temp;	
	} 
	
	string name = s.substr(0, s.find_first_of('[')); //去掉最外层中括号,并将最外层的数组名,和其对应的下标分离
	string inside = s.substr(s.find_first_of('[') + 1, s.find_first_of(']') - s.find_first_of('[') - 1);
	int in = check(inside);
	
	//判断nmae[in]中,name数组名是否经过定义,以及name[in]元素是否初始化
	if (size.count(name) == 0)
	judge = 1;
	else
	{
		if (in < 0 || in >= size[name]) judge = 1; //下标不在合法范围
		
		if (num.count(name) == 0) judge = 1; //该数组所有值尚未被初始化
		else if (num[name].count(in) == 0) judge = 1; //该数组又被初始化的元素,但是该下标对应的元素,尚未被初始化(同样非法) 
	}
	if (!judge) return num[name][in];
	return -1;
}

void definition(const string& s)
{
	string name = s.substr(0, 1); //截取出数组名
	string in = s.substr(2, s.find_first_of(']') - 1); //截取中括号内的内容,即指定的数组大小
	size[name] = stoi(in); 	 
}

void assignment(const string& s, int &flag)
{
	string left = s.substr(0, s.find_first_of('=')); //分离等式左边和右边
	string right = s.substr(s.find_first_of('=') + 1);
	
	string name_l = left.substr(0, 1); //截取等式左右的,最外层数组名
	string name_r = right.substr(0, 1);
	
	string inner_l = left.substr(2, left.find_last_of(']') - 2);//等式左值去一层中括号,判断最外层数组对应的下标内容,是否有定义且初始化,并且没下标越界;而等式右值不需去一层中括号,而是右值整体必须时一个已初始化的整数(原因在法一,此处处理的附近,有注释解释)
	
	int l_val = check(inner_l), r_val = check(right);
	if (judge == 1) flag = 1; //出错了
	else
	{
		map<int, int>tp; //temp
		if (size.count(name_l) == 0)  flag = 1; //左值的最外层数组名未定义
		else
		{
			if (l_val < 0 || l_val >= size[name_l]) flag = 1;
			else
			{
				if (num.count(name_l) == 0) //如果等式左边的值之前没被初始化,那么现在初始化
				{
					tp[l_val] = r_val;
					num[name_l] = tp;
				}
				else num[name_l][l_val] = r_val; //如果之前已经初始化,可以直接用下标(因为map的下标操作,在下标不存在时,回自己插入键并初始化为默认值,可能带来错误,故要先将不能直接用下标符号的情况,单独处理)
			}
		}
	 } 
	 
}

int ReadCode()
{
	string s;
	int cnt = 0; //计数,标记正在判断的,是读入的第几条code的正误,以便输出错误code在第几条
	size.clear();
	num.clear();
	int flag = 0; //flag标记有没有代码出错误,如果所有代码都没错,输出0 
	judge = 0;// 每次将表示是否出错的judge置0,表示还没出错
	while (getline(cin, s))
	{
		if (s == "." && cnt == 0) return 0; //一条代码都没有,就已遇到点,是输入结束的情况
		if (s == ".") //一组代码数据输入结束
		{
			if (!flag) cout << 0 << endl;
			return 1; //继续下组代码的输入 
		}
		
		cnt++; //表示当前输入的是普通代码
		if(!flag) //该if语句里的内容,只有在之前没有任何错误代码时,才有必要执行,如果之前有错误,则第一次出现错误的信息,其实已经输出了,不必再对以后代码的正误坐判断
		{
			if (s.find('=') == string::npos) definition(s); //说明是定义语句,传入s即可,因为题目设定,bug是出在赋值语句的
			else 
			{
				assignment(s, flag); //赋值语句时,传入s和判断code是否无bug的标记变量flag,注意函数中,flag应传入引用,使得其值可以被改变
				if (flag) cout << cnt << endl;
			}	 
		} 
	 } 
	 
}

int main()
{
	while (ReadCode())
	{
	}
	return 0;
}


转载于:https://www.cnblogs.com/mofushaohua/p/7789420.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值