BUAA_2022_OO_Unit1_Summary

还没从寒假的懒散惬意中摆脱出来,OO_Unit1便迎面袭来

第一次作业分析

思路分析

我们要对一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号并化简的表达式。我的基本思路如下:

  • 预处理消除多余的空格和连续的正负号('--' → '+','+-' → '-')

  • 先通过递归下降的方法将待解析表达式转换为表达式树结构(获得表达式结构)。

    这里发现所有的基本因子(除Expr因子)都可转换为\(a * x ** b\)的形式,例如:

    \[0 = 0 * x ** 0\\ 1 = 1 * x ** 0\\ x**3 = 1 * x ** 3\\ \]

    因此,我引入了Unit这一基本单元用来表示\(coe * x ** pow\),同时Expr和Unit统一实现Factor接口。

    • 当解析到数字和幂函数时直接统一用Unit封装即可。(这里是为了方便合并同类项和计算,但同时也极大的降低了可拓展性,为后续的重构做出了重要贡献
    • 当识别到左括号可以生成一个新的Expr因子。

    由此完成表达式树的构建。

  • 递归解析生成的表达式树,最终生成一个ArrayList<Unit>用来存储解析表达式生成的所有基本单元Unit

    \[ArrayList<Unit>\space→ \sum a_i*x**b_i \]

    • 表达式、项的基础组成逻辑如下:

    \[Expr = trem\quad[\quad+ \quad terms\quad]*\\ term = Factor\quad[\quad* \quad Factor\quad]*\\ Factor = Unit\ \quad|\quad\ Expr\\ Unit = coe*x**pow\\ \]

    • Expr遍历所有term,通过调用term.simplify()获取每一项的所有Unit,加入ArrayList<Unit>,然后根据表Expr自身的指数,连续自乘,获取最终的Unit列表。
    • Term遍历所有的Factor,使得每个Factor互乘,由于存在Expr因子,因此每次互乘都需要遍历2个Factor乘子的Unit列表,完成2个ArrayList<Unit>的互乘,最后返回一个ArrayList<Unit>。这里需要注意的是,项具有正负,因此最后需要根据项的正负判断位positive,决定Unit列表是否需要进行系数翻转。
  • 最后将得到的ArrayList<Unit>合并同类项化简即可。化简可以注意以下几点:

    • 优先寻找正向因子并放到表达式首部,如:

      \[-1+x → x-1 \]

    • cow为1或-1的因子可以省略输出"1*",如:

      \[1*x**3 → x**3\\-1**x**6 →-x**6 \]

    • pow为0的因子可以省略输出“x**0“,pow为1的因子可以省略输出"**1",pow为2的因子可以输出为“x*x",如:

      \[2*x**0→2\\x**1→x\\3*x**2→3*x*x \]

    • cow为0的因子直接省略,但在此过程中需要避免化简出现的一些问题,如

      \[1*x**0\\0*x**2 \]

    综上可以完成化简部分。


    程序架构分析

    类图

image

  • 可以看到结构较为清晰,操作也较为简单,Expr和Unit实现Factor接口后可以分别重写simplify()实现化简。
  • 但是由于使用Unit统一封装所有基本因子,降低了后续的可拓展性。

方法复杂度分析

image

  • getPolyFunc()、preProcessing()涉及化简、合并同类项,需要特判较多情况,因此复杂度较高
  • praseFactor()涉及判断各种因子的种类,导致复杂度的升高
  • 大部分方法复杂度较低,且分布较为均匀

类复杂度分析

image

  • 和方法复杂度同解释

Bug分析

自己的Bug分析

  • 公测和互测均没有发现Bug。
    • 自己虽然没有测出bug,但是不保证没有bug。
    • 特殊判断太多,加入新的条件便需要改动太多地方,代码可读性较差,同时不便于后续迭代。

Hack策略和结果

  • Hack上主要通过自动生成大量随机数据轰炸和手撸边界数据。

    • 代码实现(实现细节省略),构造思路也是利用递归下降生成表达式,通过合法性检测后方可Hack他人数据:
    1.def getRanExpr(maxLen) -> str:
      *****************
    2.def checkForLegal(expr) -> bool;
      *****************
    3.import sympy
    	project_dir = "C:\\**\\OO\homework1\\test"
      compare with sympy.sympify(expr)
      *****************
    
    • 手动制造的一些数据:

    \[边界:+-(-040000000*+81+-x**4*x**2)+-(+x*+3)**0\\ -+(+--0+-10)**+3-(-x**+2)**+1*(+-x+x**+0)**+1*x \]

    \[特殊数据:(-x**2)*(-1)**0*(-1)**0\\ -(+x)**0+(-x**2)**1*(-1)**0*(x**1)**1*(0)**0 \]

\[化简易出错数据:11*x**0 + 2- +1*x**0-1*x*x+1*x**1 \]

  • 最后发现别人几个bug:
Bug数据 Bug结果 Bug分析
(-x***2)(-1)*0 x*x 在计算(-x**2)生成-x**2没考虑负号
2 2* 出现2*x**0时没考虑因子没有幂函数部分,因此不应该有*号
(x-x)**+0 0 0**0的结果应为1

第二次作业分析

在屎山上建造一些新的房子,糟心程度可想而知。

出于懒惰,却往往不能达成目的。


新增内容

本次作业在上次的基础上增加了自定义函数,三角函数和求和函数,并且可允许括号的多层嵌套。由于我采用递归下降的处理方式,可以支持多层括号的嵌套。下面简单介绍一下我增量开发时采用的思路:

  • 对于三角函数我定义为下:

    \[(sin|cos)(a*x**b)**c \]

    构建三角函数累TrigoFunc,用sign,factorCoe,factorPow,pow属性分别记录对应位置信息。

  • 对于自定义函数类,我定义了CustomFunc类直接存储自定义函数,并替换成标准表达式(这种方式其实破坏了表达式的结构性,但是由于懒惰····),基础思路如下:

    • 该类的addCustomFunc方法读入一个自定义函数表达式,以 HashMap<String, ArrayList<String>> 的形式保存于该类中,索引为函数名,内容是函数的各个参数和表达式;

      \[f(x,y,z)=xy+y+z\Leftrightarrow<f,[x,y,z,xy+y+z]> \]

    • 细节处理

      • 每次替换要整体带入,即将因子左右套上括号;
      • 对于f(y,x)的替换,先保护待替换变量x,防止其被重复替换
  • 对于求和函数,我定义了求和函数类,通过字符串替换方式得到求和函数的等价表达式(这种方式其实破坏了表达式的结构性,但是由于懒惰·····),形如\(sum(i,a,b,\alpha(i,x))\)的公式,我有以下处理思路:

    • 将a,b转化为整数,判断上下界大小,如果\(a>b\),则返回0。

    • 否则,通过循环得到字符串(如下)替换\(sum(i,a,b,\alpha(i,x))\)

      \[( \alpha(a,x) + \alpha(a+1,x) + ······· + \alpha(b,x)) \]

    • 细节处理:

      • 对于形如\(sum(i,a,b,i**2)\)的替换时需要需要注意替换为\((a)**2\)保证通过合法性检测;

      • 形如\(sum(i,a,b,sin(i))\)注意对sin中的i在替换时的保护

      • 求和函数的上下界应使用BigIneger以防止溢出(本人没考虑,导致第三次作业被无尽hack

迭代思路

  • 主体表达式部分已经根据我们的“预处理”,解决了自定义函数和求和函数的困扰。

  • lexer和praser部分增添对求和函数和三角函数的识别。

  • 对于新的基本因子我们可以定义为:

    \[Unit→a*x**b*\prod{sinFunc}*\prod{cosFunc} \]

    因此我们可以这样定义Unit:

image

这样我们在解析到任何基本因子(除Expr外)都可以用Unit去包装,合并同类项也变的非常简单(重构已埋下根基)。

  • Unit新增toString()方法,方便简化输出形式较为复杂的Unit单元。

  • 化简流程

    • 特殊三角函数值的简化:

      \[sin(0)=0\space,cos(0)=1\\ sin(0)**0=1\space,cos(0)**0=1\\ sin(x**0)=sin(1),cos(x**0)=cos(1) \]

    • 基于三角函数奇偶性的化简:

      \[sin(-f(x))**(2n+1)=-sin(f(x))**(2n+1)\space,sin(-f(x))**2n=sin(f(x))**2n\\ cos(-f(x))**a=cos(f(x))**a \]

    • 特殊三角函数值合并:

      \[sin(a)**2+cos(a)**2=1\\ f(x)*sin(a)**2+f(x)*cos(a)**2=f(x) \]

    还有一些更高阶的化简办法,例如动态规划实现\(sin(x)**2,cos(x)**2,1\)之间的转换、二倍角公式的使用····由于实力有限,没有在思考处理,实属遗憾。

程序架构分析

类图

image

  • 这里的TrigoFunc、SumFunc没有发挥实际作用,进一步感受到好的架构的重要性!

方法复杂度分析

image
image

  • toString()方法由于需要特判较多情况,因此复杂度较高。
  • getTrigoFunc()和getStdExpr()由于获取的内容较大,消耗较多。

类复杂度分析

image

  • MainClass要做较多的对字符串的预处理,消耗较多,Parser同理。
  • Unit涉及较多的乘法处理,并重写了toString()(特判条件较多),故复杂度也较高。

Bug分析

  • 自己的Bug分析

    • 强测部分未发现Bug。

    • 互测部分发现了一个bug:

      Bug数据 Bug结果 Bug分析
      sin(0)**0 0 优先判断了sin(0),应该优先判断sin(0)的幂
  • hack策略

    • 使用自动生成机生成随机数据,此时去除指数运算时上限为8的情况,允许递归深度为10,此过程并没有hack到他人。

    • 手造一些特殊点去评测他人,

      • 递归层数较多的:

        \[((((x**8)**8)**8)**8)**8\\ ((((((((((((((0))))))))))))))**1\\ (x+(x+(x+(x+(x+(x+(x)**1)**2)**1)**1)**0)**1)**1 \]

      • 优化容易错误的:

        \[sin(-2)**2+cos(2)**2\\ sin(0)**0*cos(x)**2+cos(0)**0*sin(x)**2\\ 11*x*x**2*x*7 \]

      • 边界数据:

        \[100000000000*x*-1018190191*-1\\ x**8*x**8*x**8*x**8*x**8*x**8*x**8 \]

      发现了2个Bug:

      Bug数据 Bug结果 Bug分析
      sin(0)**0 0 优先判断了sin(0),应该优先判断sin(0)的幂
      sin(-1)**2-cos(1)**0 -sin(1)**2-1 Sin(-1)**2转换成了-sin(1)**2
  • 在互测前应该构造边界数据去测试自己的程序,这样就不会出现上述Bug了(因为互测的数据自己都过不了)。


第三次作业分析

纠结中,我选择了重构。虽然路途艰辛,但是光亮尽在前方照耀。

新增内容

  • 支持三角函数、自定义函数引用的嵌套
  • 痛点分析
    • 用已经有的模型在替换函数的时候需要多次反复替换,且破坏了函数的整体结构性
    • sinFunc和cosFunc内嵌表达式无法用原有形式表达,即便可以,合并三角同类项也将成为不小的麻烦

于是我选择了重构。

重构历程

  • 解析到各种因子之后,不去使用Unit包装,而是生成对应的类

这是第二次作业遇到幂函数(其余同理)声明的因子:

image

第三次作作业声明则转变为:

image

可以看到不仅节省了大量的空间消耗,也使得结构更加清晰,虽然后续处理过程较为麻烦,但是我认为良好的结构层次才是我们应该学习的重点。

  • 除基本因子以外,每种因子构造时内嵌表达式因子,并对其进行解析,得到Unit列表,这样可以避免字符串替换对表达式树结构的破坏:

image

声明时对替换过最外层后形成的新表达式(sb)进行解析:

image

这样实际上我们在声明含有嵌套表达式的时候只需将最外层嵌套替换,因为声明形成的Expr在解析式会继续向下替换,直到形成最简单的表达式为止,这样非常契合我们向下递归的思路,我们不妨举个例子:

CustomFunc: f(x) = x + 1
						g(x,y) = x*y +sin(x)
  
Quote     : f(g(f(x),sum(i,1,2,x*i)))
  
init_1.   : g(f(x),sum(i,1,2,x*i)) + 1
init_2.   : f(x)*sum(i,1,2,x*i)) + sin(f(x)) + 1
init_3.   : (x + 1)*(1*x + 2*x) + sin((x + 1)) + 1

经过递归下降便可生成最终的表达式 (x + 1)*(1*x + 2*x) + sin((x + 1)) + 1,此过程结构清晰,毫无循环替换的狼狈姿态。

  • 借用工厂模式,对所有因子统一使用Factor访问操作,只在需要时通过instanceof关键词转型,对不同因子采用不同访问操作。具体表现为:

    • 声明时统一用Factor接口实现:Factor A = new FactorType();
    • 表达式树化简时,根据因子的不同种类进行转型操作:

image

通过这种方式构建化简表达式树,逻辑清晰,可阅读性强(更可能被人观赏(bushi~)。

  • Term执行因子相乘时,不是笼统的完成2个ArrayList<Unit>相乘,而是根据因子的不同类型进行不同的乘法操作(灵活性较强,降低空间复杂度,提升性能):

    • 待乘因子为基本因子:SinFunc、CosFunc、Constant、PowerFunc,则向目前Term的每个Unit加入相关因子。
    • 待乘因子本质上为表达式因子:SumFunc、QuoteFunc、Expr时,则完成2个ArrayList<Unit>的相乘。
  • 合并同类项时由于三角函数列表长度不定、三角函数内嵌表达式等价形式多样,因此我们需要更加适用的判等方式。

    • 三角函数内重写equals方法,用于判定2三角函数是否等价。
    • Unit中重写equals方法,判别同类项,新增equalsAll方法,判断三角函数内嵌表达式是否完全一致。
    • 三角函数简化时,添加一定细节:
    sin((x**2))**3 ->  sin(x**2)**3
    sin((x+1))     ->  sin((x+1))
    

    此时我们需要在Unit增添判断三角函数内部是否只含有一个基本因子的函数hasOneFactor(),以保证任何情况下三角函数表达式输出的括号层数都是合法且最少的。

至此,重构完成!

程序架构分析

类图

image

方法复杂度分析

image

image

image

  • sortFactors()由于需要判断Factor的各种种类,导致判断的增多,addFactor()也要判断因子种类。
  • 其余的圈复杂度较为理想

类复杂度分析

image

  • Lexer和Praser由于重构后需要判断的因子种类增多,无法复杂度升高。
  • Unit中乘法和判等的情况增多。

Bug分析

  • 强测中出现了一个bug

    Bug出现点:
    1
    f(x)=(x**0)**0-sin(x)*sin(x)
    -+ sin(f(x))*sin(x)**+02 -(01*(f(01))**2 - +7*sum(i,1,5,(x+i))*(-x))
    
    错误输出:
    2*sin(1)**2-sin(x)**2*sin((1-sin(x)*sin(x)**2))-1-sin(1)**4-35*x*x-105*x
    
    分析:
    HashMap在加入元素是hashcode已经确定,我在进行sinFunc内嵌表达式项项相乘时忽略了合并同类项后对于以加入的元素忘记了删除,导致出现:sin(x)*sin(x) -->  sin(x)**2*sin(x)的错误
    
    解决:
    每次合并同类项后及时删除已经合并的项即可。
     ((SinFunc|CosFunc) factor).setPower(BigInteger.ZERO);
    
  • 互测时发现了2个bug(确实因为重构工作量大忘记了之前解决的Bug,导致又复现了)

Bug数据 Bug结果 Bug分析
sum(i,2147483648,2147483649,i) Exception sum上下界没考虑到BigIneger范围
((x**2)**3)**2 2*x x**12在处理时发现“*1”时直接用“”替换
  • 互测他人时以手造数据为主。

    • 边界数据

    \[sum(i,-11111111111,-11111111111114,i)\\ 12234565432345*x**+0\\ sin((sin((sin((sin((x+1)**2))**2))**2))**2)\\ sin((x-x))**0+cos((x-x))**2 \]

    • 嵌套数据

      \[sin((sum(i,-1,2,(sin(i**0)*(i))))\\ sin((cos(x)**2+sin(x)**2-sum(i,0,1,i)))\\ sum(i,-1,2,cos((sin((cos((i+1))))))**2) \]

    发现了2个bug,第一种同我的互测第一个bug,第二种:

    Bug数据 Bug结果 Bug分析
    sum(i , -1 ,1 ,(sin(i**0)*(i))) -sin(-1)+sin(1) Sin(i**0)在替换时发生错误

心得体会

本单元作业最大的收获是对于 Java 的了解更加深入了,包括各种类库的使用方法、工厂模式层次化编程、面向对象程序架构设计理念。另一个重要的收获就是对于递归下降算法的深入理解。递归下降的理念非常简单,实现非常巧妙,而功能强大。

除此之外,迭代、重构是这一单元的主题,一个好的架构可以起到事半功倍的效果。为了偷懒,往往达不到目的。第二次作业由于仍选择在架构较差的第一次作业基础上添砖加瓦,导致给第三次大重构带来了不小的压力和挑战。我认为重构也是一种智慧,智者说:“实践出真知。”我经历了大重构,方可知晓重构前后的构建差距、性能优劣、拓展性强弱,在对比中择优,并加深对优秀架构的理解。

自动评测机的编写以及边界数据的构造也是第一单元测试不可或缺的部分,它们不仅能找到自己的程序中的 bug,还能帮助屋子里的小伙伴一起debug。

第一单元是我初次接触面向对象编程,学到了很多新知识,但仍存在很多不足。不过细想,尽管遇到这么大的困难,我仍然不卑不亢的走了下来,虽然无法尽其美,但也在光辉三月中播撒了珍重的汗水,收获了艳丽的阳光。

posted @ 2022-03-26 01:59  `Demon  阅读(43)  评论(0编辑  收藏  举报