四则运算表达式生成 in Python (郑振练 朱海中)

Github源程序代码链接

项目相关要求

  • 题目

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

  • 说明

    自然数 :0, 1, 2, …。

    真分数 :1/2, 1/3, 2/3,1/4, 1’1/2, …。

    运算符 :+, −, ×, ÷。

    括号 :(, )。

    等号 :=。

    分隔符 :空格(用于四则运算符和等号前后)。

    算术表达式 :e = n | e1 +e2 | e1 − e2 | e1 ×e2 | e1 ÷ e2 | (e)。

    (其中e, e1和e2为表达式,n为自然数或真分数)

    四则运算题目 :e = ,其中e为算术表达式。

  • 需求

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

    Myapp.exe -n 10	# 将生成10道题目
    

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

    Myapp.exe -r 10  # 将生成10以内(不包括10)的四则运算题目
    

​ 注:该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。

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

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

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

​ 6. 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,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,它们之间不能通过有限次交换变成同一个题目。

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

​ 8. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:

​ Myapp.exe -e .txt -a .txt

​ 统计结果输出到文件Grade.txt,格式如下:

​ Correct: 5 (1, 3, 5, 7, 9)
​ Wrong: 5 (2, 4, 6, 8, 10)


项目设计

项目设计流程图

  • 首先按照一定的随机规则生成运算表达式 e1
  • 将生成的中缀表达式e1转化后缀表达式 e2
  • 将表达式e2转化为二叉树t1
  • 将t1利用加法和乘法交换律进行规范化处理,生成t2
  • 将t2进行中序遍历生成规范的中缀表达式e3
  • 利用e3可判断生成的表达式是否重复
  • 若重复,则舍弃;否则,将e1保存进表达式列表中

简单例子说明

  • 假设随机生成表达式 e1 = 2 * 3 + 4 * 5
  • 将e1转化为后缀表达式为 e2 = 23* 45*+
  • 将e2转化为二叉树t1,如下左图所示

  • 将t1按照加法交换进行规范化处理后,得t2,如上右图所示

  • 将t2进行中序遍历得表达式e3 = 5 * 4 + 3 * 2

  • 按照一定的规则将表达式e = 5 * 4 + 2 * 3 进行规范化后,也可得e4 = 5 * 4 + 3 * 2

  • 则可判断生成的表达式是否重复


规范规则说明

- 若左右子树均为叶子结点
  • 当操作符为 + 或 * 时,若左子树值小于右子树,则交换左右子树,否则不变

  • 若操作符为 / 时,不需要进行规范处理
  • 若操作符为 - 且左子树值大于右子树时,不变;若左子树值小于右子树,则结果出现负数,停止规范处理,舍弃该表达式
- 若左子树为非叶子结点,右子树为叶子结点
  • 当操作符为 + 或 * 时,将左子树中值最大的操作数a与右子树b的值进行比较,若a < b,交换左右子树;当a = b时,将深度小的子树放在左边,即交换左右子树;否则不变

  • 若操作符为 / 时,不需要进行规范处理
  • 若操作符为 - 且左子树值大于右子树时,不变;若左子树值小于右子树,则结果出现负数,停止规范处理,舍弃该表达式

-若左子树为叶子结点,右子树为非叶子结点

  • 当操作符为 + 或 * 时,将左子树的值a与右子树中值最大的操作数b进行比较,若a < b,交换左右子树;否则不变

-若左右子树均非叶子结点

  • 当操作符为 + 或 * 时,左子树中值最大的操作数为a、右子树中值最大的操作数为b、左子树的值为c、右子树的值为d,若a < b或者c < d,交换左右子树;当a = b时,若左右子树的操作符分别为 * 和 /,则交换子树;否则不变

  • 若操作符为 / 时,不需要进行规范处理
  • 若操作符为 - 且左子树值大于右子树时,不变;若左子树值小于右子树,则结果出现负数,停止规范处理,舍弃该表达式

-左右子树深度相差为2

  • 当操作符为 + 或 * 时,将左子树中值最大的操作数a与右子树中值最大的操作数b进行比较,若a < b,交换左右子树;否则不变

  • 若操作符为 / 时,不需要进行规范处理
  • 若操作符为 - 且左子树值大于右子树时,不变;若左子树值小于右子树,则结果出现负数,停止规范处理,舍弃该表达式

