382
社区成员
参考第一单元的训练任务,我建立了本次作业的基本框架。
首先是解析输入的部分,我根据训练任务中的 Parser 和 Lexer 构建了自己的 Parser 和 Lexer,结构上基本没有改变。
然后是存储表达式的部分,Factor 作为接口,Power、Expr、Term 实现这个接口。
不管是 Expr 和 Term,在存储时我都没有用哈希表的结构,但在表达式化简时,我使用了 Hashmap 结构。考虑到之后各种迭代,我需要对每一个 Term 对象得到一个特异的特征值,只有同类的 Term 才会有相同的特征值。基于学习成本、时间成本等因素,我最后决定使用字符串导出一个 Term 对象的特征值。
对于两个 Term 分别为 3*x*y 和 y*-6*x,这两个 Term 的类型应该是一样的(可以合并的)。我在设计 Term 时已经把常数项单独处理,因此 3、-6 等可以不用考虑,在导出字符串特征值时,应该优先对各个项按照字典序进行排序,否则无法保证同类型 Term 特征值的唯一性。
我的第一次作业在测试中只出现了一个 BUG,即连续加减号的处理错误。
在做第一次作业时,我一直误以为作业的输入规则与 sympy 的规则相同,因此我认为类似于“+-+5*x”的表达式不会成立。实际上我的认为是错误的,这是我没有认真分析作业书的问题。
这个 BUG 出现在对输入字符串的处理上。我在强侧中提交的代码对字符串的处理为:
private static String stringPreProcess(String s) {
return s.replaceAll("[ \r\t]", "")
.replaceAll("-\\+", "-")
.replaceAll("\\+-", "-")
.replaceAll("--", "+")
.replaceAll("\\+\\+", "+")
.replaceAll("\\*\\*\\+", "**");
}
这个函数不能处理三个连续正负号的情况。BUG 修复中,我将替换重复了一次:
private static String stringPreProcess(String s) {
return s.replaceAll("[ \r\t]", "")
.replaceAll("-\\+", "-")
.replaceAll("\\+-", "-")
.replaceAll("--", "+")
.replaceAll("\\+\\+", "+")
.replaceAll("-\\+", "-")
.replaceAll("\\+-", "-")
.replaceAll("--", "+")
.replaceAll("\\+\\+", "+")
.replaceAll("\\*\\*\\+", "**");
}
即可通过强测所有点以及 HACK 的测试点。
我在 Python 中设计了自己的数据生成器,其生成逻辑为:
def generate(recursion=1):
ans = ''
expr_len = random.randint(1, 6)
for i in range(expr_len):
item_len = random.randint(1, 4)
ans += str(random.randint(-5, +5))
for j in range(item_len):
ans += '*'
if recursion > 0 and random.randint(0, 4) == 3:
ans += '('
ans += generate(recursion - 1)
ans += ')'
if random.randint(0, 4) == 3:
ans += '**'
if random.randint(0, 1) == 0:
ans += '+'
ans += str(random.randint(0, 4))
else:
if random.randint(0, 4) != 3:
ans += value_map[random.randint(0, 2)]
if random.randint(0, 4) == 3:
if random.randint(0, 4) == 3:
ans += '**'
if random.randint(0, 1) == 0:
ans += '+'
ans += str(random.randint(0, 4))
else:
ans += str(random.randint(-10, 10))
if i != expr_len - 1:
if random.randint(0, 1) == 1:
ans += '+'
else:
ans += '-'
return ans
这个数据生成不能覆盖所有的数据,比如上面提到过的连续三个正负号。
这一次作业我算是进行了一次小型的重构。我重新构造了两个接口和一个虚类。
Base 接口:这个接口被所有的表达式相关类实现,它只有一个方法是 multiply。
Irreducible 接口:这个接口被 Sin,Cos 等类实现,这个接口中包含一个方法 innerSimplify。他的设计中实现这个接口的类是拥有内部表达式,并且这个内部表达式的化简不会对外部产生任何影响的。举个例子来说,sin((2*x)) 永远不会被化简为 2*sin(x)*cos(x)。 这个接口以后还可以 implements 到 exp、ln 等类上。
Factor 类:这是一个虚类,所有的因子都继承于这个类。
在处理自定义函数时,我将自定义函数的右边作为一个完整的 Expr 进行解析。在解析为 Expr 后,先将其进行一次展开和化简。当在表达式中遇到自定义函数时,将这个自定函数的 Expr 导出为字符串再进行字符串替换。事实表明,使用字符串替换是简单且高效的自定义函数代入方法。
对于可嵌套的括号,我在 Homework 1 中就支持了这项功能,现在只需要在输入解析中添加递归逻辑即可。
这次作业和 Homework 1 的 OO 度量基本相同。
此次在互测中表现出的 BUG 是上述细节处理中,将自定函数的 Expr 导出为字符串时未先将其展开和化简。在我的设计中,Expr::toString() 方法只对进过展开化简过的对象有作用。如果 Expr 中存在为 0 的项,那么 toString 方法不会在 0 的前面添加任何符号。因此“2+0+3”未经化简时可能会输出“20+3”的结果。
这次对基本架构没有改动。与 Homework 2 相比,我只是在 Base 类中添加了 diff 方法。
这次要实现的求导功能其实和乘法功能十分相似,任何一种因子或者项都可以进行求导,而求导后不一定能够保证保持原来的类型不变。
此次新添加的 diff 方法在各种复杂度上都很小。
此次作业在强测、互测中未出现 BUG。
设计一个好的程序框架对后续的扩展开发十分重要。
我在迭代 Homework 2 时,对各个类的共性、异性进行过不同角度的分类处理。这耗费了我很多时间,也曾经让我的内心一团乱麻(那是真的麻)。但是确定完架构后,不管是细节的实现还是 Homework 3 的迭代,都变得非常的容易。