OO Unit1 Expression Simplification

OO第一单元总结,本文将从以下四个部分展开:架构设计、完备测试、bug与hack、优化策略

设计

本单元三次作业我都采用了为优化服务、为计算服务的架构,采用这样架构的原因是:最后的输出实际上只是对原表达式的数据运算结果进行输出,不涉及原输入的文法的存储,因此完全略去了对存储的需求,采用边解析边计算的策略。

这样的架构优势在于:

  • 只涉及对一个存储单元的运算,只需要实现对存储单元的运算即可,运算结构相对统一

  • 为优化服务,优化只涉及存储单元内部的运算

  • 顶层结构简单,解析部分外只有计算类

这样简化的结构也带来一些问题:

  • 可扩展性较差,每次作业都只是当前需求的局部较优解,很难适应新的需求

  • 存储结构上耦合度过高,导致需求变化后存储结构都面临重构

  • 继承关系较少,抽象层次不够清晰

hw1

第一次作业难度较低,主要体现了整体设计上的方法:递归下降法。这一文法分析策略适用于上下文无关文法,也就是LL文法。我们的文法实际上是一种一元相关的Markov chain,每一个词法单元唯一确定了后面的词法结构,因此只需要完成对各个语法结构的解析即可。句法结构的下降体现于此,递归则体现在括号,括号内的层次又作为了高层次结构,需要再次递归,对括号内句法进行分析。规定了括号层数,实际上规定了递归表达式的递归树深度不能超过1。

设计上我只采用了一个Polynomial存储层次结构,内存若干个BigInteger对表示x的系数与对应的指数,结构过于简单,不做展示与分析

hw2

第二次作业难度骤增,为了更好体现工厂模式,我对解析过程进行了重构

  • 工厂模式:解析层次上,设置了一个工厂类产生Factor,可以作为函数参数的类实现getParameter

  • 优化导向:存储层次上,设置三个层次结构,优化有关的层次结构集中于MetaData一个部分,也就是相同x指数对应的三角多项式(Trigonometric polynomial)内部进行合并

这样的设计实际上有一定的冗余度,而且并没有在存储层次上进行分割与抽离,只是从结构上看起来更像OOP了一点,并没有实质上将不同对象的特征进行提取

UML类图如下,这个图中可以很清晰的看到递归下降的过程。

 

存储层次结构的复杂度如下:

 

可以看到,把优化的任务集中在一个类上,导致了优化相关的部分(sinMergeable、Mergeable、simplify)等复杂度都过高了,这一点为后面出现bug与debug都埋下了一定的隐患。

hw3

第三次作业虽然需求上完全可以在第二次的基础上增量开发,但我感受到了极简主义的召唤,以及预估了原结构优化的难度之大,故进行了整体重构。

我将所有解析的过程重新归于Parser处理,同样采用解析的同时计算的策略,同时为了一般化处理,我将三角函数内部视为一个多项式进行存储,比较时比较二者差异即可

这次的重构在我看来充满争议性,优势在于:

  • 简化了不必要的类,解析任务集中统一化,避免了套用工厂模式之嫌(化作了抽象工厂,更简单啦)

  • 存储层次结构使用HashMap实现,找key的过程更简化,省去了无用而繁琐的维持有序的过程

问题在于:

  • 因为是最后一次单元,故没有考虑可扩展性

  • parser表示压力山大

UML类图如下:

 

复杂度分析:

 

 

本次作业我做了勾股定理(Pythagorean theorem),优化asin(x)**2+bcos(x)**2与二倍角(Double angle),优化2**n*sin(x)**n*cos(x)**n

上图中复杂度较高的部分都是我用于化简的部分,因此带来了较高的复杂度我认为是难以避免的。同时我设计了全局优化开关,可以通过开关控制是否优化,因此若是优化部分出锅可以通过关闭全局优化开关简单解决

测试

本单元三次作业我采用了两种不同的测试方案。

随机生成普通数据

第一次作业特殊情况与优化较少,边缘数据使用随机生成的数据可以达到较好的覆盖,因此我使用java内置随机数生成随机生成数据进行测试。随机数生成的过程实际就是解析的逆过程,可以称之为“递归上升法”句法构造,生成的数据也可以通过参数的设置完成customerize。

//随机数据生成
import java.util.Random;

public class Generater {
   public static final int MAX_RECURSION_DEPTH 1;
   public static final int MAX_FACTOR_COUNT 100;
   public static final int MAX_POLY_INDEX 20;
   public static final int MAX_POWER_INDEX 20;
   private static final int MAX_NUM_LEN 5;
   private Random random;

   public Generater() { this.random new Random();}

   //最长长度与长度等级
   public String generatePoly(int maxLen, int lenLvl, int curdepth) {
       StringBuilder sb new StringBuilder();
       int random.nextInt();
       if (== 0) { sb.append('+'); }
       else if (== 1) { sb.append('-'); }
       sb.append(generateTerm(maxLen 3, lenLvl 1, curdepth));
       String generateTerm(maxLen 3, lenLvl 1, curdepth);
       // 1/EPLL probability of termination each time
       while (sb.length() s.length() maxLen && lenLvl != 0) {
           if (== 0) { sb.append('+'); }
           else { sb.append('-'); }
           sb.append(s);
           generateTerm(maxLen 3, lenLvl 1, curdepth);
      }
       return sb.toString();
  }