-特殊情况处理

  • 当表达式中操作符全部为 + 或 * 时,将操作数从左到右降序排序

    3 + 1 + 2 + 5 —> 5 + 3 + 2 + 1

    3 * 1 * 2 * 5 —> 5 * 3 * 2 * 1


程序代码说明

-生成随机表达式

import random


"""
随机生成题目数量为n,数值在r以内的四则运算表达式
parameters:
    n(int):四则运算表达式的个数
    r(int):操作数的值上界
return:
    mid_exps(list):存储中缀表达式的列表
"""
def generate_mid_exp(max_num):
    data_num = random.randint(2, 4)     # 操作数个数
    datas = []                          # 操作数
    operators = ['+', '-', '*', '/']
    select_operators = []               # 操作符

    for i in range(data_num):          # 随机生成操作数
        data = int(random.random() * max_num)
        datas.append(data)
    for i in range(data_num - 1):      # 随机生成操作符
        operator = operators[int(random.random() * 4)]
        select_operators.append(operator)

    # 操作符只存在 + 或 *,进行特殊处理
    select_operators_set = set(select_operators)
    op = select_operators_set.copy().pop()
    reg_mid_exp = None
    expression_value = None
    if len(select_operators_set) == 1 and op in ['+', '*']:
        reg_mid_exp = list(map(str, sorted(datas, reverse=True)))
        expression_value = datas[0]
        for i in range(len(reg_mid_exp)-1):
            reg_mid_exp.insert(i*2+1, op)
            if op == '+':
                expression_value = expression_value + datas[i+1]
            else:
                expression_value = expression_value * datas[i+1]
    # 生成中缀表达式
    mid_exp = [datas[-1]]
    for i in range(data_num - 1):
        mid_exp.append(select_operators[i])
        mid_exp.append(datas[i])
    mid_exp = add_bracket(mid_exp, len(datas)) # 以33%的概率随机生成括号
    if reg_mid_exp == None:         # 操作符随机的情况
        return mid_exp, None, None
    else:                           # 操作符全为 + 或 * 的情况
        return mid_exp, ' '.join(reg_mid_exp), (expression_value,1)

"""
以33%的概率,给生成的表达式加上括号
parameters:
    mid_exp(list):随机生成的表达式
    data_num(int):操作数个数
return:
    mid_exp(list):处理后的中缀表达式
"""
def add_bracket(mid_exp, data_num):
    # 为生成的表达式加括号
    if data_num == 3:  # 操作数个数为3时的加括号方式
        bracket_way = random.randint(0, 4)
        prob = [0, 1, 0, 2, 0, 0]  # 以33%的概率加上括号,67%的概率不加括号
        bracket_way = prob[bracket_way]
        if bracket_way == 1:
            mid_exp.insert(0, '(')
            mid_exp.insert(4, ')')
        elif bracket_way == 2:
            mid_exp.insert(2, '(')
            mid_exp.append(')')
    elif data_num == 4:  # 操作数个数为4时的加括号方式
        bracket_way = random.randint(0, 5)
        prob = [0, 0, 1, 0, 0, 2, 0, 3, 0, 0, 4, 0, 5, 0, 0]  # 以33%的概率加括号,以67%的概率不加括号
        if bracket_way == 1:
            mid_exp.insert(0, '(')
            mid_exp.insert(4, ')')
        elif bracket_way == 2:
            mid_exp.insert(2, '(')
            mid_exp.insert(6, ')')
        elif bracket_way == 3:
            mid_exp.insert(4, '(')
            mid_exp.append(')')
        elif bracket_way == 4:
            mid_exp.insert(0, '(')
            mid_exp.insert(6, ')')
        elif bracket_way == 5:
            mid_exp.insert(2, '(')
            mid_exp.append(')')
    return mid_exp

-将中缀表达式转化为后缀表达式

