结对项目 实现自动生成四则运算题目的程序 (C++)

本次作业由 陈余 与 郭奕材 结对完成

零、github地址:

https://github.com/King-Authur/-Automatically-generate-four-arithmetic-problems


一、项目的相关要求

实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)。
在这里插入图片描述
项目需求

  1. 使用 -n 参数控制生成题目的个数,例如

Myapp.exe -n 10

将生成10个题目。

  1. 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如

Myapp.exe -r 10

将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。

  1. 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。

  2. 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。

  3. 每道题目中出现的运算符个数不超过3个。

  4. 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。
    例如:
    23 + 45 = 和45 + 23 = 是重复的题目
    6 × 8 = 和8 × 6 = 也是重复的题目。
    3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。
    但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。

生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
1.四则运算题目1
2.四则运算题目2
……

其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。

  1. 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:

1.答案1
2.答案2

特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。

  1. 程序应能支持一万道题目的生成。

  2. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
    Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt

统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)

其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。


二、遇到的困难和解决方法

(一)如何进行结对编程
在项目开始前我们进行了时长为一个多小时的讨论,理清了思路并对项目进行了整体设计和规划,随后我们分配好各自要负责的模块,并写好了接口文件进行项目的开发。
在编程的过程中,我们遇到有疑惑或者觉得思路上有出入的地方也会在微信上及时进行沟通,保证了项目的正常推进。
完成代码编写之后,我们先各自检查了自己的代码、测试模块,随后将代码整合起来,发送给对方让对方进行代码复审。在实际操作中,我们都为对方发现了许多的bug。

(二)如何存储数据
为了能够较为简易的实现对分数的存储和计算等操作,我们经过讨论,决定选择用结构体来存储,并对结构体的部分运算符进行重载。


三、关键代码 和 设计说明

整体设计
在这里插入图片描述

数据的定义

typedef struct variable
{
	int num_or_Symbol;		 //0是数字1是符号
	int Symbol = -1;		//    + - * % ( ) 分别表示为 0 1 2 3 4 5
	int numer;				//如果是数字此为分子
	int Den = 1;			//如果是数字此为分母
	int num;                        //如果是数字此为分数前的系数
	bool operator == (variable c){
		return num_or_Symbol == c.num_or_Symbol && Symbol == c.Symbol && numer == c.numer && Den == c.Den && num == c.num;
	}
}var;

主要的几个部分如下

//生成表达式函数
Status Create(var** exp, int size, int *length);
//计算表达式函数
Status Calculation(var* exp, int size, var* result, int length);
//中缀表达式转后缀表达式
Status Infix_to_Postfix(var* p, int size, var* Postfix, int length, int& postLen);
//判断两个问题是否等价
Status is_question_same(var* Question, int lenQuest, var* newQuestion, int lenNewQuest, int size);
//m指令的执行
void M_instructions(var** expression, int amount, int size, var* result);
//判断对错
void Correction(int* save, char* answerfile, char* exercisefile);

关键代码
创建题目

Status Create(var** exp, int size, int* length) 
{
	var* expre;
	int mark_num = random(1, 4);//计算符个数
	int pre = 0;//前括号在第pre个数字前
	int aft = 0;//后括号在第aft个数字后
	int judge = 0;//判断,0写入数字,1写入符号
	int n = 0;
	*length = mark_num + mark_num + 1;
	n = 0;
	if (mark_num > 1)//如果运算符有3个,则存在括号
	{
		pre = random(1, mark_num);
		if(pre == 1)//不让括号括住整个式子
			aft = random((pre + 1), (mark_num + 1));
		else
			aft = random((pre + 1), (mark_num + 2));
		(*length) += 2;
		expre = new var[*length + 1];
		expre[pre * 2 - 2].num_or_Symbol = 1;
		expre[pre * 2 - 2].Symbol = 4;
		expre[aft * 2].num_or_Symbol = 1;
		expre[aft * 2].Symbol = 5;
	}
	else
	{
		expre = new var[*length + 1];
	}
	n = 0;
	while (n < *length)
	{
		if (expre[n].Symbol < 4)
		{
			if (judge == 0)
			{
				expre[n].num_or_Symbol = 0;
				expre[n].Den = random(2, size);
				expre[n].numer = random(0, expre[n].Den);
				expre[n].num = random(1, size);
				judge = 1;
			}
			else
			{
				expre[n].num_or_Symbol = 1;
				expre[n].Symbol = random(0, 4);
				judge = 0;
			}
		}
		n++;
	}
	*exp = expre;
	return SUCCESS;
}

中缀表达式转后缀表达式

Status Infix_to_Postfix(var* p, int size, var* Postfix, int length, int& postLen)
{
	//传入的postfix要记得为空
	var stack[maxn];
	int top = 0;
	for (int i = 0; i < length; i++)
	{
		if (p[i].num_or_Symbol == 0)//是数字
		{
			Postfix[postLen++] = p[i];//放入输出串中
		}
		if (p[i].num_or_Symbol == 1 && p[i].Symbol == 4)//左括号
		{
			++top;
			stack[top] = p[i];
		}
		while (p[i].num_or_Symbol == 1 && p[i].Symbol != 4 && p[i].Symbol != 5)
		{
			if (top == 0 || stack[top].Symbol == 4 || prio(p[i]) > prio(stack[top]))
			{
				++top;
				stack[top] = p[i];
				break;
			}
			else
			{
				Postfix[postLen++] = stack[top];
				top--;
			}
		}
		if (p[i].num_or_Symbol == 1 && p[i].Symbol == 5)//右括号
		{
			while (stack[top].Symbol != 4)
			{
				Postfix[postLen++] = stack[top];
				top--;
			}
			top--;
		}
	}
	while (top != 0)
	{
		Postfix[postLen++] = stack[top--];
	}
	return SUCCESS;
}

