实现一个自动生成小学四则运算题目的命令行程序

这个作业属于哪个课程 软工2班
这个作业要求在哪里 结对项目
这个作业的目标 实现一个能够自动生成四则运算题目并计算答案的系统。

黄娟英(学号3223001778):github

袁卓妍(学号3223004819):github

PSP表格

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

效能分析

改进程序性能花费时间:1.5h

改进思路:

·对表达式树的生成过程进行了调整,减少因特殊运算符判断(如减法、除法)的重试次数;
·在标准化表达式判断时使用更高效的数据结构(例如 set)避免重复生成;
·在 Fraction 类中的约分算法上,采用更高效的递归实现,并确保在每次运算时不产生不必要的临时对象。

性能分析图:

程序中消耗最大的函数:mainCRTStartup

设计实验过程

1. 整体结构概述

整个程序分为两大主要模块:

· 题目生成模块
负责生成符合要求的四则运算题目及其答案,保存到 Exercises.txt 和 Answers.txt 文件中。
· 判题模块
负责读取已有的题目和答案文件,对比正确答案和学生答案,输出批改结果到 Grade.txt 文件。
· 此外,还包括辅助模块,用于处理分数运算、表达式解析、表达式树操作、文件输入输出以及命令行参数解析。

2. 主要数据结构与类的设计

2.1 Fraction(分数类)