"""
将中缀表达式转化为后缀表达式
parameters:
    exp_mid(list):中缀表达式
return:
    exp_post(list):后缀表达式
"""
def mid_expression_to_post_expression(exp_mid):
    exp_post = []   # 后缀表达式
    stack = []      # 栈
    operators = {'(': 0, ')': 0, '+': 1, '-': 1, '*': 2, '/': 2} # 操作符

    while (len(exp_mid)):
        # 对中缀表达式中的操作符进行处理
        if exp_mid[0] in operators.keys():
            if exp_mid[0] == ')':   # 操作符为右括号
                while True:
                    if stack[len(stack) - 1] == '(':
                        break
                    else:           # 将栈中属于括号内的运算符加入到后缀表达式中
                        exp_post.append(stack.pop())
                stack.pop()         # 删除左括号
            elif len(stack) == 0 or exp_mid[0] == '(' or operators[stack[-1]] < operators[exp_mid[0]]:
                stack.append(exp_mid[0])    # 左括号、优先级高的操作符进栈,或者栈中没有操作符时进栈
            elif operators[stack[-1]] >= operators[exp_mid[0]]:
                while operators[stack[-1]] >= operators[exp_mid[0]]:
                    exp_post.append(stack.pop()) # 将优先级高的运算符出栈处理
                    if len(stack) == 0:
                        break
                stack.append(exp_mid[0])
        # 对中缀表达式中的操作数进行处理
        else:
            exp_post.append(exp_mid[0])        # 将操作数添加到后缀表达式中
        exp_mid = exp_mid[1:]
        if len(exp_mid) == 0:
            while len(stack):
                exp_post.append(stack.pop())
    return exp_post

-将后缀表达式转化为二叉树

"""
将后缀表达式转化为二叉树
parameters:
    post_expression(list):后缀表达式
return:
    expression_tree(dict):表达式二叉树
"""
def post_expression_to_bitree(post_expression):
    base_node = post_expression.pop()
    exp_tree = {base_node:[]}
    operators = ['+', '-', '*', '/']

    if not post_expression:
        return {}
    # 左子树递归处理
    if post_expression[-1] in operators: # 右子树为操作符
        exp_tree[base_node].append(post_expression_to_bitree(post_expression))
    else:                               # 右子树为操作数
        exp_tree[base_node].append(post_expression.pop())
    # 右子树递归处理
    if post_expression[-1] in operators: # 左子树为操作符
        exp_tree[base_node].insert(0, post_expression_to_bitree(post_expression))
    else:                               # 左子树为操作数
        exp_tree[base_node].insert(0, post_expression.pop())
    return exp_tree

-规范化处理生成的二叉树并计算表达式的值

"""
将生成的二叉树进行标准化处理
parameters:
    exp_tree(dict):表达式二叉树
return:
    value(float):表达式的值
    max_value(float):左右子树中的最大值
    key(str):当前节点的操作符
"""
def to_reg_exp_tree(exp_tree):
    (key, child), = exp_tree.items()  # 获取子树

    if type(child[0]) == type(1) or type(child[0]) == type(0.1):   # 左子树的操作数
        left_value = (child[0], 1)
        max_lvalue = child[0]
        left_operator = '_'          # 非操作符
    else:                            # 递归规范左子树
        left_value, max_lvalue, left_operator = to_reg_exp_tree(child[0])
        if left_value == None:
            return None, None, None
    if type(child[1]) == type(1) or type(child[1]) == type(0.1):  # 右子树的操作数
        right_value = (child[1], 1)
        max_rvalue = child[1]
        right_operator = '_'
    else:                            # 递归规范右子树
        right_value, max_rvalue, right_operator = to_reg_exp_tree(child[1])
        if right_value == None:
            return None, None, None
    left_value_sub = left_value[0] / left_value[1]
    right_value_sub = right_value[0] / right_value[1]
    # 进行表达式的规范化处理
    if ((max_lvalue < max_rvalue or (left_value_sub < right_value_sub and left_operator != '_' and right_operator != '_')
        or (max_lvalue == max_rvalue and ((left_operator == '*' and right_operator == '/') or (left_operator != '_' and
        right_operator == '_')))) and key in ['+', '*']):    # 进行表达式的规范化处理
        exp_tree[key] = [child[1], child[0]]                  # 交换左右子树
        (left_value, right_value) = (right_value, left_value)
    elif left_value_sub < right_value_sub and key == '-':    # 表达式出现负数
        return None, None, None
    # 计算当前子树的最大操作数
    if max_lvalue >= max_rvalue:
        max_value = max_lvalue
    else:
        max_value = max_rvalue
    # 计算表达式的值
    value = calculate(key, left_value, right_value)
    if value == None:             # 表达式中除数出现0的情况
        return None, None, None
    return value, max_value, key

