2024 BUAA OO 第一单元

架构设计

第一次作业

预处理

Simplify类:

将得到的待化简表达式,消去所有的空格与制表符
去除数字的前置0;化简叠加的+-符号; 化简*+, ^+*, ^
由于在具体实现中, 我并未判断数据的正负性, 而是将读入的数字与变量统一当作系数为正处理, 因此在遇到诸如 -1(-x)的数据时, 会产生一些奇怪的错误。 在此基础上, 进行了一次简单粗暴的字符串替换, 即: 若式子开头、左括号之后跟随有负号时,加入前置数字0, 即改为 0-1, (0-x) 的形式

分析语法: 递归下降

Lexer类:

解析字符串。
特别地, 将"*-“解析为一个单独的符号, 其定义为在乘法运算时若出现奇数次, 对所得的多项式所有系数取反, 这样, 就无需在解析时考虑读入的数字可能有前导”-"的情况

Parse类:

根据式子性质, 解析为expr-term-factor
expr:分隔符号为±
term: 分隔符号为*^
factor: 无分隔符号

将括号里的式子定义为expr, 括号整体作为factor, 递归化简
在此次迭代中, 最多只有一层括号。 而此处的 Parser 已能递归分析多重括号嵌套的情况

多项式展开

∑ a ∗ x b \sum_{}^{} a*x^b axb

在Expr, Term, Number, Variable中重写toPoly()方法

Number/Variable: 根据解析所得字符串, 转化为数字/变量x
Terms: 将 factor.toPoly() 依照op的种类进行运算: “*”, 二者相乘; “^”, 幂次运算; “*-”, 仍为二者相乘, 只不过若其出现奇数次, 则对terms最终得到的list中所有系数进行取反
Expr: 将 terms.toPoly() 依照op的种类进行运算: “+”, 二者相加; “-”, 将后者系数取反后再相加

按上述规则进行表达式化简, 最终得到 Hashmap<Integer, BigInteger> monoList
其中 key(Integer) 为x的指数, 而 value(BigInteger) 为x的系数

Poly类: 多项式类,所有多项式相关的运算函数在此定义
addPoly()   // 加法
negate()    // 系数取反
mulPoly()   // 乘法
powPoly()   // 幂次, 规则为:读到^时将前置括号内表达式自我相乘后置系数次
insertList()    // 在加法时将元素并入最终返回的monoList

经toPoly方式自底向上进行多项式合并, 合并为一个poly类。 最后只需对解析所得的总多项式expr.toPoly()便可得到最终的poly

字符串输出

Processor类:

将expr.toPoly()得到的多项式转化为字符串, 因此定义一套规则专门用于输出

第二次作业

多项式的改变

由于指数函数exp的新增,将多项式改为如下形式
∑ a ∗ x b ∗ e x p ( p o l y ) \sum_{}^{} a*x^b*exp(poly) axbexp(poly)

新增Exponent类:

实现Factor接口
读入并存储指数函数, 实际形式为exp(expr)。 通过toPoly方法将其内的expr转化为poly多项式, 再以factor的形式参与到term的运算中。

新增Unit类:
  • 包含 BigInteger expX, HashMap<Poly, BigInteger> expMap
    将poly作为exp内式子, 将 BigInteger 作为exp的幂次, 计算时默认为1, 在化简时可提取系数。
    在此基础上,将原本monolist的key由Integer改为Unit, 故monolist为HashMap<Unit, BigInteger>
    注: 第二次迭代不保证指数在Integer范围内, 因此需要将所有指数改为BigInteger的形式

  • 在一开始, 为 HashMap<String, BigInteger> expMap。 但在exp参与运算、 后续合并同类项时存在问题, 如 x*exp(x+1) + x*exp(1+x) 时无法得到最简式。 因此将unit更新为目前的形式。

  • 重写方法
    用于应对运算时unit同类项的合并, 需要重写Unit中的equals与hashCode方法。 此外, 由于unit包含有HashMap<Poly, BigInteger> expMap, 因此同样也需重写Poly中的equals与hashCode方法, 以判断两unit的expMap是否一致

更新Poly中的运算方法
mulPoly()	// 对两unit中的expMap合并, 运用到mergeMaps方法。 及时删去其中系数为0的unit, 利于多项式合并
mergeMaps()	// 实现两个expMap的合并

当遇到 exp(poly) ^ num 的形式时, 将num乘到exp内, 即为 exp(num * poly)
当遇到 exp(poly1) * exp(poly2) 的形式时, 将二者合并, 即为 exp(poly1 + poly2)

其余改动

新增Func类:
  • 包含 HashMap<String, ArrayList<String>> paras, HashMap<String, String> funcs
    依次读入自定义函数, 经Simplify化简后将每行的形参以","分隔, 并通过ArrayList 储存下来, 同时将每行的表达式用String类型储存下来, 以自定义函数的字母"f, g, h"作为键值, 将形参与表达式添加到paras与func中。 在带入自定义函数到目标式子中的时候仅需递归操作字符串即可

  • 实现细节
    自定义函数表达式中含exp, 形参中含有x时, 会造成replaceAll误判。 因此, 将exp均替换为e。
    最终需要化简的式子中存在诸如f(exp(x), x^2)的形式时, replaceAll会将exp(x)中的x误判为需要替换的形参x。 因此, 将自定义函数的表达式与形参中的所有x, y, z替换为X, Y, Z。

