第一次个人项目(2):拓展功能-四则运算自动生成
需求分析:编写程序能自动生成100以内四则运算题目,运算符不多于10个,必须出现括号及分数。还需要不能出现负数(小学生不会做),且问题不能重复。
再看到题目之后觉得较为棘手,尤其是问题不能重复,一时不知道从哪下手,但是经过简单的分析,我认为只需要利用中缀表达的一定规律便可构造出符合要求的算式。首先在不考虑括号的情况下,中缀表达式在形式上都是一个数字后跟一个算符,最后是等号。若不算等号,那么在一个式中必然有n个数字和(n-1)个四则运算符。既然这样,我想首先通过随机数确定生成的算式中运算符的数量,然后再以此随机生成对应的数字,在生成数字时需有一部分数字是分数。在生成后我们可以按照数字、符号、数字、符号的顺序插入形成一个逻辑上正确的算式。
在这基础上再考虑括号的加入,首先明确括号的使用方式,往往是在优先级高的算符前表示优先级低的算符要先进行计算时使用。例如1+2*3和(1+2)*3,所以我希望在生成了优先级较高的运算符如乘号、除号时,先检测在它之前的运算符,若优先级低于新加入的算符,则利用随机数确定是否在当前位置加入括号。
在加入括号后,就不能盲目按照数字、算符的方式插入。要先判断插入的符号是否为括号,若为左括号则先插入括号在插入数字和一个运算符,若为右括号则在插入括号之后还需插入一个运算符。
下面介绍具体实现,定义了表示数字的结构体,与上一篇中表示数字的方式大同小异,但没有加入符号的表示:
struct Number{ int inte;//整数部分 int deo;//分母 int ele;//分子 }; char tmp[20]; char cha[20];
字符型数组cha中存放随机生成的算符,下面是该程序的核心之一,生成数字和运算符:
1 void CreateNumber(int times){//根据times确定生成数字的数量 2 int iffrac;//根据随机数确定是否生成分数 3 for(int i = 0; i<=times;i++){ 4 iffrac = rand()%4; 5 if(!iffrac){ //生成分数 6 num[i].deo = rand()%98+2;//控制数字大小 7 num[i].ele = rand()%num[i].deo; 8 num[i].inte = 0;//可加整数部分 9 } 10 else{ 11 num[i].ele = 0; 12 num[i].deo = 1; 13 num[i].inte = rand()%100+1;//控制整数部分大小 14 } 15 } 16 } 17 void CreateChar(int times){//生成运算符号 18 int mode; 19 int i = 0; 20 for(int j = 0; j<times;j++){ 21 mode = rand()%4;//根据随机数值生成相应的符号 22 switch(mode){ 23 case 0: 24 cha[i] = '+';break; 25 case 1: 26 cha[i] = '-';break; 27 case 2: 28 if((cha[i-1]=='+')||(cha[i-1]=='-'))//若乘号优先级高于前一个符号是否添加括号 29 if(!(rand()%2)){ 30 cha[i] = cha[i-1]; 31 cha[i-1] = '('; 32 cha[i+1] = ')'; 33 i+=2; 34 } 35 cha[i] = '*';break; 36 case 3: 37 if((cha[i-1]=='+')||(cha[i-1]=='-'))//是否添加括号 38 if(!(rand()%2)){ 39 cha[i] = cha[i-1]; 40 cha[i-1] = '('; 41 cha[i+1] = ')'; 42 i+=2; 43 } 44 cha[i] = '%';break; 45 default:break; 46 } 47 i++; 48 } 49 cha[i]='=';//符号结尾用“=”表示 50 }
大量使用随机数来确定是否生成分数,是否添加括号等行为。
现在我们得到了一个随机的数字序列和一个随机但合乎逻辑的符号序列,现在只需要将它们按照顺序插入即可获得符合规则的算式:
1 void CreateQuestion(int quan,string &ans){//生成算式存放在ans中 2 int j; 3 4 CreateNumber(quan); 5 CreateChar(quan); 6 j = 0;//指示算符 7 for(int p = 0;p <= quan;p++){//显示算式 8 if(cha[j]=='('){//若有括号则显示括号 9 ans=ans+cha[j]; 10 j++; 11 } 12 DisplayNumber(p,ans);//显示数字 13 if(cha[j]==')'){//若有右括号则显示括号 14 ans=ans+cha[j]; 15 j++; 16 } 17 ans+=cha[j]; 18 19 j++; 20 } 21 }
这里为避免除号和分号的混淆,我用百分号"%"表示除号。生成的算式如下:
这样来看,拓展功能基本实现了,还剩下判重和非负数。这里我就使用了一个比较笨的方法,在判重时我在构造好新的算式后利用上次得到的方法进行计算,算出答案后把当前答案与之前所有的算式答案进行比较,若有答案相同,则放弃新生成的算式,重新进行生成。答案不同的算式绝对不会重复的。同理,当计算得到的答案为负数时也重新生成。以下是生成全部算式的过程:
1 int q = rand()%11+10;//生成题目个数 2 freopen("question.txt","w",stdout); 3 for(int i = 0; i<q;i++){ 4 do{ 5 memset(num,11*sizeof(Number),0);//清空上次题目数字 6 memset(cha,20*sizeof(char),0);//清空上次题目算符 7 memset(tmp,20*sizeof(char),0); 8 question.clear();//清除本次生成题目 9 CreateQuestion(rand()%4+1,question);//生成题目 10 11 ques = question.c_str(); 12 times = MiddleToSuffix(ques,word);//输入中缀表达式转为后缀表达式存入word数组 13 ans[i] = Calculate(word,times);//根据后缀表达式计算结果 14 YueFen(ans[i]);//约分 15 }while((ans[i].deo<=0)||(ans[i].ele<=0)||IsRepeat(ans,i));//若产生负数或分母为0或出现重复则重新生成 16 cout<<question; 17 //DisplayWord(ans[i]); 18 cout<<endl; 19 }
拓展功能基本完成,但我认为此次在结构上优化还不够好,在生成算式的逻辑上应该还有更为严谨的做法,以后仍需改善。