"""
计算字符串表达式的值
parameters:
    operator(str):操作符
    left_value(float):表达式的左操作数
    right_value(float):表达式的右操作数
return:
    result(tuple):分数组成的元组,第一个值为分子,第二个值为分母
"""
def calculate(operator, left_value, right_value):
    tuple_type = type(())
    if operator == '+':
        if type(left_value) == tuple_type and type(right_value) == tuple_type:
            return (left_value[0]*right_value[1]+right_value[0]*left_value[1], left_value[1]*right_value[1])
        elif type(left_value) == tuple_type and type(right_value) != tuple_type:
            return (left_value[1]*right_value+left_value[0], left_value[1])
        elif type(left_value) != tuple_type and type(right_value) == tuple_type:
            return (left_value*right_value[1]+right_value[0], right_value[1])
        else:
            return (left_value + right_value, 1)
    elif operator == '-':
        if type(left_value) == tuple_type and type(right_value) == tuple_type:
            return (left_value[0]*right_value[1]-right_value[0]*left_value[1], left_value[1]*right_value[1])
        elif type(left_value) == tuple_type and type(right_value) != tuple_type:
            return (left_value[0]-left_value[1]*right_value, left_value[1])
        elif type(left_value) != tuple_type and type(right_value) == tuple_type:
            return (left_value*right_value[1]-right_value[0], right_value[1])
        else:
            return (left_value - right_value, 1)
    elif operator == '*':
        if type(left_value) == tuple_type and type(right_value) == tuple_type:
            return (left_value[0]*right_value[0], left_value[1]*right_value[1])
        elif type(left_value) == tuple_type and type(right_value) != tuple_type:
            return (left_value[0]*right_value, left_value[1])
        elif type(left_value) != tuple_type and type(right_value) == tuple_type:
            return (left_value*right_value[0], right_value[1])
        else:
            return (left_value * right_value, 1)
    elif operator == '/' and right_value[0] != 0:
        if type(left_value) == tuple_type and type(right_value) == tuple_type:
            return (left_value[0]*right_value[1], left_value[1]*right_value[0])
        elif type(left_value) == tuple_type and type(right_value) != tuple_type:
            return (left_value[0], left_value[1]*right_value)
        elif type(left_value) != tuple_type and type(right_value) == tuple_type:
            return (left_value*right_value[1], right_value[0])
        else:
            return (left_value, right_value)
    return None

-将规范化后的二叉树转化为规范化的中缀表达式

"""
将生成的表达式二叉树转化为中缀表达式
用于判断随机生成的四则运算表达式是否重复
parameters:
    exp_tree(dict):表达式二叉树
return:
    exp_mid(str):规范的中缀表达式
"""
def exp_tree_to_mid_exp(exp_tree):
    (key, child), = exp_tree.items()  # 获取子树

    if type(child[0]) == type(1) or type(child[0]) == type(0.1):
        mid_exp = '(' + str(child[0])
    else:
        mid_exp = '(' + str(exp_tree_to_mid_exp(child[0]))
    mid_exp += " " + key + " "
    if type(child[1]) == type(1) or type(child[1]) == type(0.1):
        mid_exp += str(child[1]) + ')'
    else:
        mid_exp += str(exp_tree_to_mid_exp(child[1])) + ')'
    return mid_exp

-判定答案中的对错并进行数量统计

"""
将答案进行评分
parameters:
    exp_filename(str):表达式文件路径
    a_filename(str):需要进行评分的答案文件路径
评分结果写入到Grade.txt文件中
"""
def grade(exp_filename, a_filename):
    dirname = re.sub(exp_filename, '', os.path.abspath(exp_filename))
    correct = []
    wrong = []
    with open(dirname + 'Answers.txt') as file:
        right_answers = file.readlines()
    with open(a_filename) as file:
        answers = file.readlines()
    for i in range(len(right_answers)):
        r_answer = re.split('\s+', right_answers[i])[1]
        answer = re.split('\s', answers[i])[1]
        if r_answer == answer:
            correct.append(i+1)
        else:
            wrong.append(i+1)
    with open('Grade.txt', 'w+') as file:
        file.write('Correct:{:d} ({})\n'.format(len(correct), ','.join(list(map(str, correct)))))
        file.write('Wrong:{:d} ({})\n'.format(len(wrong), ','.join(list(map(str, wrong)))))

-生成表达式的控制流程

"""
求两个数的最大公约数
parameters:
    x(int):一个整数
    y(int):另一个整数
return:
    两个整数的最大公约数
"""
def gcd(x, y):
    while y:
        t = x % y
        x = y
        y = t
    return x

