您的位置:首页 > 其它

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

2016-07-04 13:23 218 查看
#起因
由于项目需要:弄出一套出题器算法,输入答案的范围、操作数的范围,操作数的数值范围,输出一道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,
做成一颗二叉树就是:



这样遍历后得到算式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,明显没毛病,但是这是有括号的算式,如果我们的需求是无括号的算式怎么办?
有两种方法,一种是数学老师教的那种,就是遇到括号前面是减号的话,把括号内的减号都换成加号即可,这样需要一个前看操作。
还有一种就要在栈底操作了,不知道这样还算不算是栈操作了,就是每次都拆分算式栈中的第一个数,这样就能避免括号的出现了,也不用前看操作了。
这两种方法需要你自己权衡。笔者直接用了栈底操作。

#输出



测试了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功力不够深请见谅。
如果你在用这个算法的过程中遇到了什么问题请直接评论。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: