多个数加减法算式出题器的算法分析和源码

起因

由于项目需要:弄出一套出题器算法,输入答案的范围、操作数的范围,操作数的数值范围,输出一道n个数的加减法算式题。

回忆

于是笔者立马想起了以前有一个项目也有一个出题器,但是那个是两位数的加减法出题器,可能需要改进下,于是开始改进了。
例如:a + b = c,是不是挺简单,当初笔者是这么想的:先随机出c和a,再随机出运算符, 最后求得b = c - b,脑子快一点的同志就看出来,这种情况只适用于加法,但是如果b是负数呢,那其实就是一个减法,于是应该改成这样b = (加法 ? c - b : b - c),于是运行了一下,果然行,于是随机了100次出题器,结果出现了b超出是表达式的数值范围,为什么呢?例如: 表达式的数值范围是[8, 10], 而答案的取值范围是[0, 2],那样如果a随机得到了8,而答案随机到了2, 运算符随机到加法,那怎么整呢?简单,b = (加法 ? c - b : b - c) ==> b = -6, 最终得到题目:8 + (-6) = 2,那样b就超出了表达式的取值范围了,解决办法就是控制操作符。

简单算式题生成

下面给出简单加减法出题器代码(js版):

function makeUnit(exp, vMax, vMin, level){
    if(level == 0){
        return;
    }

    var result = exp.shift();

    var operate;
    if(result < vMin){
        operate = 1;
    }else{
        operate = rand(1);
    }

    var left = rand(vMax, operate == 0 ? vMin : vMin + result);
    var right = operate == 0 ? result - left : left - result;
    if(right < 0){
        operate = operate == 0 ? 1 : 0;
        right = -right;
    }

    exp.unshift(left, operate, right);
    console.log('%d%s%d=%d',left, operateStr[operate], right, result);
}
//调用
var exp = [10]; //把答案压入栈里
makeUnit(exp, 10, 0, 1);

注①:你是否会奇怪笔者为什么要用shift方法来取出答案,还要用unshift方法把算式从底部插入,其实在简单算式里是看不出来的,下面会提及。

多数加减法

笔者一开始走错了路,虽然都是从答案开始反推,但是算式却是从整体入手的,结果没搞出来。
一个好学的同事也在想这个算法,他的想法是遍历所有的情况,知道遇到答案为止,但是明显不对,遍历所有情况的过程,是没有随机的概念的,所以只要答案一致,算式还是一样的,除非先把遍历列表随机打乱。结果他也没搞出来,但是他的一句话提醒了,我立马想到了一种方法,曾经在编译原理里看到过语法分析,就是二叉树来实现,但是原理并不一样。
其实你看到简单算式的js版的代码里就能看到方法名叫“makeUnit”,其实就是出题单元的意思,直接看一个样例(3个数的加减法):
3 + 4 = 7
这样,一个简单的算式出来了,然后怎么增加到3个数的加减法呢?答案很简单,就是再把其中一个被操作数做一次makeUnit即可,比如把里面3做一个makeUnit:
 3 + 4 = 7
 / 9 - 6= 3,
做成一颗二叉树就是:
218587-20160704124305077-1080536236.png
这样遍历后得到算式9 - 6 + 4 = 7,你还可以让它更深一些,就能产生任意n多个数的加减法算式了(n >= 2)。
注:笔者为了简单起见,直接用一个数组来存放算式,所以需要约定:约定奇数索引的值都是操作数,偶数索引的值都是操作符(0: 加法,1:减法,2:等于),例如[9, 1, 6, 0, 4, 2, 7] => 9 - 6 + 4 = 7

多数加减法算式的隐藏问题

在上面注①里面笔者提到了栈的操作是有原因的,现在揭晓一下。
因为多数加减法涉及到一个优先级的问题,就是从左到右算,遇到括号先算括号这两条。
我们再来看一个例子:
9 - 4 = 5,
  /  6 - 2 = 4
