软工第三次作业

结对项目——小学四则运算生成程序

这个作业属于哪个课程 计科23级12班
这个作业要求在哪里 个人项目 - 作业 - 计科23级12班 - 班级博客 - 博客园
这个作业的目标 开发一个可自动生成、计算并校对小学四则运算题目的程序,实现批量出题、自动判分与性能优化,以提升练习题生成与评测的效率。

作者:齐畅 3223004601 宋可月 3223001500
github仓库地址:https://github.com/sjdndndks/sjdndndks/tree/main/cal_hw

一、PSP表格

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

二、效能分析

截屏2025-10-22 15.57.19
截屏2025-10-22 16.13.49

三、设计与实现过程

3.1 整体架构设计

本项目采用面向对象的设计思想,将功能模块化为三个核心类:

ArithmeticApp(主程序类)
    │
    ├── ExpressionGenerator(表达式生成器)
    │       ├── generateExpression()
    │       ├── normalizeExpression()
    │       └── calculateExpression()
    │
    └── Fraction(分数运算类)
            ├── operator+()
            ├── operator-()
            ├── operator*()
            ├── operator/()
            └── simplify()

3.2 类设计说明

3.2.1 Fraction类(分数类)

设计目的:封装分数的表示和运算逻辑

核心成员变量

  • int numerator:分子
  • int denominator:分母
  • int integer:整数部分(支持带分数)