   public String generateTerm(int maxLen, int lenLvl, int curdepth) {
       StringBuilder sb new StringBuilder();
       int random.nextInt();
       if (== 0) { sb.append('+'); }
       else if (== 1) { sb.append('-'); }
       sb.append(generateFactor(maxLen 3, lenLvl 1, curdepth));
       String generateFactor(maxLen 3, lenLvl 1, curdepth);
       int factorCnt 1;
       // 1/EPTL probability of termination each time
       while (sb.length() s.length() maxLen &&
               random.nextInt() lenLvl != &&
               factorCnt MAX_FACTOR_COUNT) {
           factorCnt++;
           sb.append('*').append(s);
           generateFactor(maxLen 3, lenLvl 1, curdepth);
      }
       return sb.toString();
  }

   public String generateFactor(int maxLen, int lenLvl, int curdepth) {
       StringBuilder sb new StringBuilder();
       int random.nextInt();
       //递归深度达到最大,只能产生非括号factor
       if (curdepth == MAX_RECURSION_DEPTH) {
           if (== 0) {
               if (!= 0) {
                   sb.append("x**").append(generateNum(0, MAX_POWER_INDEX, 1));
              }
               else { sb.append("x"); }
          }
           else { sb.append(generateNum(Integer.MIN_VALUE, Integer.MAX_VALUE, MAX_NUM_LEN)); }
      }
       else {
           if (== 0) {
               sb.append('(').append(generatePoly(maxLen 5, lenLvl, curdepth 1)).append(')');
               if (== 0) {
                   sb.append("**").append(generateNum(0, MAX_POLY_INDEX, 1));
              }
          }
           else if (== 1) {
               sb.append("x**").append(generateNum(0, MAX_POWER_INDEX, 1));
          }
           else { sb.append(generateNum(Integer.MIN_VALUE, Integer.MAX_VALUE, MAX_NUM_LEN)); }
      }
       return sb.toString();
  }

   public String generateNum(int min, int max, int maxLen) {
       StringBuilder sb new StringBuilder();
       int random.nextInt();
       int max1 1;
       int localMaxLen = (== 0) maxLen : (== 1) maxLen 1 : maxLen 2;
       localMaxLen Math.max(1, localMaxLen);
       for (int 0; localMaxLen; ++i) { max1 *= 10; }
       int min1 Math.max(min, -max1);
       max1 Math.min(max1, max);
       int min1 + (int) ((max1 min1) Math.random());
       if (== && >= 0) { sb.append('+'); }
       //随机产生一个零
       if (== 0) { sb.append('0'); }
       sb.append(n);
       return sb.toString();
  }
}

生成的随机数据效果:

++110*-566*+33903*-233*+778*(x**+5*x**5)
-+1464*+213*291*x**+02*45449*753
+--13586*3318*-60845*(++x**4+-x**06)**+7
-+x**5*(0-436*x*x*x*x)**1*(-x**+6*x**+8)++x**8*+92813*+0706
+x**+06*(-x**+08*662)**+7*(-x*x**3*-75)
--x**+01*-779*(+-413*x*x*+779)**+7*-961
+(0-45825*x**3)**05*(x**+4*-803)*+202
++x**02*x**3*+483*x**5*-37486*-1214--8796*+362*+50204
+(-257*x-+x**+07)**+09*1807*-517
+061*(0-48*+3355++0871)**03*163
+++3313*(+x**+6*x*x-x-+-71017)**+2
-+451*-53++x**+07*x**3*x**+9*(--96003*x**+0)**01
手动构造边缘数据

第二次作业开始,随着题目、优化的复杂度上升,随机生成的数据很难有充足的说服力,极端数据的构造更依赖于我们自主设计。