这样遍历后得到 9 - (6 - 2) = 5,明显没毛病,但是这是有括号的算式,如果我们的需求是无括号的算式怎么办?
有两种方法,一种是数学老师教的那种,就是遇到括号前面是减号的话,把括号内的减号都换成加号即可,这样需要一个前看操作。
还有一种就要在栈底操作了,不知道这样还算不算是栈操作了,就是每次都拆分算式栈中的第一个数,这样就能避免括号的出现了,也不用前看操作了。
这两种方法需要你自己权衡。笔者直接用了栈底操作。

输出

218587-20160704130155405-1611245701.png
测试了10次10个100以内的数的加减法算式生成。

源码

笔者先写了js版,然后翻译了c#版,都贴出来:

/**
 * Created by AlienCoder on 16/7/2.
 *
 * 加减法算式出题器
 */
'use strict';

var operateStr = ['+', '-', '='];

function rand(max, min){
    min = min || 0;
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function makeUnit(exp, vMax, vMin, level){
    if(level == 0){
        return;
    }

    var result = exp.shift();

    var operate;
    if(result < vMin){
        operate = 1;
    }else{
        operate = rand(1);
    }

    var left = rand(vMax, operate == 0 ? vMin : vMin + result);
    var right = operate == 0 ? result - left : left - result;
    if(right < 0){
        operate = operate == 0 ? 1 : 0;
        right = -right;
    }

    exp.unshift(left, operate, right);
    //console.log('%d%s%d=%d',left, operateStr[operate], right, result);

    makeUnit(exp, vMax, vMin, level - 1);
}

function makeQuestion(rMax, rMin, vMax, vMin, level){
    var result = rand(rMax, rMin);

    var exp = [result];
    makeUnit(exp, vMax, vMin, level);
    exp.push(2, result);
    return exp;
}

function expToString(exp){
    var str = '';
    exp.forEach((v, i)=>{
        if(i % 2 == 0){
            str += v;
        }else{
            str += operateStr[v];
        }
    });

    return str;
}

for(var i = 0; i < 10; i ++){
    console.log(expToString(makeQuestion(100, 0, 100, 0, 9)));
}
using System;
using System.Collections.Generic;
using System.Collections;

namespace alien{
    class QuestionMaker{
        string[] operateStr = new string[]{"+", "-", "="};
        Random random = new Random();

        public static void Main(string[] args){
            QuestionMaker maker = new QuestionMaker();

            for(var i = 0; i < 10; i++){
                Console.WriteLine(maker.expToString(maker.makeQuestion(100, 1, 100, 0, 9)));
            }
        }

        void makeUnit(ArrayList exp, int vMax, int vMin, int level){
            if(level == 0){
                return;
            }

            int result = (int)exp[0];
            exp.RemoveAt(0);

            int operate;
            if(result < vMin){
                operate = 1;
            } else{
                operate = random.Next(0, 2);
            }

            int left = random.Next(operate == 0 ? vMin : vMin + result, vMax);
            int right = operate == 0 ? result - left : left - result;
            if(right < 0){
                operate = operate == 0 ? 1 : 0;
                right = -right;
            }

            exp.Insert(0, right);
            exp.Insert(0, operate);
            exp.Insert(0, left);

            makeUnit(exp, vMax, vMin, level - 1);
        }

        ArrayList makeQuestion(int rMax, int rMin, int vMax, int vMin, int level){
            int result = random.Next(rMin, rMax);
            ArrayList exp = new ArrayList();
            exp.Add(result);
            makeUnit(exp, vMax, vMin, level);
            exp.Add(2);
            exp.Add(result);

            return exp;
        }

        string expToString(ArrayList exp){
            string str = "";
            for(int i = 0, li = exp.Count; i < li; i++){
                if(i % 2 == 0){
                    str += exp[i];
                } else{
                    str += operateStr[(int)exp[i]];
                }
            }

            return str;
        }
    }
}

后话

笔者的markdown功力不够深请见谅。
如果你在用这个算法的过程中遇到了什么问题请直接评论。

转载于:https://www.cnblogs.com/rockyf/p/5636447.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值