核心方法

  • gcd():计算最大公约数,使用欧几里得算法
  • simplify():化简分数为最简形式
  • operator+/-/*//:重载四则运算符
  • toString():格式化输出(支持整数、真分数、带分数)

设计亮点

  1. 构造函数中自动化简,确保内部状态一致性
  2. 支持带分数表示,符合小学数学习惯
  3. 运算符重载使代码更直观易读

3.2.2 ExpressionGenerator类(表达式生成器)

设计目的:负责生成符合要求的四则运算表达式

核心成员变量

  • int range:数值范围
  • std::mt19937 rng:高质量随机数生成器
  • std::set<std::string> usedExpressions:去重集合

核心方法

  • generateRandomNumber():生成随机数(整数或分数)
  • generateExpression():生成完整表达式
  • normalizeExpression():标准化表达式(处理交换律)
  • calculateExpression():计算表达式结果

设计亮点

  1. 使用重试机制确保生成的表达式结果非负
  2. 标准化算法处理等价表达式(如1+22+1
  3. 运算符优先级正确处理(先乘除后加减)

3.2.3 ArithmeticApp类(主程序类)

设计目的:协调各模块,处理命令行参数和文件I/O

核心方法

  • parseArguments():解析命令行参数
  • generateExercises():批量生成题目
  • saveToFiles():保存到文件
  • verifyAnswers():验证答案正确性

3.3 关键函数流程图

generateExpression()函数流程图

开始
  │
  ├─→ 随机决定运算符数量(1-3个)
  │
  ├─→ 生成第一个随机数
  │
  ├─→ [循环:对每个运算符]
  │    │
  │    ├─→ 选择运算符类型
  │    │    ├─ 70%概率:加减法
  │    │    └─ 30%概率:乘除法
  │    │
  │    ├─→ 生成下一个数
  │    │
  │    ├─→ 构建临时表达式
  │    │
  │    ├─→ 计算临时结果
  │    │
  │    ├─→ 检查结果是否有效
  │    │    ├─ 是否非负?
  │    │    ├─ 是否在范围内?
  │    │    └─ 是否计算成功?
  │    │
  │    └─→ [无效则重试,最多50次]
  │
  ├─→ 标准化表达式
  │
  ├─→ 检查是否重复
  │    ├─ 是:递归重新生成
  │    └─ 否:继续
  │
  └─→ 返回表达式和结果

normalizeExpression()标准化流程

输入:原始表达式
  │
  ├─→ 分词(按空格分割)
  │
  ├─→ 判断运算符数量
  │    │
  │    ├─ 1个运算符:
  │    │   └─→ 若为+或*,按数值大小排序
  │    │
  │    └─ 2个运算符:
  │        └─→ 若都是+或都是*,三个数排序
  │
  └─→ 返回标准化表达式

3.4 模块关系图

main()
  │
  ├─→ ArithmeticApp::parseArguments()
  │     │
  │     ├─ 模式1:-n(生成题目)
  │     │     │
  │     │     ├─→ generateExercises()
  │     │     │     └─→ ExpressionGenerator::generateExpression()
  │     │     │           └─→ Fraction运算
  │     │     │
  │     │     └─→ saveToFiles()
  │     │
  │     └─ 模式2:-e(验证答案)
  │           └─→ verifyAnswers()
  │                 └─→ calculateExpression()
  │                       └─→ Fraction运算

3.5 数据流设计

用户输入参数
    ↓
命令行解析
    ↓
ExpressionGenerator初始化(设置range和rng种子)
    ↓
循环生成表达式:
    ├─ 生成随机数(Fraction对象)
    ├─ 选择运算符
    ├─ 验证表达式有效性
    ├─ 标准化表达式
    └─ 检查重复
    ↓
存储到vector<pair<string, Fraction>>
    ↓
写入文件:
    ├─ Exercises.txt(题目)
    └─ Answers.txt(答案)

四、关键代码与说明

4.1 分数类核心代码

class Fraction {
private:
    int numerator;   // 分子
    int denominator; // 分母
    int integer;     // 整数部分

    // 使用欧几里得算法计算最大公约数
    // 时间复杂度:O(log(min(a,b)))
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    // 化简分数到最简形式
    void simplify() {
        if (numerator == 0) {
            denominator = 1;
            return;
        }
        // 约分
        int g = gcd(std::abs(numerator), denominator);
        numerator /= g;
        denominator /= g;

        // 假分数转带分数
        if (std::abs(numerator) >= denominator) {
            integer += numerator / denominator;
            numerator = numerator % denominator;
        }
    }

设计思路

  • gcd()使用递归实现欧几里得算法,简洁高效
  • simplify()在构造函数中自动调用,保证不变性
  • 支持假分数自动转换为带分数形式

4.2 分数运算重载

// 分数加法:通分后相加
Fraction operator+(const Fraction& other) const {
    int new_integer = integer + other.integer;
    int new_numerator = numerator * other.denominator +
                        other.numerator * denominator;
    int new_denominator = denominator * other.denominator;
    return Fraction(new_numerator, new_denominator, new_integer);
}

// 分数乘法:先转为假分数再相乘
Fraction operator*(const Fraction& other) const {
    int total1 = integer * denominator + numerator;
    int total2 = other.integer * other.denominator + other.numerator;
    return Fraction(total1 * total2, denominator * other.denominator);
}

设计思路

  • 加减法:整数部分和分数部分分别处理
  • 乘除法:统一转换为假分数计算,简化逻辑
  • 所有运算结果通过构造函数自动化简

4.3 表达式生成核心逻辑

std::pair<std::string, Fraction> generateExpression() {
    // 随机生成1-3个运算符
    int operatorCount = (rng() % 3) + 1;
    std::vector<Fraction> numbers;
    std::vector<char> operators;

    // 生成第一个数
    numbers.push_back(generateRandomNumber());

    // 逐个添加运算符和数字
    for (int i = 0; i < operatorCount; ++i) {
        char op;
        Fraction nextNum;
        bool validExpr = false;
        int attempts = 0;

        // 最多尝试50次,避免死循环
        while (!validExpr && attempts < 50) {
            attempts++;

            // 优化运算符概率:70%加减,30%乘除
            if (rng() % 100 < 70) {
                op = (rng() % 100 < 60) ? '+' : '-';
            } else {
                op = (rng() % 2 == 0) ? '*' : '/';
            }

            nextNum = (op == '*' || op == '/') ?
                      generateRandomInteger() : generateRandomNumber();

            operators.push_back(op);
            numbers.push_back(nextNum);

            // 构建并验证临时表达式
            std::string tempExpr = buildExpression(numbers, operators);

            try {
                Fraction tempResult = calculateExpression(tempExpr);
                // 确保结果非负且在范围内
                if (tempResult.getValue() >= 0 &&
                    tempResult.getValue() < range) {
                    validExpr = true;
                } else {
                    // 回退
                    operators.pop_back();
                    numbers.pop_back();
                }
            } catch (...) {
                operators.pop_back();
                numbers.pop_back();
            }
        }

        // 如果50次都失败,减少运算符数量
        if (!validExpr) {
            operatorCount = i;
            break;
        }
    }

    // 构建最终表达式
    expression = buildExpression(numbers, operators);
    result = calculateExpression(expression);

    // 去重检查
    std::string normalizedExpr = normalizeExpression(expression);
    if (usedExpressions.find(normalizedExpr) != usedExpressions.end()) {
        return generateExpression();  // 递归重新生成
    }
    usedExpressions.insert(normalizedExpr);

    return {expression, result};
}

设计思路

  1. 渐进式生成:逐个添加运算符和数字,每次添加后立即验证
  2. 重试机制:如果当前选择导致无效结果,回退并重试
  3. 概率调优:通过调整运算符概率,减少无效生成
  4. 递归去重:检测到重复立即递归重新生成

4.4 表达式标准化

std::string normalizeExpression(const std::string& expr) {
    std::istringstream iss(expr);
    std::vector<std::string> tokens;
    std::string token;

    // 分词
    while (iss >> token) {
        if (token != "=") tokens.push_back(token);
    }

    // 处理两个数的情况:a op b
    if (tokens.size() == 3) {
        Fraction num1 = parseFraction(tokens[0]);
        char op = tokens[1][0];
        Fraction num2 = parseFraction(tokens[2]);

        // 对于加法和乘法,较小的数放前面
        if ((op == '+' || op == '*') &&
            num2.getValue() < num1.getValue()) {
            return num2.toString() + " " + op + " " + num1.toString();
        }
        return expr;
    }

    // 处理三个数的情况:a op1 b op2 c
    if (tokens.size() == 5) {
        Fraction num1 = parseFraction(tokens[0]);
        char op1 = tokens[1][0];
        Fraction num2 = parseFraction(tokens[2]);
        char op2 = tokens[3][0];
        Fraction num3 = parseFraction(tokens[4]);

        // 如果运算符相同且满足交换律
        if ((op1 == op2) && (op1 == '+' || op1 == '*')) {
            std::vector<Fraction> nums = {num1, num2, num3};
            std::sort(nums.begin(), nums.end(),
                [](const Fraction& a, const Fraction& b) {
                    return a.getValue() < b.getValue();
                });
            return nums[0].toString() + " " + op1 + " " +
                   nums[1].toString() + " " + op2 + " " +
                   nums[2].toString();
        }
    }

    return expr;
}

设计思路

  • 利用加法和乘法的交换律,将等价表达式转换为统一形式
  • 按数值大小排序,确保1+22+1标准化为同一形式
  • 只处理相同运算符的情况,避免改变计算顺序

4.5 计算表达式(支持优先级)

Fraction calculateExpression(const std::string& expr) {
    std::istringstream iss(expr);
    std::vector<std::string> tokens;
    std::string token;

    // 分词
    while (iss >> token) {
        if (token != "=") tokens.push_back(token);
    }

    // 第一遍:计算乘除(高优先级)
    for (size_t i = 1; i < tokens.size(); i += 2) {
        if (tokens[i] == "*" || tokens[i] == "/") {
            Fraction left = parseFraction(tokens[i - 1]);
            Fraction right = parseFraction(tokens[i + 1]);
            Fraction result = (tokens[i] == "*") ?
                             left * right : left / right;
            tokens[i - 1] = result.toString();
            tokens.erase(tokens.begin() + i, tokens.begin() + i + 2);
            i -= 2;  // 回退索引
        }
    }

    // 第二遍:计算加减(低优先级)
    Fraction result = parseFraction(tokens[0]);
    for (size_t i = 1; i < tokens.size(); i += 2) {
        Fraction operand = parseFraction(tokens[i + 1]);
        if (tokens[i] == "+") {
            result = result + operand;
        } else if (tokens[i] == "-") {
            result = result - operand;
        }
    }

    return result;
}

设计思路

  • 两遍扫描法:先处理乘除,再处理加减
  • 原地修改tokens数组,节省内存
  • 支持任意长度的表达式

五、测试运行与结果分析

5.1初步运行

1.运行命令

image

2.题目文件

image

3.答案文件

image

$4.统计结果

image

5.2 正确性保证

我们如何确定程序是正确的?

  1. 数学正确性
    • 分数运算基于标准数学公式实现
    • 使用欧几里得算法保证最大公约数计算正确
    • 所有运算结果经过化简到最简形式
  2. 逻辑正确性
    • 使用断言检查关键不变量
    • 运算符优先级通过两遍扫描法严格实现
    • 边界条件(如除零、负数)有完善的异常处理
  3. 黑盒测试
    • 10个测试用例覆盖基本功能、边界情况、异常输入
    • 手工验证100个随机题目的答案
    • 所有测试用例100%通过
  4. 压力测试
    • 生成10000道题目无崩溃
    • 长时间运行无内存泄漏
    • 性能稳定可靠
  5. 对比验证
    • 将程序生成的答案与在线计算器对比
    • 100个样本对比结果100%一致

六、项目小结

在这次结对编程项目中,我们不仅完成了功能的实现,更重要的是体会到了团队协作的力量。最初在任务分工和代码风格上出现过一些分歧,但通过沟通与讨论,我们逐渐学会了理解彼此的思路,取长补短,最终使程序更加完善。整个过程中,我们从需求分析到测试优化都经历了反复的修改与改进,深刻感受到编程不仅是技术活,更是耐心与合作的体现。我的搭档在逻辑分析和调试方面非常细致,帮助我们快速定位问题;而我在整体架构和代码整理上也发挥了自己的优势。通过这次合作,我们都更加明白了团队中互补的重要性,也收获了珍贵的信任与默契。这次结对开发让我们成长很多,不仅提升了编程能力,也让我们学会了如何共同面对困难、分享成果。

posted @ 2025-10-22 12:51  qokakokaks  阅读(5)  评论(0)    收藏  举报