更改Processor类:

新增expMap的输出

第三次作业

新增Derivative类:

实现求导功能。 先将dx(expr)内的expr通过toPoly()化简运算, 得到一多项式poly

  • Poly类:
    新增deriveList(), 对poly中的各项unit进行求导, 并返回多项式derivePoly作为求导结果
  • Unit类:
    新增deriveUnit(), 实现对unit的求导

再将 derivePoly * 原有系数 , 得到最终结果。 这样, 在Derivative类中, 仅需通过toPoly().deriveList(), 即可实现对dx(expr)的求导功能, 并返回一个多项式poly, 参与到后续的计算中

解决函数嵌套调用:

在读入函数表达式时, 因为是 “调用其他‘已定义的’函数” , 因此可将表达式在存入func中前进行一次展开, 使其转为符合第二次迭代要求的形式, 再存入func中

最终架构分析

UML类图

第三次迭代后的代码UML图如下, 采取PlantUML工具作图
在这里插入图片描述
递归下降的解析参考了oolens中给出的代码, 其足够应对第一次迭代。 而在后续的迭代中由于新增指数函数、自定义函数等, 对相应的Token、 Lexer、 Parse也有所修改
其中的Func为在表达式解析之前将含有自定义函数的部分通过字符串替换展开, 而不是放到Parse中再去展开。这里偷了一点懒

代码规模

在这里插入图片描述
每个类代码行数最多的是Processor, 这里有点超出预料, 但考虑到其制定了一系列输出规则、 优化性能, 倒也合情合理。
再就是Unit 和 Poly两大类, 在这里实现了表达式的化简与求导功能, 外加一些细节, 这里不过多赘述

指标计算

每个类的复杂度指标如下
在这里插入图片描述

每个类的依赖性指标如下在这里插入图片描述

一些思考

关于重构

三次作业尚未经历过重构。
唯一的一次较大更改出现在第二次迭代时将monolist由HashMap<Integer, BigInteger>改为HashMap<Unit, BigInteger>, 并重写Unit与Poly类中的equals与hashcode方法。

新迭代情景

在第二次迭代开始前, 本人一直在思考关于上一届新增sin与cos三角函数的解决方式。 以sin为例, 其可存放入 HashMap<Poly, BigInteger> sinMap 中, 其中, Poly为sin()括号中的表达式, 而BigInteger为sin函数的幂次。 cos同理, 这样就可以构建出最小单元unit

bug分析

三次强测均未出现问题
第二次互测时被hack一个点。 问题在于poly运算时未及时删去系数为0的unit, 导致在某种情况下其会输出, 继而WA

hack采用的策略

这里由于本人比较摆, 互测的时候一般都在干别的事, 因此并未着重发现他人代码的bug (也可能因为互测房同学的代码写的太好)。 最多的努力是把代码打包成jar包放到评测机中, 每次互测时拿到base的10分便不再多提交数据

分析自己进行的优化

第一次作业

将poly输出的第一项的系数尽可能变为正

第二次作业

判断exp(poly)是否需要再添加一层括号。 即若有暴露在括号外的±*运算符号, 则判断要新增一层括号。
特别地, exp(-num)无需新增括号
当exp(poly)中poly的每项系数的绝对值都相同时, 提取出它们的系数, 将形式改变为 exp(poly’) ^ num
未做: 提取公因式。 (大概是懒 + 怕超时吧)

第三次作业

课程发展方向

不要第一周就迭代
不要第一周就迭代
不要第一周就迭代

强测数据可再强一点, 时间、空间复杂度适当高些
第三周可再适当增加一些迭代任务

心得体会

经过三次迭代, 算是得到一个较为满意的结果。 俗话说万事开头难, 第一次迭代强迫我从假期的懒散中走出来。 从发布作业的周一茫然到周三, 直到周三晚才用oolens中给出的代码勉强搭建出一个框架, 再到后续的完善与debug, 可以说一周内成长了很多。
而第二周可以说是工作量最大的一周, 无论是Unit的扩展还是equals与hashcode的重写, 都让我收获颇丰; 虽然debug也确实繁琐, 最终互测还被hack了一个点, 但我认为第二周的迭代还是完成的较好的。
第三周的工作量少, 也很轻松, 在此便不过多赘述。

至此, 第一单元也落下帷幕。 最后,在此着重感谢学长 hyggge。 通过阅读他的github, 我在一开始就选择了一个可扩展性高的框架, 并从未经历过重构。 虽说很多细节与其不同, 但不可否认的是其确实在第一周的时候为我指明了方向。(虽然后面发现好多同学看的都是他的博客) 这里也标明一下出处: hyggge’s

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值