迭代、Hacks与优化
hw1
需求分析
第一次作业要求实现一个仅包含整式的、括号最多嵌套一层的单变量('x'
)括号表达式化简。要求输出中不能包含括号。
架构与实现
第一次作业相对简单,只是一个预热,但也强度不小。处理表达式化简的问题,需要先理解何为递归下降法。这个方法的核心思想在于,利用表达式的特征结构,把整个表达式逐层向下展开分析,最后得到一个树形的结构,然后再自底向上计算答案。
然而仅是理解了方法,距离落实到代码上还很远。我们需要一个方便简洁的架构来实现我们的构想。参考第一次训练的代码,我设计了外层用Lexer
和Parser
分析输入的表达式字符串,然后Parser
按照表达式的逻辑转化成内部的由Expr
、Term
、Number
和Pow
构成的递归下降树形结构,最后再使用多项式类Poly
来计算答案这样的结构。
Lexer
是最外层的、仅对输入表达式字符串进行词元划分的分析类。根据多项式的格式,我把词元划分成了以下类型:EOF、无符号纯数字、'x^...'
、'('
、')^...'
、'+'
、'-'
、'*'
。如果在'x'
和')'
后没有幂次,手动补全幂次为1保持格式统一。Lexer
的核心方法是Lexer.next()
,其作用在于分析下一个词元并存放到curToken
中,然后向前移动字符串指针。可以调用Lexer.peek()
查看curToken
。
Parser
是根据Lexer
传递的词元分析生成树形结构的类。在构建此类之前,我先构建了Expr
、Term
、Number
和Pow
四个类,分别表示表达式、项、数字、幂函数。其中除Term
外的三类都实现了Factor
接口,表示他们是因子。Expr
包含一个ArrayList<Term>
,表示表达式由一些项构成;Parser
包含一个ArrayList<Factor>
,表示项由若干因子相乘而得。在Parser
中,main
调用parseExpr
,parseExpr
按照构造规则,多次调用parseTerm
。parseTerm
又按照构造规则调用parseExpr
、parseNumber
、parsePow
,形成一个互相调用,逐级向下的结构。
最后我使用Poly
类计算答案。考虑到第一次作业的答案形式比较固定唯一,一定是一个多项式,所以我构建了Poly
多项式类。Poly
类包含一个HashMap<Integer, BigInteger>
,键表示多项式内项的指数,值表示多项式内项的系数。利用Parser
构建表达式树后,直接调用构造函数Poly(Expr)
构造答案。其内部会向下递归调用Poly(Term)
、Poly(Number)
、Poly(Pow)
等其他构造函数,遍历整个表达式树,并实现加法乘法辅助计算。
优化
由于第一次作业答案形式唯一,并且在Poly
的计算中已经体现了同类项的合并,所以并没有太多的可优化内容。我实现的唯一优化是找到最终答案中是否有正项,如果有就可以提到最前面,这样可以省下一个符号的长度。
Hacks
第一次作业房间里没有任何成功的hack。
结果
第一次作业顺利获得了满分,互测中也没有被hack。
hw2
需求分析
在第一次作业的基础上,加入了两个新内容:其一为自定义函数,规定定义内部不会嵌套,也就是自定义函数定义内不会有任何自定义函数;其二为加入了指数函数exp()
。
架构与实现
针对自定义函数,我的解决方法是在最开始就把函数带入表达式中,做字符串替换,不涉及到后面表达式分析与处理。我新建了两个新类Func
和FuncKilla
,前者内部保存一个函数,包括一个String name
表示函数名,一个ArrayList<String>params
表示参数,一个String definition
表示函数的定义;后者包括一个HashMap<String, Func>
存储所有函数,其核心方法为FuncKilla.killFuncs(String input)
,它接受包含自定义函数的输入,返回消除所有自定义函数后的纯表达式。处理过程中需要注意一些细节,例如自定义函数代入参数时需要加入额外括号,自定义函数本身替换完成后也需要加括号,为了防止出现运算符优先级相关的问题。
针对exp
,首先对Lexer
和Parser
做相应的修改。然后加入Exp
类,内部包含一个Expr
表示指数上的内容,并实现Factor
接口,这与第一次迭代相关内容并无大异。改动最大的部分是Poly
部分。虽然此次迭代答案不再一定是多项式,但是我仍然希望沿用第一次迭代求答案的形式。然而,Poly
内部不能再用 HashMap<Integer, BigInteger>
存储,这就需要另动脑筋。
第一次迭代中,键的Integer
表示x^...
,考虑到这次有指数函数,我新建了Unit
类,包含一个Poly p