支持3000万道四则运算题目生成的小学生毁灭姬
链接:https://pan.baidu.com/s/1Y9wHPE-VMA7oFtxdItZhfw 提取码:txb8
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 25 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 25 |
Development | 开发 | 980 | 1180 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 90 |
· Design Spec | · 生成设计文档 | 30 | 40 |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 30 | 60 |
· Coding | · 具体编码 | 450 | 600 |
· Code Review | · 代码复审 | 180 | 180 |
· Test | · 测试(自我测试,修改代码,提交修改) | 200 | 180 |
Reporting | 报告 | 60 | 55 |
· Test Report | · 测试报告 | 20 | 15 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1070 | 1260 |
二、耗能测试
2.1 全过程耗能分析
用户在完成全套基本功能时会调用四个功能函数,分别是:
检查答案:bool check_answer(string problem,string answer)
生成题目:bool build_problem(int max_number,int problem_number)
提取字符串效信息:string getpoint_problem(string s)和string getpoint_answer(string s)
将提取字符串效信息两个函数归为一类,则它们在全过程耗能占比在不同数量级下如图:
2.2 程序中消耗最大的函数
经过全过程耗能分析,我们可以得出程序中消耗最大的函数是:bool build_problem(int max_number,int problem_number)
它在不同数量级处理中时间花费如下
三、功能展示
命令行生成10000道题目和答案
对给定的题目文件和答案文件,判定答案中的对错并进行数量统计
3.2 拓展功能展示(桌面做题程序GUI)
主页
关于我们
操作手册
题目生成做题界面
做完题显示做题情况
自定义模式供用户选择已有做题文件
四. 设计实现过程
本程序的算法实现主要由以下三个类实现
4.1
int integer; //整数部分
int numerator; //分子
int denominator; //分母
成员函数
- 构造函数
Number(); //初始化全为 0
Number(int n); //随机生成数字不大于 n 的 Number 对象
Number(int n, int max_de);//随机生成数值不大于 n,分母不大于 max_de 的 Number 对象
Number(int a, int b, int c);// a 为整数部分,b 为分子,c 为分母
Number(string str, int &i); //将字符串 str 从第 i 位开始获取一个Number对象
Number(string str); //从字符串头开始获得一个数字
- 实现运算加、减、乘、除
Number operator+(Number t);
Number operator-(Number t);
Number operator*(Number t);
Number operator/(Number t);
- 实现所有比较运算 > , >= , < , <= , == , !=
bool operator>(Number t);
bool operator>=(Number t);
bool operator<(Number t);
bool operator<=(Number t);
bool operator==(Number t);
bool operator!=(Number t);
string ToString();//把当前的Number对象转换成 string 对象,可用于输出或其他处理
bool CheckNumber();//检查当前 Number 对象是否为一个合法的数字,可用于判断其他运算返回的结果是否合法
4.2
class Point {
public:
int type;//标记当前结构体代表的意义, 0 表示符号, 1表示数字
char symbol;//运算符
Number num; //数字
Point(Number n);// 用一个 Number 对象初始化当前 Point 对象
Point(char c);//用一个符号初始化当前 Point 对象
};
Number answer
当前题目的正确计算结果,如果答案小于 0,则题目有错
bool valid 当前题目是否为一个有效的题目
成员函数:
Expression() { }; //无参构造函数,初始化一个空的对象
Expression(string str); //用题目字符串初始化,并计算出正确答案
string ToString();//将题目转换成字符串,不包括答案,答案可直接使用 Number 类的 ToString 函数转换
//以下三个函数为向当前题目末尾以下各类元素
void Add(Number n);
void Add(char c);
void Add(Expression exp);
//以下三个函数分别为,按顺序向当前对象中插入内容,前三个为题目内容,最后一个为答案
Expression(Number num, char c, Number num2, Number ans);
Expression(Number num, char c, Expression exp,Number ans);
Expression(Expression exp, char c, Number num, Number ans);
void AddParenthesis(void); //在当前题目的左右边缘添加一对括号
void clear(); //清空对象当前内容
bool CheckValid(); //判断当前题目是否合法
vector<Point> Nifix2Postfix(); //中缀转后缀,后缀表达式用vector存放
Number Calculate(); //计算当前题目的答案
4.3
成员变量:int max_number;//题目中的最大数字
int problem_number;//要求生成题目的数量
这个类实现随机生成题目的函数,分别实现生成加减乘除题目的函数,在函数中可以递归调用任意一个生成题目的函数作为本次生成题目的子部分,,根据函数接收的参数决定是否还需继续递归调用,根据另一个参数确定本次调用是否为第一次调用,如果时第一次调用则需要向用户输出内容,在返回值之前需要进行查重,如果不需要向用户输出则本次调用不需要查重,如果没有重复,则把所有题目存入到 Trie 树中。
class Make {
private:
int max_number; //最大数字
int problem_number;
bool CheckNumber();
// set<string> stringSet; //将生成的题目和所有等价的题目插入此,用于查重
// bool CheckExist(Expression exp); //检查此题目是否已生成
// void Insert(Expression exp); //将一个题目插入到 stringSet中
typedef vector<Expression> (Make::*Function_ptr)(int, int, int, int);
//函数指针数组 randMake[4] 的四个元素为随机生成题目的函数指针,方便随机调用时使用
const Function_ptr randMake[4] = {&Make::RandPlus, &Make::RandMinus, &Make::RandMul, &Make::RandDiv };
//以下四个函数为随机生成题目函数
//max_n 为题目中数字整数部分的最大值,生成的数字式子的答案不能大于 max_n
//max_de 为题目中分母的最大值
//symbol_n 为生成题目的符号数
//若symbol > 1,在函数中随机调用生成题目的函数,作为本次调用的子部份
//symbol = 1,本次为最后一次调用,直接生成两个 Number 对象的基本运算,然后返回
//output=1 表示本次调用为第一次调用,需要返回输出
//output=0 表示本次调用为被递归调用,只需返回给上一级调用使用,无需判断生成的题目是否存在 stringSet 中
//返回值中为一个题目和它的等价题目,可随机输出一个
//如果返回的vector为空,则本次生成题目失败
vector<Expression> RandPlus(int max_n, int max_de, int symbol_n, int output);
vector<Expression> RandMinus(int max_n, int max_de, int symbol_n, int output);
vector<Expression> RandMul(int max_n, int max_de, int symbol_n, int output);
vector<Expression> RandDiv(int max_n, int max_de, int symbol_n, int output);
public:
Trie trie;
Make(int n, int m);
Expression MakeProblem(void); //供给主函数调用,返回一个随机生成运算符不超过3个的正确题目
};
4.5 算法逻辑
4.6 注意
证明:
using namespace std;
typedef long long ll;
bool Check(ll a, ll b, ll c){
if(a*(b*c+b-1)>INT_MAX)return false;
if(a*(b*c+c-1)>INT_MAX)return false;
if(b*(a*c+a-1)>INT_MAX)return false;
if(b*(a*c+c-1)>INT_MAX)return false;
if(c*(b*a+b-1)>INT_MAX)return false;
if(c*(b*a+a-1)>INT_MAX)return false;
return true;
}
void FindMax(){
ll i,j,k;
int j1=2,j2=3;
int f;
for(j=5;Check(j1,j2,j)==true;j++){
f=1;
for(i=2;i*i<=j;i++){
if(j%i==0){
f=0;
break;
}
}
if(f){
cout << j << endl;
j1=j2;
j2=j;
}
}
}
经检验,当时满足条件。
五、
5.1 Trie树
字典树, 用来代替 std::set 套 std::string 来实现查重算法, std::set 套 std::string,只要将 string 存入到 set 中即可,实现简单,但时间开销非常大,而用字典树进行优化之后程序的运行速度得到了肉眼可见明显的提升。
class Trie;
class C {
public:
char c; //下一个字符
Trie *nxt; //下一个字符的 Trie 树节点指针
C *next; //兄弟节点指针
C();
C(char cc);
};
class Trie {
public:
//用链表存储儿子节点,节省空间
C *c; //存储下一个字符是什么,以及对应的 Trie 树节点
bool flag;
Trie();
bool Insert(string s);
};
5.2
六、
用了字典树优化之后程序运行速度得到了质的飞跃,但是空间复杂度过高的缺点就暴露得很明显了,只能生成数量级是十万道题目,鉴于要满足队友贪婪的欲望,给我提出要能生成一千万道题目的合理要求,无法拒绝,决定采用字符串哈希 + 平衡树 来优化查重算法。
6.1
p 为一个素数,这里取 127
mod 为一个素数,这里取 99999999999973
long long mod = 99999999999973;
long long Hash(string &s) {
long long ret=0;
int i;
int len = s.length();
for (i = 0; i < len; i++) {
ret = (ret * 127 + s[i]) % mod;
}
return ret;
}
6.2
struct AVL {
AVL *lchild, *rchild; //左右儿子指针
long long val; //存储的哈希值
int height; //树高
AVL() {};
AVL(long long n) {
val = n;
lchild = NULL;
rchild = NULL;
}
};
单词插入操作的复杂度:O(L+logN),L为字符串的长度,N为当前已生成题目的数量,字典树优化的时间复杂度是 O(L),所以速度会较慢
6.4 空间复杂度
O(N),N为生成题目数量,每一个题目只需要消耗一个结构体的内存,即24字节,而字典树最坏情况的空间复杂度是指数级的。
6.5运行测试
七、程序优化三
继 hash + AVL 优化,又开始对程序感到不知足了,经过思考,决定用 unordered_set 来代替 AVL树 存储字符串哈希值。
7.1 时间复杂度
unordered_set 是基于哈希实现的数据结构,一次插入或查询的复杂度O(1),而 AVL树一次插入或查询的复杂度是O(logN),N 是当前已生成题目的数量。经程序检验,当生成题目超过100万道时,速度快了超过 1/10
7.2 空间复杂度
7.3 程序验证
下图中两个程序同时启动,左边一个为 hash + AVL树,右边一个为 hash + unordered_set
八、单元测试
本程序使用基于xUnit架构的Google单元测试框架Gtest进行单元测试
使用局部的事件机制以针对一个个测试套件(详细测试代码见GitHub)
#include <iostream>
#include <gtest/gtest.h>
using namespace std;
class MyTestSuite0 : public testing::Test
{
protected:
static void SetUpTestSuite()
{
cout << "TestSuite event0 : start" << endl;
}
static void TearDownTestSuite()
{
cout << "TestSuite event0 : end" << endl;
}
};
class MyTestSuite1 : public testing::Test
{
protected:
static void SetUpTestSuite()
{
cout << "TestSuite event1 : start" << endl;
}
static void TearDownTestSuite()
{
cout << "TestSuite event1 : end" << endl;
}
};
TEST_F(MyTestSuite0, test0)
{
//测试代码
}
TEST_F(MyTestSuite1, test0)
{
//测试代码
}
TEST_F(MyTestSuite0, test1)
{
//测试代码
}
TEST_F(MyTestSuite1, test1)
{
//测试代码;
}
int main(int argc, char *argv[])
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
其中一个运行片段
九、项目小结C++
据实验,C++的运行速度比java快2.1%,足足是python的50倍,这正是我们选择用C++完成本次项目的原因。健昆同学平时有做ACM相关的训练,对数据结构有较深的理解,为本次项目底层运算的实现提供了很好的支持。我(纪鹏)学习过QT界面的开发和GTest测试框架,因而可以在完成用户层搭建的同时帮忙检测健昆同学写的程序是否存在bug,帮助他及时地发现错误。
我们也存在着许多不足之处,例如:因两人用的IDE不同所造成的字符编码方式不同引发的错误,足足花了一天上午才得以解决;在家沟通不方便,有时需要一个人完成相应功能另外一方才能得以推进,影响了生产效率。
结对共同感受:代码复审次数明显比个人编程多,能够有效减少bug产生,同时也能学习到对方独到的思路和技术。
纪鹏闪光点:有责任心,学习能力强 。
健昆闪光点:C++基础扎实,思维清晰。