判断题目是否等价

Status is_problem_same(var* Question, int lenQuest, var* newQuestion, int lenNewQuest, int size)
{
	var Postfix1[maxn], Postfix2[maxn];
	var stack1[3][3], stack2[3][3];
	int len1 = 0, len2 = 0, sta_size1 = 0, sta_size2 = 0;

	//获取后缀表达式
	Infix_to_Postfix(Question, size, Postfix1, lenQuest , len1);
	Infix_to_Postfix(newQuestion, size, Postfix2, lenNewQuest, len2);

	//获取子表达式
	get_Subexpression(Postfix1, len1, stack1, sta_size1);
	get_Subexpression(Postfix2, len2, stack2, sta_size2);

	bool flag;
	for (int i = 0; i < sta_size1; i++)
	{
		flag = false;
		for (int j = 0; j < sta_size2; j++)
		{
			//短式等价
			if (cmp(stack1[i], stack2[j]))
			{
				flag = true;
				stack2[j][2].Symbol = -1;//将表达式的运算符删掉
				break;
			}
		}
		if (!flag)//如果存在不一样的,返回not same
		{
			return ERROR;
		}
	}
	return SUCCESS;
}

m操作

void M_instructions(var **expression, int amount, int size, var* result)
{
	fstream answer;
	answer.open(ANSWERFILE, ios::out | ios::app);
	var results[maxn];//后缀表达式
	int length;
	int i = 0;
	int j = 0;
	int k = 0;
	while (i < amount)
	{
		Create(&expression[i], size, &length);
		result[i].Symbol = length;
		if (Calculation(expression[i], size, results, length) == ERROR || results[0].num >= size || results[0].numer >= size || results[0].Den >= size)
		{
			continue;
		}
		result[i].Den = results[0].Den;
		result[i].num = results[0].num;
		result[i].numer = results[0].numer;
		result[i].num_or_Symbol = 0;
		result[i].Symbol = length;
		j = 0;
		while (j < i)
		{
			//结果一样,表达式可能一样
			if (result[j].Den == result[i].Den && result[j].numer == result[i].numer && result[j].num == result[i].num)
			{
				if (is_question_same(expression[i], result[i].Symbol, expression[j], result[j].Symbol, size))
				{
					break;
				}
			}
			j++;
		}
		if (i != j)
		{
			if(k ++ < 20)//连续20次重复答案表明给的size太小,而amount太大,表达式多样性不足
				continue;
		}
		Visit(expression[i], length, i + 1);

		answer << i + 1 << ".   ";
		if (result[i].numer == 0)
		{
			answer << result[i].num;
		}
		else
		{
			if (result[i].num != 0)
			{
				answer << result[i].num;
				answer << "`";
			}
			answer << result[i].numer;
			answer << "/";
			answer << result[i].Den;
		}
		answer << endl;
		i++;
		k = 0;
	}
	answer.close();
}

四、测试运行

随机生成10道题目
在这里插入图片描述
在这里插入图片描述

改变题目数值后随机生成10道题目
在这里插入图片描述
在这里插入图片描述

测试在exercisefile.txt文件内填入答案后判断对错
在这里插入图片描述


五、PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 20
· Estimate · 估计这个任务需要多少时间 10 10
·Development ·开发 120 100
· Analysis · 需求分析 (包括学习新技术) 60 60
· Design Spec · 生成设计文档 20 20
· Design Review · 设计复审 (和同事审核设计文档) 10 10
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 5 10
· Design · 具体设计 30 50
· Coding · 具体编码 180 200
· Code Review · 代码复审 20 15
· Test · 测试(自我测试,修改代码,提交修改) 20 20
Reporting 报告 30 30
· Test Report · 测试报告 20 15
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 30
合计 585 600

六、项目小结

郭奕材小结:
1、在本次项目中,我们通过结对编程,互相鼓励,相互监督,以更高的效率完成了本次任务,充分的认识到了结对编程的益处,理解它在编程效率与错误检查上的巨大作用。当然也了解到了它的缺点,尤其是在疫情期间沟通不便的客观环境下,沟通的难度也大大增加,但多亏网络会议的共享屏幕功能、微信电话的便捷、双方的耐心沟通和积极讨论等因素,让结对编程的缺点方面也得到了极大的补足。
2、在项目过程中,我学习了中缀表达式转后缀表达式的方法、计算后缀表达式的规则、判断两个题目是否等价的方法等知识,还向搭档学习了如何写更加规范的接口文件,收货颇丰。
3、另外图形化界面目前还在学习,只能做出简单的界面,未能完成项目扩展部分的需求,这是一个小小的遗憾,还需激励自己更快的学习和掌握知识,更好地完成项目需求。
4、最后再次感谢我的搭档陈余同学。

陈余小结:
1、这次项目,是我们第一次一起合作完成的项目,通过结对编程的编程方法,我们十分有效率地完成了项目需求途中出现了不少分歧点,但通过结对编程,我们都能及时地进行讨论和统一观点,使时间没有过多地浪费在不必要的地方
2、原本想让用户在程序里面也能填写答案,但由于想不到比较好的交互方式,所以最后没有实现这个功能,只让用户在txt文件上手动填写答案。
3、最后,和奕材带佬一起做项目真的很舒服!!


七、参考来源

[1] 波兰式、逆波兰式与表达式求值

原文地址:https://www.cnblogs.com/Authur-gyc/p/12683512.html