关于四则运算的一些想法

Posted on 2024-03-19 12:51  onezhan  阅读(24)  评论(0编辑  收藏  举报

表达式树

假设 E 表示有一个运算符(+-*/)的表达式,I 表示一个整数或分数

那这个表达式肯定有左操作数和右操作数,左右操作数本身可能是 E 或者 I

那么最终的表达式可以表示成一颗表达式树,就像:

通过一定方式,生成一个随机的表达式树,我们就可以从根表达式中获得一个随机的表达式。

大概就是这个思路,不过对象的行为还需要仔细考虑

一些需求的完成方法参考

(1)生成的题目不能重复

最简单的方法就是结果不能重复,这样不管怎么变换都不会一样。更复杂的没想到

(2)计算过程不能产生负数

SubExpression 对象自身保证结果不为负数就可以了,或者专门搞个子类,这个子类保证运算结果不为负数

(3)分数的处理

设置一个分数对象,专门处理分数。但这样可能会导致算法中,数据处理的不统一(可能有整数,也可能有分数)。我觉得可以整数和分数都可以看作是分数,只不过整数分母为1,输出的时候要注意就行了。

了解表达式树生成表达式的原理

我们先了解一下表达式树节点的究竟是什么,才能很好地去了解由 Expression 组成的表达式树

先来看这个最简单的模型,它只有三个属性,分别是:

  1. 指向左表达式的 leftExpression
  2. 指向右表达式的 rightExpression
  3. 这棵表达式树所要表示的表达式 exp

还有一个方法:getExp() 其实就是返回一个 exp

我们考虑最简单的一颗树,只有一个节点的树,也就是只有一个操作数

只有一个节点,很显然它本身就是一个表达式,这就是我们的表达式树里的叶子节点

而对于叶子节点来说,getExp() 其实就是返回它自身的数的字符形式

exp = toString(数)

我们再来看有两个操作数的情况:

两个操作数之间一定会有一个操作符 op 连接起来

那这棵树的表达式要怎么得来呢?

很简单,就是依靠 op 将左右子树的表达式拼接起来嘛!

所以最终的结果就是 exp =leftExpression.getExp() + "op" + rightExpression.getExp()

AddExpressionSubExpressionMulExpressionDivExpression 说到底就是中间连接的操作符 op 不同罢了(当然还有一些算法上的差异,只不过为了了解表达式树生成表达式的本质就不细说了)。

说白了,一颗表达式树要想拿到它本身的表达式,必须先通过子树获取组成部分,才能拼接起自身。

如果子树不是叶子节点,就会继续获取子树的子树的表达式。

本质上,是一种递归的方式,得到我们的表达式树的最终表达式

好了,相信你已经懂得了表达式树生成表达式的基本原理的,非常简单对吧,就是像遍历一颗普通的树那样。


而括号的添加其实也是可以有随机,比如上边的 (1+2)+(3+4)

我们从根节点的视角来看:我发现了左右子树都是 + 法,其实我自己跟这个 + 法是优先级相同的操作符,我往子树返回的表达上外边加不加括号是无所谓的,于是我将决定权交给上天(我们设定的算法),让上天决定加不加。1+2+3+4(1+2)+3+41+2+(3+4) 理论上都是能随机出来的。

这样就实现了同级间括号的随机添加。

如果根节点是 * 那情况就截然不同了,根节点发现 + 法比自己的 * 法低一级,为了保证运算结果的正确性,这个括号是必须得加的,也就是说必然是这样的形式 (1+2)*(3+4)

把上述内容添加到 getExp() 的实现中,就可以实现括号在必要时的添加,以及在不必要时的随机添加。

理论上是非常简单的,实现上不好说,需要一定的尝试。

表达式结果的计算

知道了表达式是怎么生成的,其实结果的计算也就是顺便的。

我们可以设置一个 getResult() 方法,让表达式树递归调用这个方法,最终落实到叶子节点的时候就返回一个数字结果(考虑到数字的统一性,我们可能还需要一个分数类来代表数字)。

举个例子吧:

还是这幅图,表达式树想要获得结果

根节点对左子树和右子树调用 leftEexpression.getResult() + rightExpression.getResult()

这样就可以获得结果。不用担心子树中没有结果,因为子树会递归地把 getResult() 这个任务交给子子树,直到叶子节点返回了真正的数据,然后回溯,得到最终结果。


于是,经过一番深思熟虑,我把 Expression 的层次结构做出了这样的改变:

表达式重复的解决

所谓的重复请参考:

程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。

例如,

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

先来看最简单的例子:

4+55+4

它们两最本质的区别是什么?我觉得是位置的不同。什么意思呢?

假设我们有两个数 \(a_1,a_2\) ,并且有一个操作符 + 号,要求在 + 号附近插入 \(a_1,a_2\),求有多少种插入方法?

这是一个很简单的排列问题,

答案是:\(A_2^2=2\)

我们继续,如果有\(a_1,a_2,a_3\),并且有两个 + 号时,又有多少种排列方法?

答案是:\(A_3^3=6\)

那如果是 \(a_1,a_2,a_3,a_4\),三个 + 号呢?

答案是:\(A_4^4=24\)

说的这么多,感觉都没有说到点上,这些排列数能说明什么?

其实我真正想说的是:\(a_1,a_3,...,a_n\)这些数排列的不同,会导致我们最后生成的表达式树的不同,即使它们数学意义上是相同的。

生成的表达式树都不同了,那即使我们自己能拍段出两颗数的数学意义上相同,计算机却无法通过单纯的比较判断两棵树是否相同。

那就很让人头大了啊,假设我们已经生成了一颗表达式树,后面由生成了一颗数学意义上相同的树,但却因为节点的不同,导致比较不出来,两棵树是相同的。这就使得表达式树的查重变得非常困难。

因此,应该想办法让数学意义上相同的树,全都生成同一棵树,这样我们才能做查重。

不然,我是真的不知道要怎么搞。


我们从一个简单的例子开始吧

接下来解释一下我的表达式树的生成算法:

一开始,我会准备两个队列:

  1. Expression队列,用于存储随机生成的LeafExpression,方便后续使用
  2. 操作符队列,有 n 个操作数,就会生成 n-1 个随机的操作符

假设一开始两个队列的情况是这样的:

image

我的算法是这样的:

只要 Expression 队列的长度大于或等于2,就在该队列中取出两个 Expression

然后在 操作符 队列中取出一个运算符,将取出的两个 Expression 连接成树的结构

最后将这个新的表达式树加入 Expression 队列中。

直到 Expression 队列的长度为 1,循环结束,Expression 队列中的最后一个表达式就是最终的结果。

以上面的两个队列为例,我来进行一次算法的演示:

正如我上面所说的,队列中最后的 Expression 就是最终的结果,对应的表达式是 2+3+1

那么现在有一个问题,如果 Expression 的元素的数据不变,但是顺序改变一下,就会生成一棵数学意义上相同,但计算机难以比较的树,还是给出图片才能更清楚地认识:

你可能会想:不就是节点对换了一下吗,我比较两棵树的时候考虑这种情况不就行了?

No,No,No。请你考虑一下这样的情况。

我想说的是,3个操作数,它因为位置不同而形成的表达式树,有 \(A_3^3=6\)种,其中只有一种是可以通过对换来判断相等的,也就是说还有 4 种情况无法囊括!

这样看的话,如果我们忽略掉数字排列的影响,我们的查重算法是很难有较好的查重效果的。

因此,我准备了一种策略,让树在生成时,就考虑到数字排列的影响,并尽最大努力排除这些影响(我不敢说完全排除)。


解决排列问题

假设我们发现了,操作符队列中,有两个连续的 + 号,那我们就先取出 Expression 的三个表达式,并根据某种优先级算法(还没定好,但感觉能行),不管原本队列的排列如何,我们都将这些 Expression 按照优先级,从大到小,或者从小到大依次排好。

我们以单纯的 LeafExpression 为例子,来看看优先级是怎么起作用的:

当然,也可以使用升序123,我这里使用降序321是考虑到这样做可能会一定程度上照顾一下减法。

上述的算法对于连续的 * 号也是一样的道理。


上面讨论的都是连续的 +* 号,如果出现 + * 混合,又该怎么解决。

要解决问题,首先要了解问题出现在哪里?

混合的情况也是分很多种的,先来看一个 +* 的混合

在这里,也会应用到叶子节点的优先级

比如:我们有这样的初始队列:

操作符队列中,是*+还是 +*,对结论没有影响

下面的这棵树,在我们的算法层面里是不存在的(至少是三个运算符之内不存在),对于两个运算符的,一个是 + 一个是 * 的。根节点的两颗子树,一棵是叶子,另一棵是一个表达式,只需要保证表达式固定在右边(当然也可以左边),并且子表达式的叶子要按优先级排列,就可以得到唯一的表达式了。

我们可以这样理解,不管是 + 号 还是 * 号,它们都可以随意调换子树的顺序,我们所要做的就是固定好一个顺序,让它们不能随意排列,从而达到数学意义和计算机意义的统一。


难题在于,三个操作符时出现的这样一种情况:4*3+6*2 等价于 6*2+4*3

生成的表达式树是这样的:

我总结出一套方法(没有通过什么严谨的证明,只是单纯觉得这样能行):

(1)如果一棵表达式树的左子树和右子树,都是叶子,那数值大的叶子排左边,数值小的叶子排右边;若叶子的数值相等,不用管。

(2)如果一棵表达式树的左子树和右子树,一棵是表达式树,一棵是叶子,那表达式子树放在右边。

(3)如果一棵表达式树的左子树和右子树,都是表达式树

  1. 若子树有一颗是 + ,另一棵是 *,那 * 的放左边,+ 的放右边
  2. 若子树都是 + 或者都是 *,那么:
    1. 首先对比两颗子树的 result ,大的在左,小的在右
    2. result 相同,则对比两颗子树的最大叶子 maxLeaf,大的在左,小的在右
    3. maxLeaf 相同,说明这两个表达式相同,位置换不换无所谓

我们用上述方法,用人脑模拟一下电脑的运行:

上面的算法究竟能做到多少精度我不知道,但对于我们现在的作业来说,应该是完全足够的存在了。


综上,为 Expression 添加一下特性:

  1. maxLeaf,一个数值,用于记录表达式树的最大叶子值
  2. opType,指示子树的根的操作符类型,便于算法使用

Copyright © 2024 onezhan
Powered by .NET 8.0 on Kubernetes