"""
生成n个满足要求的四则运算表达式
parameters:
    n(int):表达式的个数
    r(int):参与运算的操作数的上界
结果生成两个文件,保存题目和答案
"""
def generate_expressions(n, r):
    mid_exps = []       # 生成的表达式
    answers = []        # 表达式对应的答案
    reg_mid_exps = []   # 规范化后的表达式
    exp_num = 0
    while exp_num != n:
        origin_exp, reg_mid_exp, answer = generate_mid_exp(r)  # 随机生成表达式
        if reg_mid_exp == None:
            mid_exp = origin_exp[:]
            post_exp = mid_expression_to_post_expression(mid_exp) # 生成后缀表达式
            exp_tree = post_expression_to_bitree(post_exp)  # 生成后缀表达式对应的二叉树
            (answer,_,_) = to_reg_exp_tree(exp_tree)        # 规范化生成的二叉树
            if not answer: # 生成的表达式出现负数,舍弃
                continue
            reg_mid_exp = exp_tree_to_mid_exp(exp_tree)     # 生成规范化后的中缀表达式
        # 判断生成的表达式是否重复
        if reg_mid_exp not in reg_mid_exps:
            mid_exps.append(origin_exp)
            # 将表达式的值标准化
            common_div = gcd(answer[0], answer[1])         # 最大公约数
            answer = (int(answer[0] / common_div), int(answer[1] / common_div))  # 约分
            if answer[1] == 1:
                answer = answer[0]
            elif answer[0] > answer[1]:
                a = int(answer[0] / answer[1])
                b = answer[0] - answer[1] * a
                if b == 0:
                    answer = a
                else:
                    answer = '{:d}`{:d}/{:d}'.format(a, b, answer[1])
            else:
                answer = '{:d}/{:d}'.format(answer[0], answer[1])
            answers.append(answer)
            reg_mid_exps.append(reg_mid_exp)
            exp_num += 1
    # 保存生成的表达式和答案
    with open(r"Exercises.txt", 'w+') as e_file:
        for i in range(n):
            e_file.write('{:d}. {}\n'.format(i+1, ' '.join(list(map(str, mid_exps[i])))))
    with open(r'Answers.txt', 'w+') as a_file:
        for i in range(n):
            a_file.write('{:d}. {}\n'.format(i+1, str(answers[i])))

-程序入口

if __name__ == '__main__':
    args = sys.argv
    # 判断输入的参数格式是否符合要求
    if ((('-r' not in args or '-n' not in args) and ('-e' not in args or '-a' not in args)) or len(args) != 5
        or (args[1] not in ['-e','-a','-r','-n'] or args[3] not in ['-e','-a','-r','-n'])):
        print('the format of the parameters is error')
        sys.exit(0)
    else:
        if '-n' in args:
            try:    # 判断参数值类型是否正确
                args[2] = int(args[2]); args[4] = int(args[4])
            except:
                print('the type of the parameters is error')
                sys.exit(0)
            if args[2] <= 0 or args[4] <= 0: # 判断参数值范围是否张雀
                print('the value of the parameters is error')
                sys.exit(0)
            # 生成表达式
            if args[1] == '-n':
                n = args[2]; r = args[4]
            else:
                n = args[4]; r = args[2]
            generate_expressions(n, r)
            print('generate expressions done.')
        else:
            exp_filename = args[2]
            a_filename = args[4]
            if not os.path.exists(exp_filename) or not os.path.exists(a_filename):
                print('the file is not found')
                sys.exit(0)
            elif not os.path.exists(re.sub(exp_filename, '', os.path.abspath(exp_filename)) + 'Answers.txt'):
                print('the correct answer file is not found')
                sys.exit(0)
            else:
                grade(exp_filename, a_filename)
                print('generate grade file done.')

运行测试

-参数错误判断和提示

-生成5道题目,左边是表达式,右边是答案

-生成10道题目,左边是表达式,右边是答案

-生成20道题目,左边是表达式,右边是答案

-判定答案中的对错并进行数量统计,从左到右分别为表达式、正确答案、评分答案合评分结果

-生成1万条表达式,左边是表达式,右边是答案


PSP表格

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

项目小结

  • 这次结对编程是两人进行合作的,在刚开始拿到这个题目的时候,对查重那个模块有点懵逼,然后去网上查找了一些资料,了解了大概的思路,通过多次和队友进行交流,才慢慢对整个模块有清晰的思路,在后续很多细节的修改中,也是通过多次交流才得以完善的,所以,这次的编程,收获的更多的是怎么进行交流和合作。

posted on 2018-09-27 21:55  zzlian  阅读(256)  评论(0)    收藏  举报

导航