第一单元总结
概述
第一单元的主题是多项式求导,共三次进阶性作业
- 第一次:简单多项式求导
- 第二次:带简单正余弦函数与带乘积项的求导
- 第三次:带嵌套函数的求导
在本次作业中,我学习应用了继承,重写,多态,抽象类,接口等面向对象的知识点,并学习实现了工厂模式等设计模式,多次对作业代码进行迭代和重构。下面我将结合代码分析工具来分析我的作业代码。
第一次作业
解题思路
由于本次作业保证输入数据全部为格式正确的表达式,不需要判断空格位置是否合法,所以可以在读入一行后直接使用trim
与split
的组合技再将字符串拼接起来得到表达式,再用一个大正则对单项式进行匹配(如下图),将匹配到的字符串传入单项式的构造方法。单项式具有coefficient
、degree
和symbol
三个参数,利用单项式内的正则表达式对其进行匹配完成构造。对于符号的判断构造了setpositiveOrnot
方法使得符号在degree
和symbol
中以(BigInteger)和(String)的方式储存,对于正系数可以在求导时直接输出符号。在(Polynomial)中我以TreeMap<BigInteger, Monomial>
的数据结构存储单项式,以指数为key值,每加入一个单项式就判断其指数来合并同类项。求导时我对不同的次数进行判断并分类讨论,在differential
函数中直接输出,这里导致了一个巨大的bug,在bug分析与修复中我将重点讨论。
Bug分析与修复前的废话
写完这次作业后,我感觉OO没有听闻的那么变态,于是便自我膨胀 松懈了起来,通过中测后便将其束之高阁,既没有阅读公测互测规则,也没有仔细测试数据,完全将OO抛之脑后。开放互测之后我就懵逼了,为什么我迟迟没有分到房间??难道我的水平已经完全不用和芸芸众生平起平坐勾心斗角谈笑风生了吗?抱着略带骄傲的疑惑的心情我询问了助教大大,他也第一次遇到这种情况,在一番探索之后,他恍然大雾:害,你分数没够互测!第一次作业就gg的人他也是第一次见......我当时:o_o 。水平的确是完全不用和芸芸众生平起平坐勾心斗角谈笑风生啊,毕竟是远低于平均水平的存在......
Bug分析与修复
在正确地认识到自己的差距后,我打破了此前闭关锁国的政策,翻看了讨论区一些同学提供的测试数据,发现了一个巨大的问题。在(Monomial)类中我并没有写求导方法,而是直接在(Polynomial)中输出的,即以符号→指数乘以系数→(*x**)→指数减一的顺序输出的,但是我没有考虑到指数乘以系数是会改变导数系数的符号的(多么SB的问题),这就导致了符号输出重复或错误的问题。于是在(Monomial)类中添加了monoDifferential()
方法,其返回一个单项式,再在多项式类中输出,最终得到了正确的答案。
程序结构与复杂度分析
Complexity metrics | 周五 | 20 3月 2020 20:27:04 GMT+08:00 | |
---|---|---|---|
Method | ev(G) | iv(G) | v(G) |
MainClass.main(String[]) | 1 | 1 | 2 |
Monomial.Monomial() | 1 | 1 | 1 |
Monomial.Monomial(String) | 1 | 6 | 6 |
Monomial.getCoefficient() | 1 | 1 | 1 |
Monomial.getDegree() | 1 | 1 | 1 |
Monomial.getSymbol() | 1 | 1 | 1 |
Monomial.monoDifferential() | 1 | 1 | 1 |
Monomial.monoPlus(Monomial) | 1 | 2 | 2 |
Monomial.setCoefficient(BigInteger) | 1 | 1 | 1 |
Monomial.setDegree(BigInteger) | 1 | 1 | 1 |
Monomial.setSymbol() | 1 | 2 | 2 |
Monomial.setSymbol(String) | 1 | 1 | 1 |
Monomial.setpositiveOrnot(String) | 2 | 2 | 3 |
Polynomial.differential() | 3 | 9 | 9 |
Polynomial.setPoly(String) | 1 | 4 | 4 |
null.compare(BigInteger,BigInteger) | 1 | 1 | 1 |
Class | OCavg | WMC | |
MainClass | 2 | 2 | |
Monomial | 1.75 | 21 | |
Polynomial | 3.67 | 11 | |
Module | v(G)avg | v(G)tot | |
Unit1 | 2.31 | 37 |
本次作业结构简单,没有太高的复杂度,在Bug修复以后耦合度有所下降,但是整体还不是特别简洁而且拓展性极差,是为了降低编码难度的导致的困难。
第二次作业
解题思路
第二次作业中有三种函数并支持连乘。输入数据的结构可以拆解为:(Polynomial)(表达式)由(Monomial)(单项式)构成,由加减号分割;(Monomial)中有三种因子(sin
、cos
和power
),分别对应三个指数sinDegree
、cosDegree
和powerDegree
,并且自带系数degree
和符号symbol
(与上次相同),用数学语言描述就是(ax^{b}sin^{c}(x)cos^d(x))。因此对于单项式的存储方式依然可以沿用上次的TreeMap
,只不过我将键值改为了将三个指数链接在一起的String
,这样便可以对三种指数相同而系数不同的单项式合并同类项。
这次同样可以用大正则拆解表达式,同时可以判断出错误的输入抛出异常InputFormatException
。由于此次作业式子的结构依然不算复杂,我并没有单独为三种因子建立独立的类,但是为了降低单项式构造方法的复杂度,我建立了因子类(Variable)专门用于解析因子的指数。
由于(Monomial)格式固定,单项式求导可以直接返回三个单项式 (abx^{b-1}sin^{c}(x)cos^d(x)) 、(acx^{b}sin^{c-1}(x)cos^{d+1}(x)) 、(-adx^{b}sin^{c+1}(x)cos^{d-1}(x))。在输出时选择性打印。
性能优化
这次作业有很大的优化空间,在输入阶段我只对单项式进行了合并同类项,但是在求导阶段对于下面几种情况进行了三角函数的合并化简:
情况①
对于同正负(不同符号合并反而会导致合并后表达式变长)、同幂函数指数的单项式,如果出现上诉情况说明式中可以提取(sin^2(x)+cos^2(x))公因式,化简为:
如果有多个式子符合则优先选择三角函数指数值为0或1的,这样可以得到最简的表达式。当然,如果出现两个单项式的系数相同的情况,可相消只剩下一项。这种化简方式对于(xsin^6(x)+3xsin^4(x)cos^2(x)+3xsin^2(x)cos^4(x)+xcos^6(x))这种情况求导可以化简为1(不论是输入的时候化简还是输出的时候化简都行)。
情况②
遇到这种情况可以提取出公因式(sin^2(x)-cos^2(x)),化简为(1-2cos^2(x))。
程序结构与复杂度分析
从上面可以看出,我有代码量庞大的优化部分,光优化部分的代码量就接近总码量的一半,这大大提高了代码中出现bug的几率,而这次正是优化导致了我作业的所有bug。关于优化的讨论我放在了本文最后。下面是类图和复杂度分析:
Complexity metrics | 周五 | 20 3月 2020 21:36:49 GMT+08:00 | |
---|---|---|---|
Method | ev(G) | iv(G) | v(G) |
Total | 56.0 | 97.0 | 108.0 |
Average | 1.8666666666666667 | 3.2333333333333334 | 3.6 |
Class | OCavg | WMC |
---|---|---|
InputFormatException | 1 | 1 |
MainClass | 2 | 2 |
Monomial | 2.17 | 39 |
Polynomial | 5.11 | 46 |
Variable | 1.5 | 3 |
可以看出我的方法很多,而复杂度也较高,但最主要的问题是出现了多重嵌套的for、while语句以及判断条件很长的if语句,以后应该极力避免这种情况,简化自己的逻辑或者封装方法来化简。
Bug分析与修复
如前所述,这次带给我bug的是优化部分,我没有严格按照上面分析的情况来判断,将情况简化成了((a_2−b_2)*(a_3−b_3)=-4),导致出现了(a_2−b_2=-1)的情况,修复十分简单,不再赘述。
代码测试(互测与自测)
这次想着数据不会太难,也没设计自动化测试工具,莽就完事了,把上次错的和讨论区里的数据拿来当检测数据,没想到还真找到了自己的bug,一找一个准(我是只会写bug的屑)。在一番“充足的”自我检查后提交了作业(没想到还是有相当关键的错误)。在互测阶段我依然使用我那几个老物件,没想到啊,扔了两组数据就中了四个点,可带劲儿了~ 不过我逐渐意识到我肯定是在C屋无疑了(nmd wsm
第三次作业
解题思路
本次作业难度有了质的提升,出现了嵌套结构以及非法的空格位置,直接用一个大正则判断表达式格式的合法性的方法失效。
对于嵌套中最外层括号的判断,受到之前的博文的启发,用堆栈的方法对表达式进行括号匹配,如果遇到最外层的括号,将其替换为其它的字符,例如E
,则可以用E([^E]+)E
来匹配带有最外层的单项式或因子。
对于格式判断,我最初使用的是一层层的嵌套,一边初始化一边判断,进入内层表达式后再判断其合法性,不合法直接抛出错误,但是最初的算法完全没有考虑时间复杂度,导致了后面在互测、强测数据中出现了大量会超时的测试点,同时如果不去除空格直接去判断分解表达式会使得正则十分复杂。Bug修复后,我新建了FormatCheck
类,专门对输入进行格式检测,虽然大致原理依然是一层层地解开嵌套,但是变成了在同一个类的不同方法之间跳转(对于不同类之间的嵌套,为了减少初始化实例带来的时间开销也可以使用这种方式),导致对WRONG FORMAT
的判断速度提升地非常快,不用一层层地向下递归,并且在判断完后可以直接去掉表达式中的所有空格并把+++
等等多重符号替换为简单的正负号。
程序的结构部分我会在后文中单独说明,因为本次作业出现的主要问题就是结构的设计过于庞大,有很多冗余。
对于面向对象的运用,我使用了抽象类与多重继承,同时在对象实例化时采用了工厂模式。
在互测或强测中被找到的bug:
①TLE
是出现的主要问题,主要有三种情况会出现超时:
以及他们的结合体:
②过度优化
例如:
③细节问题
BUG修复与重构:
情况①
对于TLE问题,我最初认为是源于我过度的优化,我对求导结果再一次读入使得结果合并同类项以优化结果的长度,在注释掉所有优化代码后(比如合并同类相,第二次读入以及连乘数字的合并),我发现并没有解决TLE问题,和同屋其他代码相比运行时间在数量级上有明显的体现,不过解决掉了[情况②](# 情况②)。
问题出在了递归深度过大!于是我(被迫)开启了我的重构之旅/(ㄒoㄒ)/~~
首先应当简化读入的公式,对其去括号以减少递归的次数。
公式(alpha):
对于公式(alpha),可以理解为(Polynomial)中只有一个(Monomial),而这个(Monomial)中只有一个(Polynomial),因此可以去掉一次括号,以此为依据则可以将每个多项式中重复嵌套的括号去除。用一个循环结构即可实现。
公式(eta):
对于公式(eta),其实与上述问题类似,也即一个(Monomial)中只有一个(Polynomial),但是具体实现起来不容易与上面的情况合并,所以单独解决,也是循环结构实现。
公式(gamma):
我原本的结构是:(Polynomial)包含一个名为Poly
的ArrayList<Monomial>
。(Polynomial)类以加减号为间隔读入每一个单项式并进行存储,而每一个(Monomial)中包含一个名为Factors
的ArrayList<Factor>
,(Factor)可以是各种因子(变量、(Polynomial)和只有系数的(Monomial))对于三角函数因子其内嵌的因子则可以是上述的所有类型。这样造成了一个问题:会产生许多只有一个因子的(Monomial)和(Polynomial),造成工作量繁重的初始化实例,并增加嵌套的层数。因此针对这种情况,我将(Poly)的结构改为了ArrayList<Factor>
。如果一个单项式中只有一个因子那么则将在工厂中初始化此具体的因子而不用通过单项式这一层。这样大大减少了初始化实例带来的开销。同时我发现求导过程中可以不用对三角函数化为三个项的乘积,例如(asin(innnerFactor)^{a-1}cos(innnerFactor)*(innnerFactor)'),如果内部是数字或(x),可以减少一项,指数为一也可以减少一项。
最后的遗憾,公式(delta):
最后,我还是有两个互测点没有通过,可以对他们特殊情况特殊处理,比如对于只有幂函数的多项式可以直接用堆栈化简,但是一是在BUG修复上花费的时间太多了,快赶上做的时间消耗了(◎﹏◎);二是这次的主要问题还是最开始做的时候程序的架构设计的太庞大而拙劣,后期改起来太麻烦,如果一开始采用二叉树对多项式化简,只对三角函数内的因子嵌套或许就不存在超时问题。当然许多大佬的代码优化又精良,结构又巧妙,代码风格工整系统,速度还快,他们的数据结构肯定是值得学习的,不过上千行的代码实在太难啃了,希望以后能看到大佬解说自己的优秀代码(★ ω ★)。
情况②
如上所诉,在我对输入进行第二次读入时忽略了结果可能是非法输入的情况。当输入(x^{-50})时求导结果为(-50*x^{-51})那么再次输入时输出为WRONG FORMAT
。此问题将优化删去即可。
情况③
因为主要是之前优化中去括号的特判不完善,对于许多情况不适用,所以将这些代码注释掉便解决问题。
程序结构与复杂度分析
从上文(是我在bug修复里写的,写得比较完整就摘过来了)可以看出我本来的程序结构是多层且不能跨层创建的,如下图:
改进后,(Polynomial)可以实现对(Factor)垂直管理:
Complexity metrics | 周五 | 20 3月 2020 21:39:49 GMT+08:00 | |
---|---|---|---|
Method | ev(G) | iv(G) | v(G) |
CosFactor.differential() | 2 | 5 | 5 |
CosFactor.equals(Object) | 5 | 2 | 5 |
FactorFactory.getFactory(String) | 5 | 6 | 6 |
FormatCheck.TrigFormatCheck(String) | 5 | 6 | 7 |
FormatCheck.expFormatCheck(String) | 4 | 7 | 9 |
InputParser.parse(String) | 5 | 8 | 9 |
Monomial.equals(Object) | 5 | 2 | 5 |
Monomial.toString() | 5 | 7 | 10 |
Polynomial.Polynomial(String) | 1 | 13 | 13 |
Polynomial.equals(Object) | 5 | 2 | 5 |
Polynomial.toString() | 9 | 13 | 18 |
SinFactor.differential() | 2 | 6 | 6 |
SinFactor.equals(Object) | 5 | 2 | 5 |
Total | 123 | 141 | 179 |
Average | 2.120689655 | 2.431034483 | 3.086206897 |
Class | OCavg | WMC |
---|---|---|
CosFactor | 2.67 | 16 |
Factor | 1 | 2 |
FactorFactory | 6 | 6 |
FormatCheck | 6.5 | 13 |
InputFormatException | 1 | 1 |
InputParser | 9 | 9 |
MainClass | 1 | 1 |
Monomial | 2.58 | 31 |
Polynomial | 4.62 | 37 |
PowerFactor | 2 | 12 |
Simplifier | 3 | 3 |
SinFactor | 2.67 | 16 |
TrigFuncFactor | 1.5 | 6 |
VariableFactor | 1.57 | 11 |
Package | v(G)avg | v(G)tot |
3.09 | 179 |
CosFactor.java | 67 |
---|---|
Factor.java | 9 |
FactorFactory.java | 35 |
FormatCheck.java | 66 |
InputFormatException.java | 5 |
InputParser.java | 37 |
MainClass.java | 19 |
Monomial.java | 131 |
Polynomial.java | 173 |
PowerFactor.java | 53 |
Simplifier.java | 30 |
SinFactor.java | 67 |
TrigFuncFactor.java | 34 |
VariableFactor.java | 46 |
代码测试
这次构建了自动化测试工具(不过是在强测结束以后,自己没来得急检查/(ㄒoㄒ)/~~),具体使用了python中的Xeger
函数,根据正则表达式随机生成相应的字符串。然后利用管道编译java文件对其输入输出,最后利用sympy
中的求导函数diff
对正则表达式生成的数据求导,最后按照评测机的测评方法,比对python求导后的值与程序跑出的结果,验证程序的正确性。不过测试出别人bug的数据都是手动撸出来的...
总结
第一单元的三次作业逐渐把我从寒假日夜颠倒的娱乐放松状态拉回到日夜编码的现实(OO)世界,让我领略到人间疾苦,流下了真实之泪,圣杯战争中的勾心斗角仍历历在目、深入骨髓。
不过可喜可贺的是我每一次都在进步,不断完善了自己对面向对象的理解,并且逐渐认识到程序有一个清晰而逻辑直接的结构的重要性。不断用面向对象的思维来武装自己的程序,就算在比较大型的项目中也能把握程序的脉络。
让我感触较深的还有对优化的理解,优化过多往往不是一件好事,特别是优化分只有二十分的情况下,大量的优化只能是扣分项,如果三次我都没做任何优化(即仅仅将这部分代码删去),我的分数都能好看很多,第三次中几乎所有点的数据都很随机,基本上能化简的很少。所以我认为首先应该重视程序的正确性,完成充分的测试后,在保证正确性的条件下,再来优化,这才是可取的,并且在设计程序之初就应该为后面的拓展性、复杂度考虑,避免一而再再而三的重构!