我构建数据的目标是完全覆盖,虽然最后失败了,但不失为一次尝试(

这里我针对两种数据进行了测试:功能性数据优化性数据,优化性数据旨在保证优化的正确性,用于探寻优化空间与优化可能出现的不期结果

0
0

0
(0)**0

0
x**+2*cos((cos(x)**3)**003)**+2

2
f(y,z)=sin(z)*cos(y)
h(x,y,z)=x
x**+02*f(x,h(cos(cos(x)),1,2))

0
x**+3*cos(sum(i,-1,3,cos(i)))

0
x**+5*(sin(x)+cos(0)+sin((cos(x)-sin(x)))+sin((sin(x)-cos((-x))))

0
sin(0)**0

0
+-cos(1)--cos(-2)+sin(1)*sin(-2)

0
cos(0)**10

0
sin(-2)**2

0
sin((-x))**2

0
sin((-x))**3

0
cos((-x))**3

0
sin(7)*cos(2)*sin(-6)

0
(sin(1))**5*x*x*-4*(cos(2)**4)**+03

0
(sin(x)**2+cos(x)**2)**5

0
sin(x)**6+sin(x)**4*cos(x)**2+sin(x)**2*cos(x)**2+cos(x)**2

0
sin(x)**10+5*sin(x)**2*cos(x)**8+10*sin(x)**6*cos(x)**4+5*sin(x)**8*cos(x)**2+cos(x)**10+10*sin(x)**4*cos(x)**6

0
3*x*sin(x**2)**2-x*cos(x**2)**2

0
(1-cos(x)**2)*cos(x)**2+sin(x)**2*cos(x)**2

0
(1-sin(x)**2)*cos(x)**2+sin(x)**2*cos(x)**2

0
sin(4)*cos((-sin(x))*sin(-3)*sin(sin((-x)))*+32

0
sin(cos(sin(cos(x)-sin(x)+x**3-sin(x))))*sin(cos(sin(-2*sin(x)+x**2*(x+cos(0)-1)+cos((x**2-x-x**2)))))

0
sin((-x*cos(x)))**2+3*cos((x*cos((-x))))**+002

0
1024*sin((-x))**10*cos((x+x**2-x*x))**+0010

0
(x*cos(x)**2-x)**6

0
sum(i,1,5,(x**3-sin(i**2)**2*x**3))

0
sum(i,6,5,sin(x))

0
sum(i,6,-23,sin(x))

0
sum(i,-3,4,(sin((x**3))*cos(i)*cos((x**3))))

1
f(z,y,x)=x*sin(z)+-(-9*sin(y**2)+-x**0*sin(z))**2
f(x,x**2,x**3)*sum(i,-1,1,(sin(i)*i))

1
f(x)=x*cos(x)
f(sum(i,-2,3,sin(x)))

1
f(z)=-0032
f(sum(i,3,2,cos(i)))

1
f(x)=sin(x)
f(f(f(cos(x))))+(cos(x)-sin(x))**2+sin((2*x))**2

2
f(z)=z**2+1
h(x,y,z)=x*y+y*z+sin(z)*cos(y)
h(f(sin(cos(x))**3),(x+1)**2,sum(i,-1,1,cos(i)))

1
f(x)=x**10
f(+2)*f(x)*f(cos(x))*f(sin(x))
反思

手动构造数据很大程度上受思维影响,我们想到的极端情况自然很容易构造,但如何证明覆盖的完备、保证功能的完全测试则很困难。不通过形式化验证(Formal Verification),我们很难真正做到完全覆盖。

对于这一令人沮丧的现实,我给出的答案止步于倾尽思绪,共享数据,希望后续设计中可以进一步补全我在数据构造与测试领域的技术空缺。

bug

本单元我的bug均来自于hack@_@ 充分说明了两点:强测过得很侥幸 与 完备性测试的困难。

hw2

本部分我被hack的样例如下:

//input
0
cos(2)*cos(3)
 
//output
cos(2)**2

出现错误的部分复杂度如下:

 

这一bug产生的主要原因在于:我手动维持了列表的有序带来了高复杂度,而没有选用合适的容器导致逻辑复杂度过高,没有关注到充分的细节,以后应当尽量使用java内置的sort简化排序这一过程,手动排序只会带来复杂度与低性能,只需要实现两两比较的序即可

同时这一部分我身边与互测房间中同学存在的主要问题在于简化三角函数内符号而没有考虑到外指数是否为奇数

hw3

本部分我被hack是由于优化出现问题导致,同时优化测试未完全覆盖,因此出现了问题,样例如下:

//input
0
cos(2)*cos(1)**4-cos(2)**2*cos(1)**2
 
//output
-cos(2)*cos(1)**2*sin(1)**2

本部分其他同学出现的问题主要集中在三点:

  • 没有使用BigInteger读取sum循环上下限导致循环变量越界

  • sum函数内无法正常处理常数n次方问题

优化

最后来谈谈第三次作业的优化。做完三角和二倍角优化后,我意识到优化顺序很大程度上影响了优化性能效果,而优化顺序又使不可预知的,因此我认为只能通过遍历所有优化结构,将优化的几种不同策略作为子节点,设置优化树最大深度,然后向下完成遍历并剪枝,更新当前最短优化序或者优化结果。

最后我没有实现这一过程,主要由于我认为设置这一过程只能完成对很少数极端数据性能的提升,却带来了过高的时间复杂度,得不偿失

总结

对于本单元我的设计,客观的评价是好坏参半。

做得较好的部分:

  • 优化,三次作业强测得分100 100 98.3

做得不足的地方则更多:

  • 架构设计未充分体现OOP

  • 测试覆盖度不够

  • 未完成对比脚本的编写,不够自动化

展望:

  • 我喜欢简单,但可扩展性总是伴随着冗余度的增加。为了能够在simplinism与架构可扩展性之间取得平衡,我想我应该对冗余有着更高的包容

  • 希望下个单元可以感受到设计模式的精巧之处。

posted @ 2022-03-23 19:33  Lumyn  阅读(188)  评论(2编辑  收藏  举报