软件工程学习之小学四则混合运算出题软件 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 ...
随机推荐
- An Introduction to Protocol Oriented Programming in Swift
swift面向协议编程的根本原因在于值类型的存在:面向对象必须要有引用类型的支持: Protocol Oriented approach was introduced to resolve some ...
- 死磕nginx系列--使用upsync模块实现负载均衡
问题描述 nginx reload是有一定损耗的,如果你使用的是长连接的话,那么当reload nginx时长连接所有的worker进程会进行优雅退出,并当该worker进程上的所有连接都释放时,进程 ...
- Netty入门(三)构成部分
该节主要讲解 Netty 的构成部分. 一.Channel 它代表一个用于连接到实体如硬件设备.文件.网络套接字或程序组件,能够执行一个或多个不同的 I/O 操作的开放连接.可以比作传入和传出数据的传 ...
- PHP缓存锁原理及利用
原文链接:https://blog.csdn.net/tim_phper/article/details/54949404 概述: 项目当中经常要考虑数据高并发的情况,为了避免并发导致出现一些资源重复 ...
- Nginx端口占用问题
错误信息:nginx: [emerg] listen() to 0.0.0.0:80, backlog 511 failed (98: Address already in use) 主要是端口被占用 ...
- Excel操作
区间范围计算 方法一:用IF函数 方法二:构建一个辅助区域,用VLOOKUP函数 方法一:用IF函数 在F3中输入:=IF(E3>=90%,5%,IF(E3>=80%,4%,IF(E3&g ...
- stm32 boot0 boot1的启动方式
STM32三种启动模式对应的存储介质均是芯片内置的,它们是: 1)用户闪存 = 芯片内置的Flash. 2)SRAM = 芯片内置的RAM区,就是内存啦. 3)系统存储器 = 芯片内部一块特定的区域, ...
- 在nodeJS中操作文件系统(二)
在nodeJS中操作文件系统(二) 1. 移动文件或目录 在fs模块中,可以使用rename方法移动文件或目录,使用方法如下: fs.rename(oldPath,newPath,call ...
- jmeter数据库查询与接口返回进行对比
今天在群里又看到了一个小伙伴问类似的问题,[jmeter如何实现数据库查询出来的结果与接口返回的结果进行对比判断,或者数据库两字段的相加减与接口返回进行对比].其实都一样,因为你把运算放在查询那里就行 ...
- kubernetes 禁用虚拟内存 swapoff -a ----- 顺便复习sed 命令
1.如果不关闭swap,就会在kubeadm初始化Kubernetes的时候报错,如下图: [ERROR Swap]: running with swap on is not supported. P ...