BUAA_OO_2024_第一单元总结

BUAA_OO_2024_第一单元总结


前言

OO第一单元作业主题为表达式化简,主要学习目标为熟悉面向对象思想。在本单元三次作业中,运用递归下降法让我初步感受到面向对象中不同类各司其职、各尽其责的功能。如今,第一单元的任务已经告一段落,我想在此分享我在第一单元中的学习心得和成果。


第一次作业

先上UML

在这里插入图片描述
从UML图中我们可以看出,实心三角箭头表示具有关联关系,空心三角箭头表示具有泛化(继承)关系。我们用Main类处理核心业务(读取表达式),然后调用Parser类进行表达式解析,Parser类包含了ExprTermFactorParser类调用Poly来做具体的四则运算。

任务

第一次作业的任务是完成一个最多含有一层括号的多项式化简,要求化简的结果中不包含括号。

思路

我们考虑将表达式分解为表达式、项和因子,分别用Expr,TermFactor管理。于是我们就形成了Expr—Term-Factor-Expr的递归下降链,即表达式由项相加组成,项由因子相乘组成,因子又分常数因子、变量因子和表达式因子。而表达式因子拆括号后又是更低一层的表达式。

Lexer作句法解析。Lexer的作用主要是获取下一个数字或者运算符,在获取数字的时候同时处理前导零的情况。

为了进行表达式计算,在ExprTermFactor中重写的toPoly方法,用Poly的加法和、乘法和乘方运算来得到最终的Poly

为了连续加减号问题,优先处理括号,然后在进行符号选择。这样做就可以先将每一个基本项:expressionMonoNumber先化简成一个多项式后再视情况决定是否取反。

Method复杂度:

methodCogCev(G)iv(G)v(G)
Coefficient.addcos(Poly)0.01.01.01.0
Coefficient.addsin(Poly)0.01.01.01.0
Coefficient.Coefficient()0.01.01.01.0
Coefficient.Coefficient(BigInteger, BigInteger, BigInteger, String)0.01.01.01.0
Coefficient.Coefficient(BigInteger, BigInteger, BigInteger, String, HashMap, HashMap)0.01.01.01.0
Coefficient.fit(Coefficient)1.01.05.05.0
Coefficient.getC()0.01.01.01.0
Coefficient.getX()0.01.01.01.0
Coefficient.getY()0.01.01.01.0
Coefficient.getZ()0.01.01.01.0
Coefficient.merge(Coefficient)0.01.01.01.0
Coefficient.mul(Coefficient)2.02.02.02.0
Coefficient.mymul(HashMap, HashMap)8.04.05.05.0
Coefficient.reverse()0.01.01.01.0
Coefficient.scequal(HashMap, HashMap)6.05.02.05.0
compare(Coefficient, Coefficient)0.0
Expr.addTerm(Poly, int)0.01.01.01.0
Expr.Expr()0.01.01.01.0
Expr.toPoly()4.01.03.03.0
Lexer.getNumber()7.01.06.07.0
Lexer.Lexer(String)0.01.01.01.0
Lexer.next(boolean)6.02.06.07.0
Lexer.peek()0.01.01.01.0
Main.main(String[])0.01.01.01.0
Number.Number(BigInteger, int)2.01.02.02.0
Number.toString()0.01.01.01.0
Parser.parseExpr()4.01.03.04.0
Parser.parseFactor()24.01.014.015.0
Parser.parseFim(Poly)0.01.01.01.0
Parser.Parser(Lexer)0.01.01.01.0
Parser.parseTerm()1.01.02.02.0
Poly.add(BigInteger, BigInteger, BigInteger, String)0.01.01.01.0
Poly.FactorOne(BigInteger, int)2.01.01.03.0
Poly.FactorsOne(BigInteger)1.01.01.02.0
Poly.get_first()0.01.01.01.0
Poly.Poly()0.01.01.01.0
Poly.Poly_add(Poly)8.04.05.05.0
Poly.Poly_mul(Poly, Poly)3.01.03.03.0
Poly.Poly_power(Poly, int)2.02.03.03.0
Poly.Poly_reverse()1.01.02.02.0
Poly.Poly_sub(Poly)0.01.01.01.0
Poly.print()27.04.012.014.0
Poly.printV(String, BigInteger)2.02.01.02.0
Poly.sorting()0.01.01.01.0
Term.addFactor(Poly)0.01.01.01.0
Term.Term()0.01.01.01.0
Term.toPoly()1.01.02.02.0
Total112.065.0108.0121.0
Average2.291.352.252.52

