zoukankan      html  css  js  c++  java
  • 结对项目:一个自动生成小学四则运算题目的命令行程序(c++)

    结对编程

    这个作业属于哪个课程 软件工程
    这个作业要求在哪里 作业要求
    这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序,以及对给定的题目文件、答案文件有统计对错功能。
    成员 3118005386 吴永力 、3118005382 王舜鑫

    作业Github地址

    • GitHub链接
    • 可运行PrimaryArithMetic.exe已放在Release文件夹上

    PSP表格

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

    效能分析

    在-n 10000 -r 100 5 100 前提下进行测试

    CPU时间占比:

    函数调用占比:

    可以看到主要时间都集中在计算式子计算过程不会出现负数,除以0以及生成的式子等价的函数上,甚至大量的string操作符上。

    一开始用生成的式子与之前生成的式子进行等价判断,导致了时间复杂度有O(n!),程序会卡很久才可以生成,后续优化了算法就可以快速生成。(当答案相等时候才进行等价判断)

    设计实现过程

    1. 通用数类:观察题目发现要表示分数以及所有整数也可以用分数来表示,即实现一个通用的类Frac就可以实现全部数一起用

      class Frac
      {
      public:
      	Frac();
      	Frac(long long molecule, long long denominator);
      	Frac(std::string ex);
      	~Frac();
      
      	Frac operator+(const Frac& right) const;
      	Frac operator-(const Frac& right) const;
      	Frac operator*(const Frac& right) const;
      	Frac operator/(const Frac& right) const;
      
      	bool operator==(const Frac& right) const;
      	bool operator!=(const Frac& right) const;
      
      	//化简
      	void Simplify();
      	//最大公因数
      	long long MaxDiviSor( long long a,  long long b);
      	//转换为字符串
      	std::string to_string() const;
      private:
      
      	long long m_Up;					//分子
      	long long m_Down;				//分母
      };
      
    2. 题目生成

      1. 随机生成一定数量的操作符(+ - x ÷),数量范围在1~3;

      2. 根据范围参数生成以及操作符数量生成随机指定数量的数(分成整数和真分数)

      3. 根据操作符数量来生成括号

        • 操作符为1个:即不产生括号
        • 操作符数量为2个:若生成的括号数量为1个时候,根据随机位置来决定括号的位置(如:(1+2)+3以及1+(2+3) );若为0个,即不产生括号
        • 操作符数量为3个:若生成的括号数量为2个时候,括号直接放在两侧(如:(1+2)+(3+4) );若为一个时候,即根据随机数来决定放在右侧还是左侧;若为0个,也是不产生括号。
      4. 判断式子是否有计算过程有负数以及除以0的情况;若存在,则重复上述步骤。

        --根据式子的后缀表达式生成子表达式,再根据子表达式计算判断是否有计算过程有负数以及除以0

      5. 判断式子是否与之前生成的式子存在等价的情况;若存在,则重复上述步骤。

        --根据式子的后缀表达式生成子表达式,再根据子表达式来进行判断式子是否存在等价

      6. 若生成成功,即可以生成答案并把题目和答案存起来。

    3. 计算结果

      1. 首先把式子规整化,如空格剔除,以及把 ÷转换为# (因为单字符匹配不了÷)以及补零(如:(-2+3) -> (0-2+3) ; (1+(-2*3))-> (1+(0-2*3)))
      2. 然后把中缀表达式转换为后缀表达式,若数值为真分数即把真分数直接当作数字存起来
      3. 最后根据后缀表达式来计算结果

    关键函数

    void CreateAriTitlesAndAnswer(int titleNums = 10, long long naturlrange = 10, long long fracrange = 5, long long downrange = 10);
    
    1. 参数:题目数量,自然数范围,真分数范围和分母范围
    2. 具体功能:生成题目
    bool Cmp_ExChild(std::vector<std::string> ex1, std::vector<std::string>ex2);
    
    1. 参数:子表达式1的数组和子表达式2的数组(数组索引:0:操作数1 ;1:操作数2;2:运算符)
    2. 返回值:bool
    3. 具体功能:判断两个子表达式是否等价
    bool IsSimilary(std::string scource,std::string destination);
    
    1. 参数:原表达式和要比较的目的表达式
    2. 返回值:bool
    3. 具体功能:判断两个表达式是否等价
    std::string SuppleInfix(std::string infix);
    
    1. 参数:中缀表达式
    2. 返回值:字符串
    3. 具体功能:规整化中缀表达式
    bool ReversePolish(std::string infix);
    
    1. 参数:中缀表达式
    2. 返回值:bool(表示是否转换成功)
    3. 具体功能:生成后缀表达式并存起来
    std::string GetResult(std::string infix);
    
    1. 参数:中缀表达式
    2. 返回值:计算结果的字符串
    3. 具体功能:输入中缀表达式即可得到计算结果
    std::vector<std::vector<std::string>> GetChildExpression(std::stack<std::string> reversePolish);
    
    1. 参数:后缀表达式
    2. 返回值:子表达式的二维数组
    3. 具体功能:根据后缀表达式得到一个子表达式的二维数组

    关键代码展示

    1. 生成题目部分

      void PriAriCreator::CreateAriTitlesAndAnswer(int titleNums, long long naturlrange, long long fracrange, long long downrange)
      {
      	for (int i = 0; i < titleNums;)
      	{
      		int opNum = random_Int(1, mOpNumMax);
      		std::vector<Frac> numbers;
      		for (int i = 0; i <= opNum; ++i)
      		{
      			//产生随机数(整数还是分数)
      			int isCreate = random_Int(0, 1);
      			if (isCreate == 0)
      			{
      				long long num_R = random_LL(1, naturlrange);
      				Frac frac(num_R, 1);
      				numbers.push_back(frac);
      			}
      			else if (isCreate == 1)
      			{
      				long long num_2 = random_LL(1, downrange);
      				long long num_1 = random_LL(1, fracrange * num_2);
      				Frac frac(num_1, num_2);
      				numbers.push_back(frac);
      			}
      		}
      		std::vector<std::string> ops;
      		//随机生成符号
      		for (int i = 0; i < opNum; ++i)
      		{
      			std::string op = random_OP();
      			ops.push_back(op);
      		}
      
      		int bracketNum = random_Int(0, opNum - 1);
      		
      		std::string infix = "";
      		if (opNum == 1)
      		{
      			infix += numbers[0].to_string() + ops[0] + numbers[1].to_string();
      		}
      		else if (opNum == 2)
      		{
      			if (bracketNum == 1)
      			{
      				int position = random_Int(0, 1);
      				if (position == 0)
      				{
      					infix += "(" + numbers[0].to_string()  + ops[0]  + numbers[1].to_string() + ")";
      					infix += ops[1] + numbers[1].to_string();
      				}
      				else
      				{
      					infix += numbers[0].to_string() + ops[0];
      					infix += "(" + numbers[1].to_string() + ops[1] + numbers[2].to_string() + ")";
      				}
      			}
      			else if(bracketNum==0)
      			{
      				infix += numbers[0].to_string() + ops[0] + numbers[1].to_string() + ops[1] + numbers[2].to_string();
      			}
      		}
      		else if (opNum == 3)
      		{
      			if (bracketNum == 0)
      			{
      				infix += numbers[0].to_string() + ops[0] + numbers[1].to_string() + ops[1] + numbers[2].to_string()+ops[2]+numbers[3].to_string();
      			}
      			else if (bracketNum == 1)
      			{
      				int position = random_Int(0, 2);
      				if (position == 0)
      				{
      					infix += "(" + numbers[0].to_string() + ops[0] + numbers[1].to_string() + ")";
      					infix += ops[1] + numbers[1].to_string() + numbers[2].to_string() + ops[2] + numbers[3].to_string();
      				}
      				else if (position == 1)
      				{
      					infix += numbers[0].to_string() + ops[0];
      					infix += "(" + numbers[1].to_string() + ops[1] + numbers[2].to_string() + ")";
      					infix += ops[2] + numbers[3].to_string();
      				}
      				else if (position ==  2)
      				{
      					infix += numbers[0].to_string() + ops[0] + numbers[1].to_string();
      					infix += ops[1] + "(" + numbers[2].to_string() + ops[2] + numbers[3].to_string() + ")";
      				}
      			}
      			else if (bracketNum == 2)
      			{
      				infix += "(" + numbers[0].to_string() + ops[0] + numbers[1].to_string() + ")";
      				infix += ops[1] + "(" + numbers[2].to_string() + ops[2] + numbers[3].to_string() + ")";
      			}
      		}
      		
      
      
      		if (IsCreaateNegative(infix) || IsDownZero(infix))			//过程不能有负数以及除0
      		{
      			continue;
      		}
      		std::string answer = mCalculator.GetResult(infix);
      		bool flag = false;
      		for (size_t i = 0; i < mAnswers.size(); ++i)
      		{
      			if (mAnswers[i]._Equal(answer) && IsSimilary(infix, mInfixs[i]))
      			{
      				flag = true;
      				break;
      			}
      		}
      
      		if (!flag)
      		{
      			mInfixs.push_back(infix);
      			mAnswers.push_back(answer);
      			++i;
      		}
      	}
      	SaveTitleToFile();
      	SaveAnswerToFile();
      }
      
    2. 后缀计算式子结果

      std::string Calculator::CalcuReversePolish()
      {
      	if (number.empty())
      	{
      		return "";
      	}
      	std::stack<Frac>* temp = new std::stack<Frac>;
      	std::string str;
      	while (!number.empty())
      	{
      		str = number.top();
      		number.pop();
      		if (!IsOp(str))
      		{
      			Frac frac(str);
      			temp->push(frac);
      		}
      		else
      		{
      			if (temp->size() > 1)
      			{
      				Frac first, second;
      				first = temp->top();
      				temp->pop();
      				second = temp->top();
      				temp->pop();
      
      				char c_op = str.at(0);
      
      				switch (c_op)
      				{
      				case '+':
      					second = second + first;
      					temp->push(second);
      					break;
      				case '-':
      					second = second - first;
      					temp->push(second);
      					break;
      				case '*':
      					second = second * first;
      					temp->push(second);
      					break;
      				case '#':
      					if (first == Frac(0, 1))
      					{
      						return std::string("X");   //	X代表无结果
      					}
      					second = second / first;
      					temp->push(second);
      					break;
      				default:
      					break;
      				}
      			}
      		}
      	}
      	Frac result = temp->top();
      	temp->pop();
      	delete temp;
      	return result.to_string();
      }
      
    3. 判断式子相似

      bool PriAriCreator::IsSimilary(std::string scource, std::string destination)
      {
      	//获取后缀表达式
      	std::stack<std::string> RP1 = mCalculator.GetReversePolish(scource);
      	std::stack<std::string> RP2 = mCalculator.GetReversePolish(destination);
      
      	//得到子表达式
      	std::vector<std::vector<std::string>> ex1 = mCalculator.GetChildExpression(RP1);
      	std::vector<std::vector<std::string>> ex2 = mCalculator.GetChildExpression(RP2);
      
      	for (auto& p : ex1)
      	{
      		for (auto& v : ex2)
      		{
      			if (Cmp_ExChild(p, v))
      			{
      				return true;
      			}
      		}
      	}
      	return false;
      }
      

    效果展示

    1. 题目生成

      在 -n 10000 -r 50 5 10情况下

      部分题目:

      1: 1 * 10 = 
      2: 19 ÷ 5 = 
      3: 40 * 3'3/4 = 
      4: 21 ÷ 22 = 
      5: 28 * 17 = 
      6: 5 ÷ 1/3 = 
      7: 5 + 3'3/10 + 4 ÷ 4 = 
      8: 38 + 2'3/8 = 
      9: 4'1/9 + 27 = 
      10: 46 * 3 ÷ 30 * 2'2/3 = 
      11: (4'2/7 ÷ 3) + (21 ÷ 11) = 
      12: (2 ÷ 14) + (21 + 4'1/3) = 
      

      以及答案:

      1: 10
      2: 3'4/5
      3: 150
      4: 21/22
      5: 476
      6: 15
      7: 9'3/10
      8: 40'3/8
      9: 31'1/9
      10: 12'4/15
      11: 3'26/77
      12: 25'10/21
      
    2. 验证答案

      因为全部正确,所以只贴一部分出来了:

      Correct:10000(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,.......
      

    单元测试

    单元测试:对一定情况下的题目生成和自身验证

    共有以下组:

    样例
    1 -n 10 -r 10
    2 -n 100 -r 100
    3 -n 100 -r 10
    4 -n 1000 -r 1000
    5 -n 1000 -r 10 6 10
    6 -n 1000 -r 10 6 50
    7 验证正确的文件(含错误答案)
    8 验证不存在的文件
    9 -n 10000 -r 10
    10 -n 10000 -r 100

    部分代码:

    TEST_METHOD(TestMethod9)
    {
    	PriAriCreator* Par = new PriAriCreator();
    	Par->CreateAriTitlesAndAnswer(10000, 10);
    	Assert::IsTrue(Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"));
    	if (Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"))
    	{
    		Par->VerificationAnswer();
    	}
    	delete Par;
    }
    
    TEST_METHOD(TestMethod10)
    {
    	PriAriCreator* Par = new PriAriCreator();
    	Par->CreateAriTitlesAndAnswer(10000, 100);
    	Assert::IsTrue(Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"));
    	if (Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"))
            {
    		Par->VerificationAnswer();
            }
    	delete Par;
    }
    

    测试结果:

    不得不说:c++还是挺快的

    项目小结

    分享经验,总结教训

    要仔细查看题目理解要求,不要一概而论,不然容易产生二义性导致多余重复的工作;

    在实现某些算法时候,应注意其的时间复杂度,更注意下有无更好的实现方法,所以对于算法部分仍要加强学习;以及写代码注意代码复审,可以减少一定的Debug时间。

    这是第一次合作编程,我们通过代码和注释表达我们对这次作业的想法,这是一种很好的交流方式。在开发种遇到问题,我们及时地交流反馈,认真聆听对方的意见,这让我们可以很好地解决困难。

    成败得失

    • 未实现图形界面(有想法用QT,但时间不够放弃了)
    • 还有有一定的代码耦合度,以后得继续学习写耦合度低的代码。

    结对感受

    结对感受 对彼此的闪光点或建议
    吴永力 这次结对编程,有同伙的帮助,帮助让我更好的理解了题目的要求以及更好地开发接口模块,有小伙伴(或团队)有时候可让开发效率更高。 有一定的学习能力以及题目理解能力。
    王舜鑫 这次结对编程,有个厉害的同伴的帮助,让我自身的能力有了一定的提高,也让我节省了许多查找资料的时间,对比之前,学习效率高了很多,团队的力量就是如此强大。 学习能力强,很强的责任感,很强的团队精神。

    作者:Ligo丶

    出处:https://www.cnblogs.com/Ligo-Z/

    本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。

  • 相关阅读:
    【vijos】1768 顺序对的值(特殊的技巧)
    【vijos】1789 String(组合计数+奇怪的题)
    【vijos】1790 拓扑编号(拓扑+贪心)
    【vijos】1629 八(容斥原理+dfs)
    【vijos】1543 极值问题(数论+fib数)
    【vijos】1447 开关灯泡(高精度+特殊的技巧)
    【vijos】1164 曹冲养猪(中国剩余定理)
    【vijos】1882 石阶上的砖(中位数+特殊的技巧)
    【vijos】1881 闪烁的繁星(线段树+特殊的技巧)
    【vijos】1286 座位安排(状压dp)
  • 原文地址:https://www.cnblogs.com/Ligo-Z/p/13805994.html
Copyright © 2011-2022 走看看