OO 2022 第一单元(表达式变换)思路分析

去年的作业要求是求导,今年题目换成拆括号+内联自定义函数了。

补充一种今年想到的基于表达式树的表达式存储架构(去年本人的作业采用的是语法分析树,比今年想到的这个表达式树要复杂一些)。

架构示意

节点图例

存储示例

在此表达式树存储的基础上, 我们可以很方便地进行拆括号与化简操作。

在进入正题之前,首先补充一些基本设定:

  • 在这个表达式树中是不存在减法的。如果要存储 a-b,则以 a + -1*b 的形式存储,为减数 b 添加一个系数 -1
  • 每一种树节点均应实现 equalshashCode 这两个方法,其作用主要有以下两点:
    • 判断两个项是否是同类项,需要比较其非系数部分是否完全相等
    • 能够使表达式树对象被装入 HashSet 或者作为 HashMap 的 Key。
  • 加法节点代表累加,乘法节点代表累乘,这两种节点内部都需要用容器来管理其子节点。
    • 相比于有序的容器 List,本人更加倾向使用无序的容器 SetMap(例如 HashSet),因为加法和乘法都满足交换律,顺序不重要。
    • 使用 HashSet 存储子节点时,同一个累加运算下不能出现两个完全相同的项,如出现了,则将即将加入的项乘以 2 加入,并从 HashSet 中删除原有的旧元素。
      例如 x+x,当准备添加第二个 x 时,HashSet 中已经有了一个 x,此时将旧的 x 删掉,并创建一个 2*x 加入 HashSet 中。对于乘法同理,sin(x)*sin(x) 将以 sin(x)**2 存储。但 x+2*x 可以存在,因为 x2*x 是两个不同的项,可以在同一个加法运算节点的 HashSet 下共存。

括号拆解

首先说拆括号,在暂不考虑函数的前提下,必须拆括号的情形主要有以下几种:

  • (a+b)+c, 即一个累加节点的子节点是累加节点。解决方案: 这种表达式树的累加和累乘节点均有多个子节点,其内部用容器来管理子节点(推荐用 SetMap 而不是 List,因为加法和乘法满足交换律,顺序不重要)。我们只需让累加和累乘节点实现 合并 操作(类似于 Java 容器的 addAll 方法),将 ab 并到上层,也就的到了 a+b+c
  • (a*b)*c, 即累乘节点有子节点是累乘。解决方案同上。
  • (a+b)*c, 即累乘节点有子节点是累加。解决方案: 这时需要利用乘法分配律,循环遍历累加节点下的子节点,并且对累加运算的每个项,复制一份累乘节点下其他的因子并与之相乘,最后将这些相乘的结果相加,即得到 a*b + a*c
  • (...)**n, 乘幂节点的底数是累加或累乘。解决方案: 与乘法分配律类似,多次利用乘法分配律。

化简

接下来讨论化简。主要考虑的化简是合并同类项以及乘幂的折叠等。

在开始化简之前,我们需要做这样的一个准备工作: 为表达式树的每种节点实现 equalshashCode 方法。这两个方法的含义不在此赘述,只需要确保实现之后,我们可以判断两棵树是相等的(例如两个 x*sin(x)*cos(x**2) 相等,更进一步, x**2+2*x+11+x**2+x*2 这两棵树也应该是相等的,因为加法和乘法顺序不重要)

有了判断两棵树相等的能力(以及将树存入 HashSet 或者作为 HashMap 的 key 的能力)后,就可以正式开始实现化简操作了。这里以合并同类项距离,乘幂折叠的操作与合并同类项相似。

同类项合并

例如 2*sin(x**2)*x**3 + 3*x**3*sin(x**2) 应化简为 5*sin(x**2)*x**3。所谓同类项,也就是除了常数的系数以外,其余含有字母的部分是相同的。

首先,我们需要能够判断出来什么样的两个表达式是同类项,也就是将任何一棵表达式树表示成 系数 * 其他部分 的形式。因此,我们可以对每一种表达式树节点实现 查询系数查询非系数部分 两个方法(注意我们并不需要真正把系数存下来,只需要能查询即可),其中 查询系数 返回一个整数,而 查询非系数部分 返回一棵表达式树。

  • 累加节点: 系数为 1,非系数部分为自身。例如 a+b 等价于 1*(a+b)。注意,两个累加节点不可以合并同类项,否则会产生新的括号。但是可以将它们合并到一起。例如 a+b + a+b 合并为 a+b+a+b
  • 累乘节点: 遍历存储其子节点的容器, 将所有的常数节点提取出来,计算其乘积作为系数。而剩下的节点用一个新的累乘号连接起来就是非系数部分。
  • 常数节点: 系数是自身, 非系数部分无。
  • 三角函数、变量等: 系数为 1,非系数部分为自身。

对于 Java 中遍历容器的操作,可以尝试采用 stream 简化代码。

实现了以上两个查询方法后,我们就可以对一个累加节点的子节点们进行合并同类项:

  • 创建一个 HashMap<Node, Integer> ,Key 为代表非系数部分的表达式树,Value 为其对应的系数之和。
  • 依次遍历累加节点的每个子节点, 分离其系数和非系数部分,并更新到上述 HashMap 中(类似于单词计数)。
  • 最后遍历上述 HashMap,将每一组 系数 * 非系数部分 用一个新的累加节点连接起来,得到合并同类项后的结果。
    特殊系数的处理:
    • 系数为 0: 直接丢弃该项
    • 系数为 1: 直接保留 非系数部分 而无需添加多余的乘法。

乘幂折叠

乘幂折叠与合并同类项在操作上相近,只不过操作是在累乘节点上进行,将底数相同的因子合并,指数相加。

输出

最后是表达式树的输出,对每种节点实现 toString 方法,递归遍历输出即可。在输出时可以针对特殊系数(例如 0, 1, -1 等)以及 x**2 -> x*x 等特殊情形进行特判以减少输出长度。实现累加节点的 toString 方法(也就是输出若干项之和)时,如存在至少一个正系数的项,可将该正项最先输出从而少输出一个 - 号。

自定义函数

另外,今年的作业中新增了自定义函数,先输入函数定义然后需要把表达式中的函数调用展开。这里也提供一种基于上述表达式树的参考思路:

自定义函数处理

posted @ 2022-09-02 17:46  Dong_HY  阅读(79)  评论(0编辑  收藏  举报