可以看出Poly.print()的复杂度比较高,主要是因为函数内部集成了多个嵌套的if-else语句,用于控制不同数字和幂指数下的输出

额外实现的功能

  • 支持多变量的计算
  • 支持多层嵌套括号,增强代码的鲁棒性
  • 支持无限多个加减号,

优化

  • e x p r e s s i o n 0 expression^0 expression0可以无条件化简为1(tips:题目保证了 0 0 = 1 0^0=1 00=1

  • a i = ± 1 a_i=±1 ai=±1时,前面的数字可以省略

  • 如果所有 a i a_i ai中至少有一个为正,那么就要把一个 a i > 0 a_i>0 ai>0的项放最前面。如 − 1 + x × -1+x × 1+x× x − 1 √ x-1 √ x1√

Bugs

中测、强测和互测均未出现Bugs,强测最终得分100分。

自动化评测机搭建:

对题目中可能出现的不同致错因素,我们尽量能够覆盖:

  • 前导零
  • 连续加减号
  • 指数嵌套

采用表达式树随机展开的思路,通过调节随机因子控制不同运算出现的频率。对于任意的表达式,考虑最后一次的运算,我们总能分解成 e x p r e s s i o n x expression_x expressionx o p op op e x p r e s s i o n y expression_y expressiony的形式。( o p op op可以是 + 、 − 、 × +、-、\times +×,注意乘方已经被单独考虑。然后可以对 o p op op两边的表达式进行进一步的拆分,注意适时引入前导零和连续加减号。执行到控制的最后一层时,返回最基本的因子 x k x^k xk N u m b e r Number Number

在评测机判断正确性的时候,采用pythonsympy中的simplifyequals函数判断答案和sympy的化简是相等。

互测策略

在第一次作业的互测中,我采用了自动化评测机测评+手动构造特殊数据的思路。结合往年学长学姐在互测中遇到的问题诸如”表达式为0不输出“,”连续加减号处理出错“,以及我自己构造的幂函数嵌套、指数为0的情况,均未发现问题。

互测房采用的是无差别攻击,最终发起hack:0/48;受到hack:0/28。互测最终得分10。(基本分)

总结与改进

输出方法的复杂度过高,需要重新规划其架构。


第二次作业

先上UML

在这里插入图片描述

任务

在第一次作业的基础上迭代开发,实现自定义函数和指数函数的功能。

思路:

自定义函数

自定义函数形如 f ( x , y ) = x 2 + y − y 2 f(x,y)=x^2+y-y^2 f(x,y)=x2+yy2这种形式,且不能互相调用。

解决这个问题主要分成两种思路。非常容易想到的思路就是递归替换表达式中的自定义函数,使之最终成为一个只含有x的表达式。(这么做的好处就是思路直接,只需要写一个Substitute的代换类就可以解决问题。另一个思路是将自定义函数看成基本因子,在ParserParseFactor中直接解决。

我们注意到,自定义函数不能直接用正则表达式替换,否则会出现错误,如调用函数为 f ( f ( 1 , 1 ) , 1 ) f(f(1,1),1) f(f(1,1),1)的时候。其次,当自定义函数中出现exp时,要将换成另一个函数,然后在替换后再将其换回来。因此,我们可以考虑采用递归替换的方式,每次替换的过程中循环扫描自定义函数中“,”出现的位置,然后对表达式进行分层替换。在替换过程中,最好在每个表达式外面加一层括号,防止计算顺序发生改变。

指数函数

不难发现 e a e^a ea x a x^a xa同质,唯一的变数是指数上不仅仅是数字。这样来说只需要在PolyCoefficient(Unit)类之间搭桥,相互调用就可以轻松解决。这次作业,与上次作业变动比较大的就是同类项的判定和项的乘法。

  • 对于同类项的判断,第一次作业我们只需看Poly中的每一项的 x x x次幂是不是相等即可,现在我们还要知道每一项中exp括号里的项相不相同。非常容易想到可以通过PolyCoefficient(Unit)类相互调用来解决。
  • 对于项的乘法,在第一次作业的基础上,增加将exp中的内容相加的函数。

Method复杂度:

methodCogCev(G)iv(G)v(G)
Coefficient.addExp(Poly)0.01.01.01.0
Coefficient.clonexyz(Coefficient, BigInteger, BigInteger, BigInteger)0.01.01.01.0
Coefficient.Coefficient(BigInteger, BigInteger, BigInteger, BigInteger)0.01.01.01.0
Coefficient.Coefficient(BigInteger, BigInteger, BigInteger, BigInteger, Poly)0.01.01.01.0
Coefficient.expz()0.01.01.01.0
Coefficient.fit(Coefficient)1.01.04.04.0
Coefficient.getC()0.01.01.01.0
Coefficient.getX()0.01.01.01.0
Coefficient.getY()0.01.01.01.0
Coefficient.getZ()0.01.01.01.0
Coefficient.merge(Coefficient)0.01.01.01.0
Coefficient.mul(Coefficient)2.02.02.02.0
Coefficient.mulc(BigInteger)0.01.01.01.0
Coefficient.printFactor()7.01.05.05.0
Coefficient.printV(String, BigInteger)3.03.02.03.0
Coefficient.reverse()0.01.01.01.0
Coefficient.setC(BigInteger)0.01.01.01.0
Coefficient.toExpDiff(Character)0.01.01.01.0
compare(Coefficient, Coefficient)0.0
Expr.addTerm(Poly, int)0.01.01.01.0
Expr.Expr()0.01.01.01.0
Expr.toPoly()4.01.03.03.0
Lexer.getNumber()7.01.06.07.0
Lexer.Lexer(String)0.01.01.01.0
Lexer.next(boolean)8.02.08.09.0
Lexer.peek()0.01.01.01.0
Main.main(String[])1.01.02.02.0
Parser.getreaL()3.01.02.03.0
Parser.parseExpr()4.01.03.04.0
Parser.parseFactor()22.01.015.015.0
Parser.parseFim(Poly)0.01.01.01.0
Parser.Parser(Lexer)0.01.01.01.0
Parser.parseTerm()1.01.02.02.0
Poly.add(BigInteger, BigInteger, BigInteger, BigInteger)0.01.01.01.0
Poly.addFactor(Coefficient)0.01.01.01.0
Poly.dec(Poly, BigInteger)16.06.05.012.0
Poly.egf(Coefficient, Character)6.01.07.07.0
Poly.equals(Poly)17.09.04.011.0
Poly.gcd(BigInteger, BigInteger)3.02.02.02.0
Poly.getAllFactor()38.01.013.015.0
Poly.getArr()1.01.02.02.0
Poly.getDiv(BigInteger, BigInteger)0.01.01.01.0
Poly.myclone(Poly)1.01.02.02.0
Poly.Poly()0.01.01.01.0
Poly.Poly_add(Poly)12.04.06.06.0
Poly.Poly_mul(Poly, Poly)3.01.03.03.0
Poly.Poly_multifly(Poly)0.01.01.01.0
Poly.Poly_power(Poly, int)4.03.04.04.0
Poly.Poly_reverse()1.01.02.02.0
Poly.Poly_sub(Poly)0.01.01.01.0
Poly.polyToDiff(Character)3.01.03.03.0
Poly.polyToExp()2.01.02.02.0
Poly.print(BigInteger)12.02.05.06.0
Poly.printE()16.05.010.013.0
Poly.simplify()3.01.03.03.0
Poly.sorting()0.01.01.01.0
Poly.toNewPoly(ArrayList>)6.01.05.05.0
Poly.twoFormPoly(Coefficient, Coefficient, BigInteger, BigInteger)0.01.01.01.0
Print.decideNumber(int, int, BigInteger)23.08.06.09.0
Print.end_execu(int, BigInteger)1.01.02.02.0
Print.extendedEuclid(BigInteger, BigInteger, BigInteger[])2.02.01.02.0
Print.findMinimalLength(BigInteger, BigInteger, BigInteger)3.02.04.04.0
Print.findSolutionForAxPlusByEqualsC(BigInteger, BigInteger, BigInteger)1.02.01.02.0
Print.gen(BigInteger, BigInteger, BigInteger, BigInteger, BigInteger)2.01.01.03.0
Print.getLenwhy(BigInteger, BigInteger, BigInteger[], BigInteger, BigInteger, BigInteger)3.01.01.04.0
Print.getmod(int, BigInteger, BigInteger[])6.01.04.05.0
Print.getsl()0.01.01.01.0
Print.init(ArrayList)1.01.02.02.0
Print.pan(BigInteger, BigInteger)2.03.01.03.0
Print.Print(ArrayList)0.01.01.01.0
Print.yp(Poly, int, int, ArrayList)39.07.012.015.0
Substitute.add(Character, String)0.01.01.01.0
Substitute.delta(String, char)29.05.07.010.0
Substitute.replaces(String)6.04.04.04.0
Substitute.replacing(String, char)11.07.04.08.0
Substitute.Substitute()0.01.01.01.0
Term.addFactor(Poly)0.01.01.01.0
Term.Term()0.01.01.01.0
Term.toPoly()1.01.02.02.0
Time.ok()0.01.01.01.0
Time.setBg(long)0.01.01.01.0
Total337.0139.0220.0268.0
Average4.161.742.753.35

可以看出Poly.print()经过改进后,复杂度有所降低。但是由于功能的复杂性,整体函数的复杂度较第一次作业来说都有所上升。

额外实现的功能

  • 多变量指数函数、自定义函数计算

  • 基于启发式的双因子优化

  • 实现求导算子(第三周)

  • 自定义函数间可相互调用(第三周)

基于启发式的双因子优化

第一次看到这个题的时候,认为这个题有理论最优解不就是把所有exp外面的指数提进去变乘法,输出的时候提最大公因数就行了吗

直到我在讨论区看到 e x p ( ( 20 ∗ x 2 + 30 ∗ x 3 + 40 ∗ x 4 ) ) exp((20*x^2+30*x^3+40*x^4)) exp((20x2+30x3+40x4))提5不提10的神操作和分解exp,我才意识到问题没有这么简单。

我们所能做的优化就是在尽可能覆盖更多情况的原则下,选择最短的那一个。举几个例子:

  • e x p ( ( 10000000 + 20000000 ∗ x + 29999999 ∗ x 2 + 30000000 ∗ x 3 ) ) exp((10000000+20000000*x+29999999*x^2+30000000*x^3)) exp((10000000+20000000x+29999999x2+30000000x3))化简出来的最优表达式为: e x p ( ( − x 2 ) ) ∗ e x p ( ( 6 ∗ x 2 + 6 ∗ x 3 + 4 ∗ x + 2 ) ) 5000000 exp((-x^2))*exp((6*x^2+6*x^3+4*x+2))^{5000000} exp((x2))exp((6x2+6x3+4x+2))5000000
  • e x p ( ( 20 ∗ x 2 + 30 ∗ x 3 + 40 ∗ x 4 ) ) exp((20*x^2+30*x^3+40*x^4)) exp((20x2+30x3+40x4)),提5不提10
  • e x p ( ( 7 ∗ 8736823764348 ∗ x + 9 ∗ 8736823764348 ∗ x 2 + 6 ∗ 87348734 + 8 ∗ 87348734 ∗ x 3 ) ) exp((7*8736823764348*x+9*8736823764348*x^2+6*87348734+8*87348734*x^3)) exp((78736823764348x+98736823764348x2+687348734+887348734x3))化简出来的最优表达式为: e x p ( ( 8 ∗ x 3 + 6 ) ) 87348734 ∗ e x p ( ( 9 ∗ x 2 + 7 ∗ x ) ) 8736823764348 exp((8*x^3+6))^{87348734}*exp((9*x^2+7*x))^{8736823764348} exp((8x3+6))87348734exp((9x2+7x))8736823764348。可以发现这里有两个基本大因数相同,可以把整个表达式拆解成两份。

针对这些情况,首先要阐明几个误区:

  • 因数并不是应提尽提的,要考虑exp内外数字的位数。
  • 大因数不一定是质数,只要两个大因子相差不大提出一个来都会使表达式更优。
  • 质因数分解并不能够是表达式变短,我们显然有 ∣ a 1 ∣ |a_1| a1+ ∣ a 2 ∣ |a_2| a2+ ∣ a 3 ∣ |a_3| a3+…+ ∣ a n ∣ |a_n| an ∣ ∏ i = 1 n a i ∣ |\prod_{i=1}^{n} a_i| i=1nai,况且质因数分解还要添括号呢

弄清楚这几点之后,就可以构思优化的算法了。我们主要用了启发式说白了就是贪心。我们先明确一点,任何一个项前都有一个系数 c c c

  1. 通过观察发现,这些提出来的因子大多数与 c c c有直接或间接的关系。那么我们不妨作个boli,把exp中所有的项的系数c以及它们两两(及附近,如101和150可以提出个50来)之间的最大公因子提取出来,当成我们能够使用的因子集。(当然1肯定属于因子集)

  2. 我们提出来的 e x p ( ( . . . a i ∗ x k . . . ) ) p i exp((...a_i*x^k...))^{p_i} exp((...aixk...))pi,必然满足 ∑ i = 1 n a i p i = c \sum\limits_{i=1}^{n} a_ip_i = c i=1naipi=c。(*)根据裴蜀定理,如果 g c d ( a , b ) ∣ c gcd(a,b)|c gcd(a,b)c,则 a x + b y = c ax+by=c ax+by=c必有整数解。这题数字很离散,而且因子集数量庞大,姑且认为一定存在最新研究成果,该式对任意选中因子不成立,那么必有替换因子能达到最优解) g c d ( a , b ) ∣ c gcd(a,b)|c gcd(a,b)c。所以,每个系数不妨只考虑*式n=1或n=2的情况。当然有人会问,那n=3呢?可以想想,多加一个exp函数要添多少个字符,如果分解成三个大概率不如两个exp因子

  3. 准备完因子集,接下来就要做组合选择了。根据大量的实验表明,一般分解的exp函数不超过四个,我们不妨就最多选择四个因子进行分解。对于每一个系数 c c c,考虑选择任意1个或2个因子对它进行线性表示(这个过程用到扩欧),取表达式最短的为答案。

  4. 如果每一个系数 c c c都被表示了至少一次,那么可以说这次对因子集的选择成功了,可以更新一次最短答案。

当然,这个算法的时间复杂度是难以准确估计的。在我的代码中加入了一个性能上界,让这个优化算法不要跑太久,以免超时。

本次作业中,检测出了一个比较精细的优化。但是,还是有一些情况

Bugs

中测、强测和互测均未出现Bugs,强测最终得分100分。本次作业中检测出了一个比较精细的优化

自动化评测机优化

在hw2中,我借鉴了讨论区jzy同学的思路,将答案比对方式由simpy化简改为“对拍式”检验。

具体来说,本次作业中输入和输出的格式是一致的,所以可以考虑采用表达式相减是否为零来判定两个人的答案是否相等。例如,两个人输出的答案是A和B,则可以构造"A-(B)"这个表达式,将它用其中一个人的程序化简。如果化简的结果是0,则对拍成功;否则,两人中至少有一个人答案错误。

这个方法很好地解决了sympy在化简复杂表达式中突出的性能问题,但是它也有一定的局限性,需要我们想办法规避:

  • 该方法对化简"A-(B)"这个表达式的程序提出了更高的要求,该程序必须做到最优化简(若A,B相等,不管以什么形式出现,结果都必须是0),并且能够处理大指数如 e x p ( ( x ) ) 10000000000 exp((x))^{10000000000} exp((x))10000000000之类的数据。
  • 两个人的程序基本都是基本正确的,不能出现检测程序有bugs,无条件输出0这种情况。。
  • 两个人的程序不能错到一个地方去(当然这种可能性很小)

在本次作业中,我们还搭建了后端评测机,能够更方便地用命令行评测。

互测

与第一次作业的互测相比,这次的互测可谓是峡谷大乱斗,先说战绩。最终发起hack:11/84;受到hack:0/39。互测最终得分:10+4.93。这次小组中由于有“福利机”的存在,所以基本上每人都有刀成功的经历。(最后“福利机”中枪21发)

我在互测中hack出了四个bugs:

  • 对于 e x p ( ( − x ) ) exp((-x)) exp((x))这种情况无法正确识别导致的异常情况
  • 当幂函数和指数函数幂次为0时没有去除乘号,出现 1 ∗ + x 1*+x 1+x这种错误答案
  • 对于exp指数处理不当,用int保存exp的指数,一旦指数超过int范围就会RE
  • 乘方过程效率太低,导致多层乘方后TLE

当然,还有一个同学的问题是出现 e x p ( ( 0 ) ) exp((0)) exp((0))不化简,本来想hack,但是官方评测机判对,遂无下文。

究其原因,出现这些bugs还是由于代码的框架没有规划好。这启示我们在进行框架构思的时候,要充分考虑数据的特殊情况。

总结与改进

  • 进一步优化架构
  • 进一步改进双因子优化算法(好吧,最后鸽了

第三次作业

任务

在第二次作业的基础上迭代开发,实现自定义函数相互调用和求导的功能。

这里就不放UML图了。因为第二次作业已经实现了第三次作业的全部功能,所以代码一行也没动,第二次作业的框架就是最终的框架。😂

思路

求导算子

这次作业相对于往年来说大幅度削减了难度,自定义函数中不再含有求导算子,这样的话给我们直接做表达式替换的同学带来了利好。如果没有这个条件,我们在做表达式替换的时候会遇到一些问题,什么问题呢?通过指导书我们知道,对 d x ( f ( e x p r e s s i o n x ) ) dx(f(expression_{x})) dx(f(expressionx))这种情况,我们只需要先求出 f ( e x p r e e s i o n x ) f(expreesion_x) f(expreesionx)的值,然后对整个化简之后的表达式进行求导即可。

这本质上是一个代值的过程。

d x ( f ( e x p r e s s i o n x ) ) = f ′ ( e x p r e s s i o n x ) dx(f(expression_{x})) = f'(expression_{x}) dx(f(expressionx))=f(expressionx)

而非 d x ( f ( e x p r e s s i o n x ) ) = f ′ ( e x p r e s s i o n x ) × ( e x p r e s s i o n x ) ′ dx(f(expression_{x})) = f'(expression_{x}) \times (expression_x)' dx(f(expressionx))=f(expressionx)×(expressionx)

而为什么自定义函数中有求导算子会带来麻烦呢?举个栗子:

1 1 1

f ( x ) = d x ( x 2 ) f(x)=dx(x^2) f(x)=dx(x2)

f ( e x p ( x ) 2 ) f(exp(x)^2) f(exp(x)2)

如果我们直接做表达式替换,会发生什么情况呢?

d ( e x p ( x ) 2 ) ( ( e x p ( x ) 2 ) 2 ) d(exp(x)^2)((exp(x)^2)^2) d(exp(x)2)((exp(x)2)2)

不难发现,此时的求导算子可能是任意的因子(变量因子、常量因子、表达式因子),那么我们直接用表达式替换来做这个问题就会变得相当复杂。当然,这个问题有很好的解决办法。也许,聪明的同学已经"注意到",我们其实可以对自定义函数进行一遍预化简,这样自定义函数中就不含求导算子啦~~

函数相互调用

用表达式替换和递归做都可以。

优化

没什么好说的,其实就是在原代码的基础上进行调参。在两次作业中,我的双因子优化法中两个参数的系数大小只调到了10,即最终exp里面的两个项前面的系数只能小于10(当初设计这点的时候,主要是碰到了几组比较恶心的数据,让我的代码跑得很慢)。但是,事实上,调整这个参数的大小还是十分有意义的。
For example:

exp((44427946663936*x^6-94409386660864*x^4+50154986663585*x^2)) //系数为10
exp(x^2)^50154986663585*exp((8*x^6-17*x^4))^5553493332992 //系数为20
exp(x^2)*exp((289*x^2+256*x^6-544*x^4))^173546666656 //系数为600

当然啦,该优化算法最nt的一点就是时间复杂度不可计算,所以,这个系数调大了容易T,调小了容易丢性能分,这个度不好把控。

Bugs

因为优化问题被干了一个点:
0 0 0
d x ( e x p ( e x p ( e x p ( e x p ( e x p ( e x p ( e x p ( e x p ( x 2 ) ) ) ) ) ) ) ) ) dx(exp(exp(exp(exp(exp(exp(exp(exp(x^2))))))))) dx(exp(exp(exp(exp(exp(exp(exp(exp(x2)))))))))
至于为什么会被hack,那是因为互测数据的CPU时限是2s,而强测CPU时限是10s;而我的时间上界设成了5s,所以寄了。把优化删了就对了

互测:

这次加入求导方法后,整体复杂度并没有很大的提升。所以互测成功率很低,我构造了几组形如
0 0 0
d x ( e x p ( d x ( e x p ( x 2 ) ) ) ) dx(exp(dx(exp(x^2)))) dx(exp(dx(exp(x2)))
这样的嵌套数据后,也没有发现同学们有什么问题。
反而是被人给hack了。

架构设计概览

第一次作业
调用解析方法
获取下一个算术单元
Down
算术基本运算
项之间的加减运算
Main
Parser
Lexer
Expr
Term
Factor
Const_Factor
Variable_Factor
Expression_Factor
Poly
Coefficient
第二/三次作业
调用解析方法
获取下一个算术单元
Down
算术基本运算
项之间的加减运算
输出优化
时间上界
指数函数调用
求导调用
Main
Parser
Lexer
Expr
Term
Factor
Const_Factor
Variable_Factor
Expression_Factor
Poly
Coefficient
Optimizer
Time

因为三次作业结构比较类似,所以没有经历重构的过程。但是,我们可设想一个新的迭代场景,并在这个场景下设想迭代的思路。就比如说,我们可以设想在本项目的基础上,增加三角函数。
容易发现,即使增加三角函数,我们的项目也不用改进过多,只需在原有Coefficient的基础上,增加一个HashMap储存三角的项即可。整体处理思路和exp完全一样。(其实第一次作业我写了三角,结果今年直接换成exp了


项目总结

在三次作业中,始终贯穿着的是“因子”的思想。引入了“因子”的概念,就可以用面向对象的思想进行抽象层级管理,当然也不用反复重构。我代码的逻辑核心其实就是"表达式因子"。所有的因子、项、表达式之间,都采用了一个抽象的"表达式因子"(Poly)进行传递。这个做法的数学原理就是加法、乘法、乘方、求导、自定义函数等运算通通对Poly类封闭。

用一个不严谨的数学表达:

P o l y = ∑ a i x k ⋅ e x p ( P o l y s u b ) Poly = \sum a_i x^k·exp(Poly_{sub}) Poly=aixkexp(Polysub)

这样,我们就建立起了因子之间清晰的层次关系,让嵌套括号和表达式处理的逻辑变得非常简单。

在本次作业中,通过分析表达式的层次结构,我逐渐领悟到了面向对象思想中各司其职、各尽其责的含义。通过是否有数据关联和行为关联,对不同的因子类及其数据和方法进行聚合,使用接口或继承,减少代码量,提高代码的可读性。

个人认为,在理解整个代码的逻辑框架之后,难点还是在精简表达式层面。这本身是一个永无止境的NP-HARD过程,但是,相信通过我们6系同学的努力,一定能设计出来一个具有局部最优性的启发式算法,最小化表达式的长度。


经验总结与心得体会

经验总结

经历了第一单元的三次迭代开发任务后,我总结了如下几点:

  • 一定要认真做课程组的trainning

  • 善用课上讲的面向对象思想来设计架构

  • 总体框架想得越早越好,尽量兼顾扩展性和鲁棒性

  • 做好项目的测试工作,可以搭建评测机或者手动构造数据

项目中SOLID原则的体现

  • 单一职责原则(SRP):在本次项目中,我对表达式做了深度的解析,每一个类都有鲜明的职责,比如Lexer类负责解析运算基本元,Mono类负责项之间的基本运算。每个类并不会实现与其关联不大的功能。
  • 开闭原则(OCP):项目中对表达式中各个部分进行了精细的划分,无论增加何种运算和符号,都非常容易扩展。对于解析、算数运算、表达式优化部分进行高度封装,不需要修改已经存在的代码。
  • 里氏替换原则(LSP):在本项目中没有过多体现。
  • 接口隔离原则(ISP):本项目中实现了几个公共接口,如toString,toPoly,toDiff等。这些方法并不是放在同一个类来实现的,而是按照每个类的需求来继承。
  • 依赖反转原则(DIP):Expr,Term相对于Factor来说都是高层次模块,但它们之间并没有过度的依赖,总体来说,Expr和Term并不依赖于因子之间相互运算和合并的细节,而是侧重于抽象的“表达式”和“项”方面。
    总的来说,本项目的代码基本践行了低耦合高内聚的思想,让类中的方法更加易于维护。

心得体会

这三次作业由浅入深,让我们从零开始逐步建立起对面向对象设计理念的了解。表达式本身就是一个由因子,运算符,括号组成的有机整体,具有着天然的分层架构。课程组选择用表达式化简作为背景,让我们通过递归下降法为切入点,在编写代码的过程中锻炼自己的面向对象思维。经过第一单元,让我对面向对象设计思想有了更为深刻的理解,面向对象代码能力得到了显著的提高。

通过这份详细的总结和反思,我也发现了本次代码中一些需要优化和提升的部分,希望能够在第二单元的学习中进一步优化和改善。

未来展望

希望助教以后能够适当降低性能分,更加关注于面向对象思想的设计与构造,避免无意义的内耗。卷到最后一无所有

  • 28
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值