作业要求:https://edu.cnblogs.com/campus/nenu/2018fall/homework/2148
作业地址:https://git.coding.net/KamiForever/FourOperations.git
具体代码为其中的f4.cpp文件
要求一:
1.重点和难点
(1).加减法和乘除法先后的实现,最初的实现方法是想要先去计算所有的乘除法再去计算加减法,但是由于是对与每一个数和运算符号都是用数组存储,计算完之后的合并问题就比较复杂,于是我们这里套用了一个想法就是用两遍遍历,第一遍计算所有乘除法,第二遍计算所有加减法。a + b * c * d = a + 0 + e * d (e = b * c) = a + 0 + 0 + f (f = e * c),这样只是把第一个乘数变成0,第二个乘数变成两个数的积,中间符号变成前一种运算符就可以用于第二遍遍历计算所有加减法而且还不会有任何问题。当然这个想法只能应用于功能一,在加入括号的时候就不再采用了。
void getans() { for(int i = 1; i < 4; i++) { if(op[i] == '*') { fra[i] = mul(fra[i - 1], fra[i]); fra[i - 1].d = 1; fra[i - 1].m = 0; op[i] = op[i - 1]; } if(op[i] == '/') { fra[i] = div(fra[i - 1], fra[i]); fra[i - 1].d = 1; fra[i - 1].m = 0; op[i] = op[i - 1]; } } for(int i = 1; i < 4; i++) { if(op[i] == '+') fra[i] = add(fra[i - 1], fra[i]); else fra[i] = sub(fra[i - 1], fra[i]); } getgcd(&fra[3].m, &fra[3].d); return; }
注:上面代码中fra为分数结构体,m表示分子,d为分母,op记录运算符,其中结构体的加减乘除都是经过运算符重载。
运算符重载举例:
struct Fraction { int m; //分子 Molecule int d; //分母 Denominator bool operator == (const Fraction &rhs) const { if(m == rhs.m && d == rhs.d) return true; else return false; } Fraction& operator + (Fraction &rhs) { m = rhs.d * m + rhs.m * d; d = rhs.d * d; return *this; } };
我觉得各种意义上这个功能四反而是最好去实现的。
(2).运算式的生成,运算式生成本身不是非常难得事情,但是由于有了括号和功能三中不能出现重复的,所以变得比较复杂。首先我们把所有的数都用分数去表示,记录每一个数的分子和分母(功能四)。在生成括号方面我用递归的方式,每次都从一个最左边没有被括号的开始,递归生成括号,并且将括号进行编号方便之后的计算结果。在生成括号中,我们会删去一些没有用的括号类似于a+(b+c)+d,这样去避免重复,而且由于还有样例中避免分配律的问题,例如:1*2+3*2和2*(1+3)+0。经过我们的认识,我们认为四个数的运算中出现这种分配律的问题,关键原因出现在这个0上面,于是我们在生成运算式的时候会避免0左右都是加减号且0在最左和最右情况,这样虽然删去了很多种问题的可能,出题不可能那么全面,而且加减0本身也没有变化所以就这样去处理了,我觉得反而增加了出题的难度。在生成运算式的时候注意每一个数的分母都不能为0。
for(int i = 1; i < maxn; i++) { op[i] = rand() % 4; if(op[i] == 0) { op[i] = '+'; pre.op[0]++; } else if(op[i] == 1) { op[i] = '-'; pre.op[1]++; } else if(op[i] == 2) { op[i] = '*'; pre.op[2]++; } else { op[i] = '/'; pre.op[3]++; } } op[0] = '+'; op[maxn] = '+'; for(int i = 0; i < maxn; i++) { fra[i].m = rand() % 10; while(op[i] == '/' && fra[i].m == 0) fra[i].m = rand() % 10; fra[i].d = rand() % 10; while(fra[i].d == 0) fra[i].d = rand() % 10; while(fra[i].m == 0 && (op[i] == '+' || op[i] == '-') && (op[i + 1] == '+' || op[i + 1] == '-')) fra[i].m = rand() % 10; getgcd(&fra[i].m, &fra[i].d); pre.f[i].m = fra[i].m; pre.f[i].d = fra[i].d; fra[i].r = 0; fra[i].l = 0; } bracnt = 0; getbra(0, maxn - 1);
括号的添加:
void getbra(int s, int e) { if(s == e) return; for(int i = s; i <= e; i++) { for(int j = e; j > i; j--) { if(!(i == s && j == e) && i != j) { int r = rand() % 10; if(r <= 7 && checkbra(i, j)) { fra[i].l += 1; fra[j].r += 1; bra[bracnt].l = i; bra[bracnt++].r = j; getbra(i, j); i = j + 1; break; } } } } return; }
(3).带括号算式的运算,这应该属于这次作业最难的一步了,由于其中有括号的存在,我想到的方法是用递归的思想去求解,就类似于上面添加括号的思想,因为一直是先计算括号内的内容,所以在每次运算只要碰到括号,就递归运算括号里面的,然后在把结果拿出来进行运算,这里有两种方式去运算,我一开始本来是从头到尾一直运算,如果是加减法就继续运算,如果是乘除法就把从这个数计算乘除法,一直到出现加法为止,将得到的数运用第一个数前的符号与一开始得到的书进行运算;另一种就是我在最开始所说的加减法和乘除法先后的实现,这个可以参考(1)。至于为什么选择递归,由于一开始是有想过其他的方法类似于栈,因为括号本身就是一种进栈出栈的方式,由于我是生成的题目,所以我能够直接对最大括号进行记录,并且把括号按从大到小,从前到后的方式进行记录,所以我就放弃了用栈,而且递归写起来就很有意思,比较推荐大家去写递归。关于结果问题,由于我全程用分数进行计算,而且我对除法的重载也变成了乘以除数的倒数,所以对于除以0的问题也变成乘以一个分母是0的数,但是在计算过程中并不会出现任何问题,但是这是一道错误的题目,所以在得到运算结果,如果分母是0,怎这个运算式是错的,重新编写一个运算式,这种公式生成的可能例如:2 / (1 - 1) + 1,这种情况就会即使你在获得运算式的时候控制被除数不为零但这种情况还是不好去写,只好再出现的时候重新生成比较好。
从头算到尾的方式:
Fraction getans(int s, int e, int k) { Fraction res, temp; memsetfra(&res); if(k > maxk) maxk = k; for(int i = s; i <= e; i++) { if(op[i + 1] == '*' || op[i + 1] == '/') { temp = fra[i]; int t = i; for(int j = i + 1; j <= e; j++) { if(fra[j].l != 0) { fra[j].l--; k = maxk; temp = operation(temp, getans(j, bra[k].r, k + 1), op[j]); j = bra[k].r; fra[j].r--; } else temp = operation(temp, fra[j], op[j]); if(op[j + 1] == '+' || op[j + 1] == '-' || j == e) { i = j; break; } } if(t == s) res = temp; else res = operation(res, temp, op[t]); } else { if(fra[i].l != 0) { fra[i].l--; k = maxk; temp = getans(i, bra[k].r, k + 1); int t = i; i = bra[k].r; if(op[i + 1] == '*' || op[i + 1] == '/') { for(int j = i + 1; j <= e; j++) { if(fra[j].l != 0) { fra[j].l--; k = maxk; temp = operation(temp, getans(j, bra[k].r, k + 1), op[j]); j = bra[k].r; fra[j].r--; } else temp = operation(temp, fra[j], op[j]); if(op[j + 1] == '+' || op[j + 1] == '-' || j == e) { i = j; break; } } } if(t != s) res = operation(res, temp, op[t]); else res = temp; } else { if(i != s) res = operation(res, fra[i], op[i]); else res = fra[i]; } } } return res; }
运用(1)的方法:
Fraction getans(int s, int e, int k) { if(k > maxk) maxk = k; for(int i = s; i <= e; i++) { if(fra[i].l != 0) { fra[i].l--; if(k < maxk) k = maxk; fra[bra[k].r].r--; getans(bra[k].l, bra[k].r, k + 1); i = bra[k].r; } } for(int i = s + 1; i <= e; i++) { if(op[i] == '*') { fra[i] = fra[i - 1] * fra[i]; fra[i - 1].d = 1; fra[i - 1].m = 0; op[i] = op[i - 1]; if(i == s + 1) op[i] = '+'; } if(op[i] == '/') { fra[i] = fra[i - 1] / fra[i]; fra[i - 1].d = 1; fra[i - 1].m = 0; op[i] = op[i - 1]; if(i == s + 1) op[i] = '+'; } } for(int i = s + 1; i <= e; i++) { if(op[i] == '+') fra[i] = fra[i - 1] + fra[i]; else fra[i] = fra[i - 1] - fra[i]; } for(int j = s; j < e; j++) { if(op[j] == '*' || op[j] == '/') { op[j + 1] = op[j]; fra[j].m = 1; fra[j].d = 1; if(k >= 2 && bra[k - 2].l == s) { op[j + 1] = '+'; fra[j].m = 0; } } else { op[j + 1] = op[j]; fra[j].m = 0; fra[j].d = 1; } } return fra[e]; }
注:如果仔细阅读1中的(1)就会发现我不是本来说在加括号的时候放弃了这种方法么,但是我在写博客的时候,写到这里想了一想,我为什么要放弃这种方法,我也不知道理由,写到这里又重新去用(1)的方法实现了一波,这里就不对(1)中内容进行更改。
(4)分子分母化简和假分数变成真分数
分子分母化简很简单,求分子和分母的最大公约数,然后两个数都处以这个最大公约数,就是化简后的结果。关于假分数变成真分数,用m / d获得真分数的整数部分,m % d获得真分数的分子部分,注意其中如果m为负数(d一定是正数),则m / d和m % d必定为负数,但是真分数的分子是正数,所以分子部分需要去绝对值。
化简:
//用辗转相除法求最大公因数 int gcd(int a, int b) { return (a % b == 0) ? b : gcd(b, a % b); } //将两个数除以最大公因数 void getgcd(int *a, int *b) { int t = gcd(*a, *b); *a /= t; *b /= t; return; }
变为真分数:
if(abs(ans.m) > ans.d) printf("%d %d/%d ", ans.m / ans.d, abs(ans.m % ans.d), ans.d);
(5)回答者的答案输入,由于输入的数可能是假分数,真分数,正数和小数,输入都是设置一个初始值为0的变量t,每当得到一个数字就把t先乘以10,再把t加上这个数,循环下去就会得到一个正数,然后对一些分数和小数点的判定就可以搞定。把得到的分数化简就是输入的结果。
输入部分代码:
int input() { gets(myans); m = 0; d = 0; int t = 0; int flag = 0; for(int i = 0; i < strlen(myans); i++) { if(myans[i] >= 48 && myans[i] <= 57) { if(flag == 3) { t = m; m = 0; flag = 0; } if(flag == 0) { m *= 10; m += myans[i] - 48; } else if(flag != 3) { d *= 10; d += myans[i] - 48; if(flag == 2) t++; } } else if(myans[i] == '/') flag = 1; else if(myans[i] == '.') flag = 2; else if(myans[i] == '-') continue; else flag = 3; } if(flag == 0) d = 1; if(flag == 1) m = t * d + m; if(flag == 2) { md = (double)d; while(t--) md /= 10; md += m; } if(myans[0] == '-') { m = -m; md = -md; } if(flag != 2) getgcd(&m, &d); if(d < 0) { d = -d; m = -m; } return flag; }
(6)输入结果与正确答案对比,分数之间比较只要再化简后分子和分母都是对应相等的就行,但是小数就不一样了,而且在c++中double的浮点数没办法直接进行相等的判断,只能用eps(浮点相对误差限)来判定,也就是当两个数的差小于设定的eps的时候我们认为这两个数是相等的。
判定程序:
const double eps = 0.000001; //认为输入数据不超过小数点后六位 void check(int flag) { if(flag == 2 && (md - (double)ans.m / (double)ans.d) < eps) { // 当输入结果为小数的时候 printf("答对啦,你真是个天才! "); corcnt += 1; } else if(m == ans.m && d == ans.d) { printf("答对啦,你真是个天才! "); corcnt += 1; } else { if(ans.d == 1) printf("再想想吧,答案似乎是%d喔! ", ans.m); else printf("再想想吧,答案似乎是%d/%d喔! ", ans.m, ans.d); } return; }
(7).排除相同运算式,四个数的关于分配律地避免我在1的(2)说了,这里就不赘述,关于交换律和结合律,我们的想法是在得到相同结果的前提下,如果所用到的每种符号的数量对应相等,两个运算式的4个数都能一一对应,我们就认为这两个运算式能在交换律和结合律的变化下能够完全一样。因为如果是通过交换律和结合律变化,两个算式四个数一定是一一对应的,而且运算符使用也是相同的,结果也是一致的,在这3个条件的限制下才会有这种变化,为此我们只要在每次生成运算式的时候对运算式进行记录,并且每次都与结果相同的运算式进行比较,当所有数都能找到相同且所有运算符都是一致的,我们就把当前生成的运算式排除掉,生成新的。
判定方法:
struct Formula { //用于记录运算式 Fraction f[maxn]; int op[maxn]; Fraction ans; }For[1000], pre; bool checkFor(int t) { //t为当前一共有t组计算式 for(int i = 0; i < t; i++) { if(pre.ans == For[i].ans) { //在结果相同的情况下 int tcnt = 0; for(int j = 0; j < maxn; j++) { //判定所用运算符是否一样 if(pre.op[j] == For[i].op[j]) tcnt++; } if(tcnt != maxn) continue; tcnt = 0; for(int j = 0; j < maxn; j++) { //对每个数进行判定 for(int p = 0; p < maxn; p++) { if(pre.f[j] == For[i].f[p]) { tcnt++; break; } } } if(tcnt == maxn) return false; } } for(int i = 0; i < maxn; i++) { For[t].op[i] = pre.op[i]; For[t].f[i] = pre.f[i]; } For[t].ans = pre.ans; return true; }
(8)输入到txt中,题目中是要打印出来,但是txt也可以用word打开(题中原话),于是我们就用重定向的方式将结果输入到out.txt中,用freopen()函数去将输出重定向。
char buf[200]; char out[200]; getcwd(buf, sizeof(buf)); //获取本地文件直接地址 strcat(buf, "\"); strcpy(out, buf); strcat(out, "out.txt"); freopen(out, "w", stdout); // freopen重定向使输出到out.txt中
2关于结对编程的体会,我们先定一种代码规范,一边双方都看得舒服,在编程中一个人敲打代码,另一个人也去看是否与思路相符合,这样便于发现编写程序中间的错误,也可以及时发现程序中的错误。结对编程可以使人思路更加开阔,两个人去思考方法确实会比一个人更加轻松,也更快的能够想到更好的方法。在结对编程的过程中锻炼了两个人的编程能力,也改进了编写代码中各种不好的习惯,我一开始觉得独自能完成的东西不太需要两个,但实际上两个人是另一种锻炼,也能加快作业进展和编写过程的正确率是一个非常好的方法。
3.在结对编程中的争论,复审和收获
(1)第一个争论地方实在对于乘除法和加减法的地方,由于考虑到后续的加括号处理,结对者提出了我在1中(1)的方法,但我一开始认为不妥,我其实一开始是想要用递归加从头到尾运算去想括号问题,由于我一时不太好想和迫于时间问题,我才用了他的方法,并且对其中的一些地方进行了改进最终得到了1中的(1)成型版,这个思路确实是我一开始没有想到的,虽然先算乘除后算加减是小学就教过的,我在做题的时候我没有往这个方向是想要一次性把所有的乘除都算完在去计算加减,这也是我所有的新的思路,也是如果只是我在想无法快速得到结论的地方。
(2)第二个争论的地方是关于如何排除相同运算式的地方,这里想了很多的方法,甚至有的地方还用具体的数学思路去进行证明,但实际上在争论的过程中各自找出了对方的错误,这个最终版本也是双方商量出来的结果,最开始我对分配律想要真的去把所有符合分配的运算式给进行变化,但对于匹配和变化上过于复杂,不得以去想别的办法,但再商讨过程中还是统一了结论。
(3)第三个争论的地方在对于输入的小数地方,其实我一开始并不想要输入小数,因为虽然功能一给出了输入小数,但是由于在判断上1/3 != 0.33333333333333333333333333333333,所以还是用分数去输入更好,更便于判断,而且要求中也没说需要小数输入,但是他给我的认识就是说功能一既然有这个样例我们就要搞。这种费力不讨好的工作我一开始其实是拒绝的,但迫于对于作业完成度的压力和在完善工作上的想法,还是把这个小数输入给制作了。
(4)复审点在于括号的处理,我们一开始的想法有从头到尾进行括号的加入,并且用栈去控制,还有就是在递归的思路上有些问题,一开始会生成类似于(a+(b+c)+d)的运算式,这个运算式本意上是想要让a到c运算一次,b到d运算一次,但这种其实违反了关于括号的规定,于是我们在已经写好的括号处理上进行了改进,并且得到了现有的括号处理方法。
(5)收获之处在于思路上,当一个人去想问题,思路总会比较狭隘,会在自己得知市面上转圈,但有别人的思路加进来的时候就会有成倍的效应,会让复杂的问题很快就有简单的方法被想出来,这是我这次结对编程中最大的收获之处。
二.结对编程
工作地点:东北师范大学冬华公寓B519(我们两个一个寝室)。