·成员函数
·构造函数:支持默认构造和带参数构造
·normalize():对分数进行约分和符号标准化
·四则运算重载:operator+, operator-, operator*, operator/
·比较运算符:operator< 和 operator==
·toString():输出格式化字符串,支持混合分数格式(例如 2'3/8)

说明:Fraction 类封装了分数的基本运算和格式化,整个程序在运算过程中均使用 Fraction 对象进行运算和存储结果。

关系:为整个程序提供数值计算基础,Expression 节点的值、evaluate 函数和算数运算均依赖 Fraction 类。

2.2 Expression(表达式树节点结构)

构造函数
数字节点:传入 Fraction 对象
运算节点:传入运算符以及左右子树指针
说明:Expression 结构构成了一个二叉树,用于表达四则运算题目的语法树,后续通过递归遍历来计算值、生成字符串和进行标准化表示。

3. 主要函数及模块功能

3.1 辅助函数myGCD(a, b)

计算最大公约数,用于 Fraction 类中的约分。

3.2 Fraction 相关函数normalize()

保证分数格式统一、分母正、并约分。
运算符重载
对分数的加、减、乘、除进行定义,保证运算正确性。

3.3 表达式树相关函数

deleteExpr(Expression expr)*
递归释放表达式树的内存。

evaluate(Expression expr)*
递归计算表达式树的值,返回 Fraction 结果。

exprToString(Expression expr)*
递归生成表达式的字符串表示,确保运算符和括号之间有空格,以便于输出和后续判题时解析。

canonical(Expression expr)*
生成表达式的标准化字符串,用于判断题目是否重复。

说明:对加法和乘法操作进行左右子树排序,保证等价表达式标准化后结果一致。

3.4 随机题目生成模块

generateNumberValue(int range)
根据范围生成自然数或真分数(包括混合分数)。

generateExpression(int opsLeft, int range)
递归生成表达式树:
· 当运算符数为 0 时返回数字节点。
· 随机选择运算符,并随机分配左右子树的运算符个数。
· 针对减法和除法作特殊判断(如确保不产生负数和除法结果为真分数)。

generateExercises(int num, int range)
负责生成指定数量的题目:
· 使用 generateExpression 生成表达式树。
· 使用 canonical 判断题目唯一性。
· 调用 exprToString 输出题目,调用 evaluate 计算答案。
· 将题目和答案分别写入 Exercises.txt 和 Answers.txt。

3.5 解析与判题模块

tokenize(const string &line)
将一行表达式按空格分割成各个 token。

parseFraction(const string &token)
解析字符串形式的数字(支持 5, 3/4, 2'3/8 格式),返回 Fraction 对象。parseFactor, parseTerm, parseExpression
构成一个递归下降解析器,用于解析生成的题目字符串(从文件中读取),并重建表达式树。

gradeExercises(const string &exFileName, const string &ansFileName)
读取题目文件和答案文件,使用解析器重建题目表达式计算正确答案,比较学生答案,统计正确和错误的题号,并输出到 Grade.txt。

3.6 命令行参数处理与帮助信息

printHelp()
输出程序使用方法的帮助信息。

main()
入口函数,解析命令行参数,根据参数选择生成题目或判题功能,并调用相应函数。

代码说明

#include <sstream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <set>
#include <algorithm>
#include <cmath>
using namespace std;

// --------------------- 辅助函数 ---------------------
int myGCD(int a, int b) {
    a = abs(a); b = abs(b);
    return b == 0 ? a : myGCD(b, a % b);
}

// --------------------- 分数类 ---------------------
struct Fraction {
    int num; // 分子
    int den; // 分母,始终大于0

    Fraction(int n = 0, int d = 1) : num(n), den(d) {
        if (d == 0) {
            throw "Denominator cannot be zero";
        }
        normalize();
    }

    void normalize() {
        if (den < 0) { num = -num; den = -den; }
        int g = myGCD(num, den);
        if (g == 0) g = 1;
        num /= g;
        den /= g;
    }

    Fraction operator+(const Fraction& other) const {
        return Fraction(num * other.den + other.num * den, den * other.den);
    }

    Fraction operator-(const Fraction& other) const {
        return Fraction(num * other.den - other.num * den, den * other.den);
    }

    Fraction operator*(const Fraction& other) const {
        return Fraction(num * other.num, den * other.den);
    }

    Fraction operator/(const Fraction& other) const {
        // 假设 other 不为 0
        return Fraction(num * other.den, den * other.num);
    }

    bool operator<(const Fraction& other) const {
        return num * other.den < other.num * den;
    }

    bool operator==(const Fraction& other) const {
        return num == other.num && den == other.den;
    }

    // 输出格式:当分子大于分母时转换为带整数部分的混合分数,否则 a/b 格式
    string toString() const {
        if (num < 0) { // 保证非负(本题生成均为非负结果)
            return "-" + Fraction(-num, den).toString();
        }
        if (num >= den) {
            int whole = num / den;
            int remainder = num % den;
            if (remainder == 0)
                return to_string(whole);
            else
                return to_string(whole) + "\'" + to_string(remainder) + "/" + to_string(den);
        }
        else {
            return to_string(num) + "/" + to_string(den);
        }
    }
};

// --------------------- 表达式树 ---------------------
enum ExprType { NUMBER, OPERATOR };

struct Expression {
    ExprType type;
    Fraction value;   // 当 type==NUMBER 时有效
    char op;          // 当 type==OPERATOR 时有效
    Expression* left;
    Expression* right;

    // 构造数字节点
    Expression(const Fraction& val) : type(NUMBER), value(val), left(nullptr), right(nullptr) { }

    // 构造运算节点
    Expression(char opr, Expression* l, Expression* r) : type(OPERATOR), op(opr), left(l), right(r) { }
};

// 递归释放表达式树
void deleteExpr(Expression* expr) {
    if (expr == nullptr) return;
    if (expr->type == OPERATOR) {
        deleteExpr(expr->left);
        deleteExpr(expr->right);
    }
    delete expr;
}

// 计算表达式值
Fraction evaluate(Expression* expr) {
    if (expr->type == NUMBER)
        return expr->value;
    Fraction l = evaluate(expr->left);
    Fraction r = evaluate(expr->right);
    switch (expr->op) {
    case '+': return l + r;
    case '-': return l - r;
    case '*': return l * r;
    case '/': return l / r;
    }
    return Fraction(0, 1);
}

// 输出表达式字符串(每个运算符和括号间用空格分隔)
string exprToString(Expression* expr) {
    if (expr->type == NUMBER)
        return expr->value.toString();
    // 始终加括号以明确运算顺序
    return "( " + exprToString(expr->left) + " " + string(1, expr->op) + " " + exprToString(expr->right) + " )";
}

// 计算表达式的标准化表示,用于唯一性判断(对 + 和 * 交换律排序)
string canonical(Expression* expr) {
    if (expr->type == NUMBER)
        return expr->value.toString();
    string leftStr = canonical(expr->left);
    string rightStr = canonical(expr->right);
    if (expr->op == '+' || expr->op == '*') {
        if (rightStr < leftStr)
            swap(leftStr, rightStr);
    }
    return "(" + leftStr + " " + expr->op + " " + rightStr + ")";
}

// --------------------- 随机题目生成 ---------------------
// 随机生成一个自然数或真分数(生成的数值均在给定范围内)
// 参数 range 表示取值范围(例如 10 表示生成 10 以内的自然数,以及分数中的分子、整数部分均在此范围内)
Fraction generateNumberValue(int range) {
    bool useFraction = (rand() % 2 == 1);
    if (!useFraction) {
        int num = rand() % range;
        return Fraction(num, 1);
    }
    else {
        bool isMixed = (rand() % 2 == 1);
        if (!isMixed) {
            int denominator = rand() % (range - 1) + 2; // [2, range]
            int numerator = rand() % (denominator - 1) + 1; // 1 ~ denominator-1
            return Fraction(numerator, denominator);
        }
        else {
            int whole = rand() % range; // 混合分数整数部分
            int denominator = rand() % (range - 1) + 2;
            int numerator = rand() % (denominator - 1) + 1;
            return Fraction(whole * denominator + numerator, denominator);
        }
    }
}

// 递归生成表达式树,opsLeft 表示还要生成的运算符个数(最多 3 个),range 为数值范围
Expression* generateExpression(int opsLeft, int range) {
    if (opsLeft == 0) {
        Fraction num = generateNumberValue(range);
        return new Expression(num);
    }
    // 随机选择运算符
    char ops[4] = { '+', '-', '*', '/' };
    char op = ops[rand() % 4];
    // 随机分配左右子树中的运算符个数,注意:总数 = opsLeft - 1
    int leftOps = rand() % (opsLeft);
    int rightOps = opsLeft - 1 - leftOps;
    Expression* leftExpr = generateExpression(leftOps, range);
    Expression* rightExpr = generateExpression(rightOps, range);

    // 对减法,确保左子表达式值不小于右子表达式值(保证不产生负数)
    if (op == '-') {
        Fraction lVal = evaluate(leftExpr);
        Fraction rVal = evaluate(rightExpr);
        if (lVal < rVal) {
            deleteExpr(leftExpr);
            deleteExpr(rightExpr);
            return generateExpression(opsLeft, range);
        }
    }
    // 对除法,确保除数不为 0 且结果为真分数(即被除数小于除数)
    if (op == '/') {
        Fraction rVal = evaluate(rightExpr);
        if (rVal.num == 0) {
            deleteExpr(leftExpr);
            deleteExpr(rightExpr);
            return generateExpression(opsLeft, range);
        }
        Fraction lVal = evaluate(leftExpr);
        // 要求商小于1:lVal < rVal
        if (!(lVal < rVal)) {
            deleteExpr(leftExpr);
            deleteExpr(rightExpr);
            return generateExpression(opsLeft, range);
        }
    }
    return new Expression(op, leftExpr, rightExpr);
}

// --------------------- 题目生成与答案输出 ---------------------
void generateExercises(int num, int range) {
    ofstream exFile("Exercises.txt");
    ofstream ansFile("Answers.txt");
    if (!exFile || !ansFile) {
        cerr << "文件打开失败" << endl;
        return;
    }

    set<string> exprSet; // 存储标准化表达式,避免重复
    int count = 0;
    int trial = 0;
    while (count < num && trial < num * 10) { // 限制重试次数
        trial++;
        int opsCount = rand() % 4; // 运算符个数 0~3
        Expression* expr = generateExpression(opsCount, range);
        string cano = canonical(expr);
        if (exprSet.find(cano) != exprSet.end()) {
            deleteExpr(expr);
            continue;
        }
        exprSet.insert(cano);
        count++;
        // 写题目:格式 "1. 题目内容 ="
        exFile << count << ". " << exprToString(expr) << " =" << endl;
        // 计算答案
        Fraction ans = evaluate(expr);
        ansFile << "(" << count << ") " << ans.toString() << endl;
        deleteExpr(expr);
    }
    exFile.close();
    ansFile.close();
    cout << "生成 " << count << " 道题目到 Exercises.txt 和 Answers.txt" << endl;
}

// --------------------- 解析表达式(用于批改答案) ---------------------
// 分词(按照空格分隔,去掉空串)
vector<string> tokenize(const string& line) {
    vector<string> tokens;
    istringstream iss(line);
    string token;
    while (iss >> token) {
        tokens.push_back(token);
    }
    return tokens;
}

// 解析数字(支持形如 5, 3/4, 2'3/8)
Fraction parseFraction(const string& token) {
    if (token.find("'") != string::npos) {
        int pos = token.find("'");
        int whole = stoi(token.substr(0, pos));
        string fracPart = token.substr(pos + 1);
        int slashPos = fracPart.find("/");
        int numerator = stoi(fracPart.substr(0, slashPos));
        int denominator = stoi(fracPart.substr(slashPos + 1));
        return Fraction(whole * denominator + numerator, denominator);
    }
    else if (token.find("/") != string::npos) {
        int slashPos = token.find("/");
        int numerator = stoi(token.substr(0, slashPos));
        int denominator = stoi(token.substr(slashPos + 1));
        return Fraction(numerator, denominator);
    }
    else {
        int num = stoi(token);
        return Fraction(num, 1);
    }
}

// 前向声明
Expression* parseExpression(vector<string>& tokens, int& pos);

Expression* parseFactor(vector<string>& tokens, int& pos) {
    if (tokens[pos] == "(") {
        pos++; // 跳过 "("
        Expression* expr = parseExpression(tokens, pos);
        if (pos < tokens.size() && tokens[pos] == ")")
            pos++; // 跳过 ")"
        return expr;
    }
    else {
        // 数字可能包含 ' 或 /
        Expression* numExpr = new Expression(parseFraction(tokens[pos]));
        pos++;
        return numExpr;
    }
}

Expression* parseTerm(vector<string>& tokens, int& pos) {
    Expression* left = parseFactor(tokens, pos);
    while (pos < tokens.size() && (tokens[pos] == "*" || tokens[pos] == "/")) {
        char op = tokens[pos][0];
        pos++;
        Expression* right = parseFactor(tokens, pos);
        left = new Expression(op, left, right);
    }
    return left;
}

Expression* parseExpression(vector<string>& tokens, int& pos) {
    Expression* left = parseTerm(tokens, pos);
    while (pos < tokens.size() && (tokens[pos] == "+" || tokens[pos] == "-")) {
        char op = tokens[pos][0];
        pos++;
        Expression* right = parseTerm(tokens, pos);
        left = new Expression(op, left, right);
    }
    return left;
}

// 解析一道题目(题目行格式: "1. ( ... ) =" ),返回表达式字符串部分
string extractExprStr(const string& line) {
    // 找到第一个"."后和"="之前的部分
    size_t dotPos = line.find('.');
    size_t eqPos = line.rfind('=');
    if (dotPos == string::npos || eqPos == string::npos)
        return "";
    // 去掉前后空格
    string expr = line.substr(dotPos + 1, eqPos - dotPos - 1);
    // trim
    while (!expr.empty() && expr.front() == ' ')
        expr.erase(expr.begin());
    while (!expr.empty() && expr.back() == ' ')
        expr.pop_back();
    return expr;
}

// --------------------- 判题功能 ---------------------
void gradeExercises(const string& exFileName, const string& ansFileName) {
    ifstream exFile(exFileName);
    ifstream stuAnsFile(ansFileName);
    if (!exFile || !stuAnsFile) {
        cerr << "打开题目或答案文件失败" << endl;
        return;
    }

    vector<string> correctAns; // 存放正确答案的字符串(标准化格式)
    vector<int> correctIdx, wrongIdx;

    string line;
    // 读题目文件,每行格式:"i. <expression> ="
    while (getline(exFile, line)) {
        if (line.empty()) continue;
        string exprStr = extractExprStr(line);
        // 分词:注意,生成时我们保证各符号之间有空格
        vector<string> tokens = tokenize(exprStr);
        int pos = 0;
        Expression* expr = parseExpression(tokens, pos);
        Fraction ans = evaluate(expr);
        correctAns.push_back(ans.toString());
        deleteExpr(expr);
    }
    exFile.close();

    // 读学生答案文件,每行格式:"(i) <answer>"
    vector<string> stuAns;
    while (getline(stuAnsFile, line)) {
        if (line.empty()) continue;
        // 找到右括号后面的答案部分
        size_t parenPos = line.find(')');
        if (parenPos == string::npos) continue;
        string answer = line.substr(parenPos + 1);
        // trim
        while (!answer.empty() && answer.front() == ' ')
            answer.erase(answer.begin());
        stuAns.push_back(answer);
    }
    stuAnsFile.close();

    int total = correctAns.size();
    for (int i = 0; i < total; i++) {
        // 简单比较字符串(要求生成和学生答案格式一致)
        if (i < stuAns.size() && stuAns[i] == correctAns[i])
            correctIdx.push_back(i + 1);
        else
            wrongIdx.push_back(i + 1);
    }

    ofstream gradeFile("Grade.txt");
    gradeFile << "Correct: " << correctIdx.size() << " (";
    for (size_t i = 0; i < correctIdx.size(); i++) {
        gradeFile << correctIdx[i];
        if (i != correctIdx.size() - 1)
            gradeFile << ", ";
    }
    gradeFile << ")" << endl;

    gradeFile << "Wrong: " << wrongIdx.size() << " (";
    for (size_t i = 0; i < wrongIdx.size(); i++) {
        gradeFile << wrongIdx[i];
        if (i != wrongIdx.size() - 1)
            gradeFile << ", ";
    }
    gradeFile << ")" << endl;
    gradeFile.close();

    cout << "批改结果已保存至 Grade.txt" << endl;
}

// --------------------- 帮助信息 ---------------------
void printHelp() {
    cout << "使用方法:" << endl;
    cout << "  生成题目: Myapp.exe -n <题目数量> -r <数值范围>" << endl;
    cout << "  判题: Myapp.exe -e <exercises file> -a <answer file>" << endl;
}

// --------------------- 主函数 ---------------------
int main(int argc, char* argv[]) {
    srand((unsigned)time(0));

    if (argc < 3) {
        printHelp();
        return 1;
    }

    string mode = argv[1];
    if (mode == "-n") {
        // 生成题目模式
        if (argc < 4) {
            printHelp();
            return 1;
        }
        int num = atoi(argv[2]);
        int range = 0;
        for (int i = 1; i < argc; i++) {
            if (string(argv[i]) == "-r" && i + 1 < argc) {
                range = atoi(argv[i + 1]);
                break;
            }
        }
        if (range <= 0) {
            cout << "必须指定有效的数值范围参数 -r" << endl;
            printHelp();
            return 1;
        }
        generateExercises(num, range);
    }
    else if (mode == "-e") {
        // 判题模式
        if (argc < 5) {
            printHelp();
            return 1;
        }
        string exFile, ansFile;
        for (int i = 1; i < argc; i++) {
            if (string(argv[i]) == "-e" && i + 1 < argc) {
                exFile = argv[i + 1];
            }
            if (string(argv[i]) == "-a" && i + 1 < argc) {
                ansFile = argv[i + 1];
            }
        }
        if (exFile.empty() || ansFile.empty()) {
            printHelp();
            return 1;
        }
        gradeExercises(exFile, ansFile);
    }
    else {
        printHelp();
        return 1;
    }

    return 0;
}

测试运行

生成题目(输入命令参数 -n 10 -r 10)



判题(输入命令参数 -e Exercises.txt -a Answers.txt)

未修改前面生成的答案


修改了生成答案的第1、3、6、7条



项目小结

成功的地方

·算法优化:
使用表达式标准化方法确保唯一性,减少重复计算
改进分数约分算法,提高计算效率
优化递归生成策略,减少无效重试次数

·分工合作:
前期设计:共同讨论需求,明确分工
编码阶段:一人负责表达式生成,另一人负责计算与文件存储
测试优化:一起分析性能瓶颈,共同优化代码

·结对编程体验:
通过结对合作,减少了单人开发的盲区,互相补充优势
代码风格逐步统一,培养了团队协作能力

遇到的挑战与解决方案

·表达式去重问题
问题:如何防止 (1 + 2) × 3 和 3 × (1 + 2) 这样的表达式被认为是不同的?
解决方案:使用标准化表达式表示(如 canonical()),确保等价表达式的存储格式唯一。

·浮点数与分数计算精度
问题:浮点数计算精度问题导致 1/3 + 1/3 + 1/3 ≠ 1。
解决方案:使用分数类 (Fraction) 进行精确计算,避免精度丢失。

·性能优化瓶颈
问题:表达式递归生成时,重复计算次数较多,导致运行速度慢。
解决方案:使用计时工具分析,调整递归策略,减少无效计算。

posted @ 2025-03-22 21:56  zzZz614  阅读(28)  评论(0)    收藏  举报