软件工程学习之小学四则混合运算出题软件 Version 1.1 设计思路及感想
继上次采用形式文法来生成混合运算的算式,由于算法中没有引入控制参数而导致容易产生形式累赘(多余的括号等)的算式。本次更新决定采用一种更为简单有效的生成方式,由给出的一个随机的最终答案S,通过给定的一个基本运算(加减乘除)将数字分解为两个数a,b,使得这两个数的运算结果为之前的数S,那么a,b分别可按同样的规则进行拆分,如此反复多次便可得到一个混合运算算式。这个过程实际上也是二叉树的生成过程,也是我们相当熟悉的算法了。当然,为了生成正确的算式还是需要解决一基本些问题的。
问题1:使数S按某一运算拆解,那么应该如何去拆解?
分析:对数字实现按某运算实现拆解,无疑我们的讨论范围只有加减乘除了,那么只需对四种情况分别设计相应的算法最终打包即可。注:本次算法仅实现正整数运算拆解。
⑴加法:S=a+b,那么令 a=rand()%S,b=S-a 即可;特殊地,S=0 时 a=b=0 ;
⑵减法:S=a-b,那么令 a=S+rand()%N (N>0),b=a-s 即可;特殊地,S=0 时 a=b=C (C是大于零的常数);
⑶除法:S=a/b,那么令 a=S*(1+rand()%N),b=a/s 即可;特殊地,S=0 时 a=0,b=C1(C1是大于零的常数);
⑷乘法:既然把乘法放到最后,那么可想而知实现起来是有点麻烦了。
S=a*b,那么意味着 a,b 都是S的因子,那么如何随机地获得一组S的因子呢?
首先,最简单的方法是我们可以用枚举法来获得S的所有因子:
for(i=i;i<S;i++) //枚举,这个算法会得到重复的因子组合
if(!(S%i)) //能否除尽
cout<<i<<endl; //打印因子
假设已经获得S的n个因子,那么我们只需要等可能地从这n个因子中随机选择一个a,那么 b=S/a 即可。
鉴于本人懒╮(╯∇╰)╭,我采用如下算法:
for(i=1;i<S;i++)
if(!(S%i))
if(rand()%2) //这样做的坏处是对于S的所有因子而言,a取到1的概率最大,嘛~~╮(╯_╰)╭,下个版本再改进吧。
break;
a=i,b=S/i;
特殊地,S=0 时,a or b=0即可。
分装后的函数:
/* Random split */
void OP_Decompose(Operator OP,int &a,int &b,int num) //分别用 0,1,2,3 来代表加、减、乘、除 typedef unsigned char Operator
{
switch(OP)
{
case 0:// +
{
if(num)
{
a=rand()%(num+1);
b=num-a;
}
else a=b=0;
}break;
case 1:// -
{
if(num)
{
a=num+rand()%100;
b=a-num;
}
else a=b=rand()%100;
}break;
case 2:// *
{
if(num)
{
int i;
for(i=1;i<num;i++)
if(!(num%i))
if(rand()%2)
break;
a=i,b=num/i;
}
else
{
if(rand()%2)
a=rand()%100,b=0;
else
b=rand()%100,a=0;
}
}break;
case 3:// /
{
if(num)
{
a=num*(1+rand()%10+rand()%10);
b=a/num;
}
else
a=0,b=rand()%100+1;
}
}
}
通过上述讨论,拆解问题就此告一段落了,其实也是整个程序中的核心问题之一吧(,,#゚Д゚)。
问题2:之前说过了是以二叉树生成的方式来产生算式,那么如果父节点的运算符优先级高于子节点的运算符,但实际上是先计算子节点的,那么就需要括号介入了。
分析:如果在生成二叉树的过程中,以先建立好父节点的所有数据信息再分别递归构造子节点的方式生成,若父节点对子节点有某些要求,那么只需要通过参数传递来约束子节点的建立。即,当父节点在构造子节点时将自己的运算符告诉子节点,然后子节点通过对比与父节点的运算优先级来决策是否添加括号。而在对比优先级时,我们只需要考虑父节点的优先级是否高于子节点,无需关心孰高孰低。在上一个问题中,我们定义了加减乘除的代号分别为0,1,2,3 ,观察发现0,1的二进制数高位为0,而2,3的二进制数高位为1,那么只需要对高位进行对比即可。便有如下宏定义:
#define OA 0 //加法
#define OS 1 //减法
#define OM 2 //乘法
#define OD 3 //除法
#define JUD_Priority(OP1,OP2) (((Operator)(OP1)>>1<(Operator)(OP2)>>1)?true:false) //当且仅当OP2优先级高于OP1时返回1
到这里,需要面临的也就是递归下降生成的最后问题了。( ̄- ̄)
问题3:形式控制之算符偏好与数量(递归深度)。
所谓偏好,就是指在生成的式子里某种运算出现的频率高低。而数量就是一道算式里出现的算符个数。
分析:首先来考虑偏好吧,回顾我们最初的需求是随机地产生混合运算题目,那么这个随机既代表了算数的随机也代表了算符的随机。基于上述讨论中的按算符拆分原则,每次拆解一个数的时候我们需要随机的选择一个算符(也就是本节点的算符),即每个节点都会为自己随机产生一个算符,最终的题目在逻辑上是这些节点连成的二叉树。如果在每次选择算符的时候引入概率,那么当生成很多道题目的时候在每种运算出现的频率上就可以体现出偏好了。以最简单的情况为例,如果算符选择的概率是一定的,我们之需要分装一个函数来实现选择就可以了。当然,为了让这个函数能参与更多的子程序编写,这里采用不定参数实现:
#include <cstdarg>
/* Random selection */
int PR_Select(unsigned int n=1,...) //实现n个数分别按概率P1,P2...Pn的随机选择
{
if(!n) return 0;
va_list ap;
va_start(ap,n);
float p=0.0f,*pn=new float[n];
if(!pn) return -1;
int i,j=0;
for(i=0;i<n;i++)
p+=pn[i]=(float)va_arg(ap,double);
if(p-1>=-1e-5&&p-1<=1e-5)
{
float pbt=((float)rand())/((float)RAND_MAX); //#define RAND_MAX 0x7fff 包含在stdlib.h
float ff;
for(i=0,ff=0.0f;i<n;ff+=pn[i],i++)
{
if(pbt>=ff&&pbt<ff+pn[i])
{
j=i+1;
break;
}
}
}
delete [] pn;
return j;
}
/* Random operator */
inline int OP_Select(float p1,float p2,float p3,float p4) //随机选择一个算符,概率分别为p1,p2,p3,p4
{
return PR_Select(4,p1,p2,p3,p4)-1;
}
最后,为什么需要考虑算符数量?先来考虑函数的递归;终止条件是递归函数必须具备的,在递归生成算式的时候,递归何时终止将直接影响所生成算式的长度。在逻辑上看,算符的数量决定了二叉树的非叶子节点个数,同时它又影响着叶子节点的个数。直观地,随着非叶子节点的增加,叶子节点的数量与非叶子节点的数量呈正相关,可见控制非叶子节点的数量即控制了总节点的数量,即算式的长度。假设用一个参数OP_num来表示算式中出现的算符个数,那么如何引入该参数呢?
分析:
设函数 void func(...) 为递归函数,它将实现生成一个算符数量为OP_num并且计算结果为num的算式。那么我们就确定了它需要OP_num和num作为参数。同时,因为是递归调用的,回顾问题2可知还需要一个供括号决策的Operator型参数Parent。
那么函数声明为:void func(Operator Parent,int num,OP_num);
显然参数Parent和num对递归深度无影响。
定义:OP_num=0 时,函数将直接打印num并返回;即表示不需要对num进行任何拆分。
当 OP_num>0 时,首先对num进行了一次拆分获得a,b,OP_num减一;当OP_num不为0就意味着a,b可以用OP_num(已经减一)个算符去拆分,此时我们可以将OP_num以加法拆分为两个数n1,n2(问题1所分装的函数中已表明对0的加法拆分依然是两个0),那么对a,b分别递归调用就可以了。最终分装函数如下:
/* probability of operators */
#define PO_A 0.35f
#define PO_S 0.35f
#define PO_M 0.2f
#define PO_D 0.1f
#include <iostream>
/* Output operator */
void OP_print(Operator OP)
{
switch(OP)
{
case 0: cout<<"+";break;
case 1: cout<<"-";break;
case 2: cout<<"×";break;
case 3: cout<<"÷";break;
}
}
/* Produce formula */
void FormulaGenerator(Operator Parent,int num,int OP_num)
{
if(!OP_num)
{
cout<<num;
return;
}
Operator here=OP_Select(PO_A,PO_S,PO_M,PO_D);
int a,b,OP_n1,OP_n2;
OP_num--;
OP_Decompose(here,a,b,num);
OP_Decompose(0,OP_n1,OP_n2,OP_num);
if(JUD_Priority(here,Parent)) //括号决策
{
cout<<"(";
FormulaGenerator(here,a,OP_n1);
OP_print(here);
FormulaGenerator(here,b,OP_n2);
cout<<")";
}
else
{
FormulaGenerator(here,a,OP_n1);
OP_print(here);
FormulaGenerator(here,b,OP_n2);
}
}
至此,整个生产算符就实现完毕了,如下是效果图:

可见这次程序相比上次对算式形式的控制更加精确,目前就这样吧,更多功能及优化将在后续版本实现。2016/3/14
软件工程学习之小学四则混合运算出题软件 Version 1.1 设计思路及感想的更多相关文章
- 软件工程学习之小学四则混合运算出题软件 Version 1.00 设计思路及感想
对于小学四则混合运算出题软件的设计,通过分析设计要求,我觉得为了这个软件在今后便于功能上的扩充,可以利用上学期所学习的<编译原理>一课中的LL1语法分析及制导翻译的算法来实现.这样做的好处 ...
- C语言编程学习:写的秒速计算四则混合运算项目
C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...
- MathExamV2.0四则混合运算计算题生成器
MathExamV2.0四则混合运算计算题生成器----211606360 丁培晖 211606343 杨宇潇 一.预估与实际 PSP2.1 Personal Software Process Sta ...
- 如何处理加括号的四则混合运算表达式——基于二叉树的实现(Eclipse平台 Java版)
记得上<数据结构>课程时,利用栈的特性解决过四则混合运算表达式.而如今在编写小型关系数据库的时候,编译部分要处理where后面的逻辑表达式——检查语法正确与否的同时,还要将信息传给下一个接 ...
- java 解析四则混合运算表达式并计算结果
package ch8; import java.util.LinkedList; import java.util.List; import java.util.Stack; /** * 四则混合运 ...
- 用C++实现的有理数(分数)四则混合运算计算器
实现目标 用C++实现下图所示的一个console程序: 其中: 1.加减乘除四种运算符号分别用+.-.*./表示, + 和 - 还分别用于表示正号和负号. 2.分数的分子和分母以符号 / 分隔. 3 ...
- web四则混合运算2
一.设计思路: 先出题(String型)(上周已经实现),再写方法计算结果,加入控制有无乘除法,范围,参与计算数,出题数,页码显示等简单功能,有无括号和分数的计算目前还没能实现. 二.代码: 界面 & ...
- web四则混合运算3
一.程序要求: 可以控制下列参数: 是否有乘除法: 是否有括号(最多可以支持十个数参与计算): 数值范围: 加减有无负数: 除法有无余数! 二.设计思路 要求能够通过参数来控制有无乘除法,加减有无 ...
- Web四则混合运算
一.代码1: <%@ page language="java" contentType="text/html; charset=UTF-8" pageEn ...
随机推荐
- Netty入门(八)构建Netty HTTP/HTTPS应用
HTTP/HTTPS 是最常见的一种协议,这节主要是看一下 Netty 提供的 ChannelHaandler. 一.HTTP Decoder,Encoder 和 Codec HTTP 是请求-响应模 ...
- php 微信公众号接入支付宝支付
真是无力吐槽这个需求了,好端端的非要在微信公众号接入支付宝,都知道微信公众号是拒绝支付宝的,屏蔽了支付宝,所以在微信公众号接入支付宝的话就必须手动复制链接跳出微信内置浏览器,强制性打开web浏览器完成 ...
- 随手练——P1141 01迷宫
1.暴力版 本质上就是求连通块数量,那么DFS或者BFS都行,暴力跑. 写完发现题目比较特殊,m次提问,那每次都暴力搜,肯定是要跑死了. #include <iostream> #incl ...
- Mysql千万级数据删除实操-企业案例
某天,在生产环节中,发现一个定时任务表,由于每次服务区查询这个表就会造成慢查询,给mysql服务器带来不少压力,经过分析,该表中绝对部分数据是垃圾数据 需要删除,约1050万行,由于缺乏处理大数据的额 ...
- JS兼容各个浏览器的本地图片上传即时预览效果
JS兼容各个浏览器的本地图片上传即时预览效果 很早以前 在工作曾经碰到这么一个需求,当时也是纠结了很久,也是google了很久,没有碰到合适的demo,今天特意研究了下这方面的的问题,所以也就做了个简 ...
- HDU1166
https://vjudge.net/contest/66989#problem/A C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了.A国在海岸线沿直 ...
- JS 01 变量_数据类型_分支循环_数组
点击直通车↓↓↓ 数据类型及数据类型的手动转换 数组 一.概念 JavaScript(JS)是一种基于对象和事件驱动.且可以与HTML标记语言混合使用的脚本语言,其编写的程序可以直接在浏览器中解释执 ...
- Android“寄生兽”漏洞技术分析
一.关于app的缓存代码 安卓的应用程序apk文件是zip压缩格式的文件,apk文件中包含的classes.dex文件相当于app的可执行文件,当app运行后系统会对classes.dex进行优化,生 ...
- Express app.listen 函数了解
最近一直在学习如何用原生的 Node.js 来做一个网站.在写的同时也在学习 Express 源码. 一直觉得 Express 开启服务器的方法挺有趣的,就看了一下. 在 Express 运行的时候会 ...
- Luogu P1972 [SDOI2009]HH的项链
很清新自然凶猛的数据结构题,都是套路啊 我们可以考虑离线做,先把区间按右端点从小到大排序 首先注意到一种贝壳如果在一段中出现超过1次,那么它在前面或后面就无关紧要了 举一个例子: 对于数列1 2 3 ...