还没从寒假的懒散惬意中摆脱出来,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接口。
由此完成表达式树的构建。
递归解析生成的表达式树,最终生成一个ArrayList<Unit>用来存储解析表达式生成的所有基本单元Unit。
\[ArrayList<Unit>\space→ \sum a_i*x**b_i \]最后将得到的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 \]综上可以完成化简部分。
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) *****************
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>>
的形式保存于该类中,索引为函数名,内容是函数的各个参数和表达式;
细节处理
对于求和函数,我定义了求和函数类,通过字符串替换方式得到求和函数的等价表达式(这种方式其实破坏了表达式的结构性,但是由于懒惰·····),形如\(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:
这样我们在解析到任何基本因子(除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\)之间的转换、二倍角公式的使用····由于实力有限,没有在思考处理,实属遗憾。
自己的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了(因为互测的数据自己都过不了)。
纠结中,我选择了重构。虽然路途艰辛,但是光亮尽在前方照耀。
于是我选择了重构。
这是第二次作业遇到幂函数(其余同理)声明的因子:
第三次作作业声明则转变为:
可以看到不仅节省了大量的空间消耗,也使得结构更加清晰,虽然后续处理过程较为麻烦,但是我认为良好的结构层次才是我们应该学习的重点。
声明时对替换过最外层后形成的新表达式(sb)进行解析:
这样实际上我们在声明含有嵌套表达式的时候只需将最外层嵌套替换,因为声明形成的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 A = new FactorType();
通过这种方式构建化简表达式树,逻辑清晰,可阅读性强(更可能被人观赏(bushi~)。
Term执行因子相乘时,不是笼统的完成2个ArrayList<Unit>相乘,而是根据因子的不同类型进行不同的乘法操作(灵活性较强,降低空间复杂度,提升性能):
合并同类项时由于三角函数列表长度不定、三角函数内嵌表达式等价形式多样,因此我们需要更加适用的判等方式。
sin((x**2))**3 -> sin(x**2)**3 sin((x+1)) -> sin((x+1))
此时我们需要在Unit增添判断三角函数内部是否只含有一个基本因子的函数hasOneFactor(),以保证任何情况下三角函数表达式输出的括号层数都是合法且最少的。
至此,重构完成!
强测中出现了一个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”时直接用“”替换 |
互测他人时以手造数据为主。
发现了2个bug,第一种同我的互测第一个bug,第二种:
Bug数据 | Bug结果 | Bug分析 |
---|---|---|
sum(i , -1 ,1 ,(sin(i**0)*(i))) | -sin(-1)+sin(1) | Sin(i**0)在替换时发生错误 |
本单元作业最大的收获是对于 Java 的了解更加深入了,包括各种类库的使用方法、工厂模式、层次化编程、面向对象程序架构设计理念。另一个重要的收获就是对于递归下降算法的深入理解。递归下降的理念非常简单,实现非常巧妙,而功能强大。
除此之外,迭代、重构是这一单元的主题,一个好的架构可以起到事半功倍的效果。为了偷懒,往往达不到目的。第二次作业由于仍选择在架构较差的第一次作业基础上添砖加瓦,导致给第三次大重构带来了不小的压力和挑战。我认为重构也是一种智慧,智者说:“实践出真知。”我经历了大重构,方可知晓重构前后的构建差距、性能优劣、拓展性强弱,在对比中择优,并加深对优秀架构的理解。
自动评测机的编写以及边界数据的构造也是第一单元测试不可或缺的部分,它们不仅能找到自己的程序中的 bug,还能帮助屋子里的小伙伴一起debug。
第一单元是我初次接触面向对象编程,学到了很多新知识,但仍存在很多不足。不过细想,尽管遇到这么大的困难,我仍然不卑不亢的走了下来,虽然无法尽其美,但也在光辉三月中播撒了珍重的汗水,收获了艳丽的阳光。