301
社区成员
发帖
与我相关
我的任务
分享本单元的主体内容是表达式的化简,得益于上学期先导课程的学习,我对于工具链、Java 语法、基本的面向对象编程思想等有了一定的基础,本单元的任务就没有了过多设计以外的难度。
在这三次作业中,我初步尝试应用了抽象层次结构,对于各种因子,一开始使用了实现 接口 来统一行为,后来发现因子之间可以有一些共同的属性,因此改为继承 抽象类 。最终的层次还是有瑕疵的,我会在后面进行分析,以期将来做出更好的设计。
在整体的 架构 方面,我整体的架构比较稳定,只是在第二次作业强测后,因为出现了爆内存的问题需要重新划分部分类功能(主要是运算化简功能)。
本单元的学习中,我对于 SOLID原则 有了更深刻的理解,尤其是其中的 单一职能原则 和 开闭原则 。由于本单元中我使用的抽象层次比较简单,其它三项原则的重要性还没有得到明显的体现,还需要我在未来的学习实践中多加体会。
在 测试 方面,我主要都是采用自动生成大量数据,自动运行并检验的“广撒网”式测试,并根据 hw1 的要求制作了一个简易测评机(后两个作业因为数据生成器比较复杂,而且有大佬的测评机用就没有动手更新)。这种测试方法比较“拼人品”,从测试的角度来说技术含量并不高,不能保证做到全覆盖式测试,这就导致了我 hw3 强测中出现了触发条件比较苛刻,但是其实含金量并不高的错误。以后的作业中还需要进行更多的针对性测试,提高自己的测试水平。
| 类名 | 属性个数 | 方法个数 | 源码行数 |
|---|---|---|---|
| MainClass | 0 | 1 | 13 |
| Parser | 1 | 4 | 78 |
| InputProcessor | 1 | 1 | 12 |
| lexer.Lexer | 4 | 5 | 106 |
| lexer.Token | 2 | 2 | 15 |
| expression.Factor | 0 | 3 | 6 |
| expression.VarFactor | 2 | 4 | 33 |
| expression.Expr | 2 | 11 | 103 |
| expression.NumFactor | 2 | 5 | 44 |
| expression.Term | 1 | 6 | 96 |
| ans.FinalExpr | 2 | 3 | 107 |
| ans.FinalTerm | 2 | 4 | 33 |
| ans.FullVar | 2 | 2 | 15 |





我在本次作业的评测中没有出现 bug,也没有 hack 到别人的 bug。
主要的测试思路就是用大量样例自动测试,为此制作了一个简易评测机,包括数据生成器、正确性检验器、评测机主体三大部分,具体说明如下:
数据生成器:
用 Java 完成,生成数据的过程类似于解析表达式的逆过程,根据形式化定义随机生成表达式即可。
在随机生成的过程中,我会记录每一个单元的 cost 并按照规则计算出总和,最终用 cost 和表达式长度对生成的数据进行筛选,保证数据的有效性,但是也一定程度上损失了强度,而且生成缓慢。
实力不足,没有想到怎样增加数据的强度,只能大量生成。
类图:

正确性检验器:
采用 python 搭建,总体思路就是读入原表达式和化简后的表达式,用 sympy 库实现化简和展开括号,验证化简结果是否与读入的化简结果等价,即可判断正误。
值得注意的是,sympy 库的化简功能并不支持前导零,需要利用正则表达式进行预处理。此外 python 中的乘方符号与作业要求不同,需要加一步替换。
源码:
# Checker.py
import re
import ast
from sympy import parse_expr
from sympy import expand
def pre(s):
result = re.sub("[\t ]+", "", s)
result = result.replace("^", "**")
result = re.sub(r'\b0+(\d+)\b', r'\1', result)
return result
def are_expressions_equal(expr1, expr2):
try:
# 解析两个表达式的AST
ast1 = ast.parse(expr1, mode='eval')
ast2 = ast.parse(expr2, mode='eval')
# 比较两个AST是否相同
return ast.dump(ast1) == ast.dump(ast2)
except (SyntaxError, ValueError):
# 如果表达式无法解析,返回False
return False
def check(expr, ans_str, lines):
expr = pre(expr)
std_str = str(expand(parse_expr(expr)))
print("std : " + std_str.replace(" ", "").replace("**", "^"))
lines.append("std : " + std_str.replace(" ", "").replace("**", "^") + "\n")
print("ans : " + ans_str.replace("\n", ""))
lines.append("ans : " + ans_str.replace("\n", "") + "\n")
ans_str = pre(ans_str)
ans_str = str(parse_expr(ans_str))
if are_expressions_equal(ans_str, std_str):
print("AC!\n")
lines.append("AC!\n" + "\n")
return 0
else:
print("WA!\n")
lines.append("WA!\n" + "\n")
return -1
评测机主体:
采用 python 搭建,主要负责调用以上两个部分,并进行记录,负责与文件交互。
主要逻辑是调用数据生成器生成指定数量的数据,然后依次调用 toHack 目录下的所有 jar 包,获取其输出并使用 Checker 进行检验,如果出错则立即终端循环并报告错误信息。
源码:
# MainTester.py
import os
import Checker
def test(round_per_time, cur_time, total_time, targets):
print("Getting data ...")
os.system("java -jar data_generator.jar > data.txt " + str(round_per_time))
print("Done")
data = open("data.txt", "r").read().splitlines()
lines = []
correct = True
for j in range(round_per_time):
print("time " + str(cur_time) + "/" + str(total_time) + ":", end="\t")
lines.append("time " + str(cur_time) + "/" + str(total_time) + ":" + "\t")
print("round " + str(j + 1) + "/" + str(round_per_time) + ":")
lines.append("round " + str(j + 1) + "/" + str(round_per_time) + ":" + "\n")
expr = data[j * 5 + 1]
with open('in.txt', 'w') as java_in:
java_in.write(expr)
for tar in targets:
print(tar + " :")
lines.append(tar + " :")
os.system("java -jar toHack/" + tar + " < in.txt > out.txt ")
with open('out.txt', 'r') as java_out:
ans = java_out.readline()
if Checker.check(expr, ans, lines) == -1:
correct = False
print(tar + "ERROR!")
print("expr : " + expr)
break
if not correct:
break
with open("log.txt", "w") as log:
log.writelines(lines)
if correct:
print("You're so NEWBEE!\n")
return False
else:
return True
target = os.listdir("toHack")
times = eval(input("How many times?\n(100 rounds per time)\n"))
for i in range(times):
if test(100, i+1, times, target):
break
input("press enter to continue ...")
此外,我还制作了一个可以手动输入样例并自动对 toHack 目录下的所有 jar 包进行测试的 python 程序:
# ManualHack.py
import os
import re
import Checker
ctn = True
while ctn:
target = os.listdir("toHack")
expr = input("your expression:\n")
with open('in.txt', 'w') as java_in:
java_in.write(expr)
lines = []
for tar in target:
print(tar + " :")
os.system("java -jar toHack/" + tar + " < in.txt > out.txt ")
with open('out.txt', 'r') as java_out:
ans = java_out.readline()
if Checker.check(expr, ans, lines) == -1:
print(tar + " ERROR!")
print("expr : " + expr)
break
with open("log.txt", "w") as log:
log.writelines(lines)
ctn = re.match("[yY]", input("Enter Y to try again ...\n"))
第一次作业的要求比较简单,因此我只做了一些结果上的优化,没有考虑计算过程的开销。
结果的优化:
最终输出的表达式可以化成统一形式:

基本的优化包括:
此外还有一个比较隐蔽的优化点(从张奕彤同学的分享贴中学到的),即“正项提前”。例如: -x+1 比 1-x 长。
为了完成这项优化,我采用的方法是:先遍历最终表达式中的所有项,找到第一个正项就先输出并 删除 该项,这导致了我的 toString 方法修改了对象本身的字段,导致 debug 时出现了意想不到的错误(因为 IDEA 的 debug 功能会自动调用对象的 toString 方法)。
| 类名 | 属性个数 | 方法个数 | 源码行数 |
|---|---|---|---|
| MainClass | 0 | 1 | 13 |
| Parser | 1 | 10 | 114 |
| InputProcessor | 1 | 1 | 17 |
| SelfDefFuncTemp | 3 | 2 | 47 |
| lexer.Lexer | 4 | 5 | 114 |
| lexer.Token | 2 | 2 | 15 |
| std.StdPoly | 2 | 6 | 88 |
| std.StdMono | 3 | 10 | 150 |
| expression.Factor | 1 | 5 | 33 |
| expression.VarFactor | 1 | 1 | 21 |
| expression.Expr | 1 | 6 | 79 |
| expression.NumFactor | 1 | 2 | 37 |
| expression.ExpFactor | 1 | 3 | 55 |
| expression.Term | 1 | 4 | 79 |





我在本次作业的强测中出现了一个数据点 内存超限 的问题,经过分析,我认为问题出在计算过程缺少优化,冗余计算太多导致需要同时管理过多的对象。
优化的方式是:
我的 hack 策略主要是两方面结合:一方面通过大量样例批量化测试,另一方面则是从特殊样例出发,主要针对格式进行攻击,包括指数不能为负、"-x" 不是单一因子、计算结果为0不能输出空串等。
本次作业中,计算性能上的优化已经在 bug 部分说过了,结果上的优化做得比较保守,除了与上次相同的几点基本优化之外,只做了一个很保守的优化:用正则表达式,将形如 "exp((a*x))" 的式子替换为 "exp(x)^a"(其中 a 为正整数)。
替换代码:
ans = ans.replaceAll("(exp\\(\\()([0-9]+)(\\*)(x\\^?\\d*)(\\)\\))", "exp($4)^$2");
此外,完成本次作业的过程中,由于明白了在 toString 方法中修改对象内字段会导致各种意外,我修改了对象管理方式和求值方法,平时用 HashSet 管理内部的标准多项式,然后在 toString 方法中用该集合创建一个 ArrayList,并按系数进行排序。
关键代码:
// ...
public class StdPoly {
private static final Comparator<StdMono> cmp = (o1, o2) -> o2.getCoe().compareTo(o1.getCoe());
// ...
@Override
public String toString() {
ArrayList<StdMono> stdMonoList = new ArrayList<>(stdMonoSet);
stdMonoList.sort(cmp);
// ...
}
}
| 类名 | 属性个数 | 方法个数 | 源码行数 |
|---|---|---|---|
| MainClass | 0 | 1 | 13 |
| Parser | 1 | 11 | 126 |
| InputProcessor | 1 | 1 | 17 |
| SelfDefFuncTemp | 3 | 2 | 47 |
| lexer.Lexer | 4 | 5 | 117 |
| lexer.Token | 2 | 2 | 15 |
| std.StdPoly | 2 | 11 | 145 |
| std.StdMono | 4 | 13 | 190 |
| expression.Factor | 1 | 5 | 35 |
| expression.VarFactor | 1 | 1 | 21 |
| expression.DerFactor | 1 | 1 | 25 |
| expression.Expr | 1 | 5 | 70 |
| expression.NumFactor | 1 | 2 | 44 |
| expression.ExpFactor | 1 | 3 | 55 |
| expression.Term | 1 | 4 | 79 |






标准单项式:

if-instanceof 等“坏味道”存在,保有过多分支会导致一些触发条件相对严格的 bug 不容易被测试出来。本次作业需要修改的量并不大,但是由于粗心出现了两个很简单但是比较隐蔽的小错误,由于测试不彻底没有被发现,最终在强测暴雷了。
本次作业的 hack 策略与上次类似,“广撒网”和针对格式攻击。
我的针对性测试能力尚且大大不足,可能还是有很多触发条件较为苛刻的 bug 测不出来,而且也没有针对递归层数、内存或时间限制进行测试,这方面能力有待提升。
本次作业的计算过程优化没有变化,结果长度优化则仍然比较保守,增加了针对所有 exp() 括号中所有系数绝对值相等的情况的提公因式,因为担心负优化,没有做更多的提公因式和拆分(也是因为码力不足)。
后来经过反思,其实还可以加一条提最大公因式,然后比较提取前后的长度,如果出现负优化,取消优化即可。
第一次作业中,我猜测后期迭代中会加入多变元的情况,因此在变量因子中,预留了变量名不同的可能性,并在最终项中用列表存储变量因子,显得很不优雅,最后这个设计部分地派上了用场,即可以用来解析自定义函数定义式,但是最终的结果中并不会有多种变量,因此我在第二次作业中就将变量列表删除了。
此外,我在第二次作业中修改最终输出模式时,采用的方法是求改原有的单项式模板,而不是新建类并进行聚合,违反了开闭原则,由于我们的迭代次数较少,且只改变了一次输出形式(增加exp),所以影响不大,但是这三次作业我已经很明显地感觉到记不住自己一开始设计某个类的用意了,如果以后更大的项目中还这样修改而不是新增的话很有可能会增加大量的工作量并且会埋下一些隐患。
总之,今后的设计一定要尽可能遵循 SOLID 原则,让“自己和自己的合作”更加顺畅。
比如说我们未来需要新增三角函数因子,则需要做一下四个修改:
本次作业的运算过程中,我们需要很多对对象的拷贝,因为我们有多层自定义类对象的嵌套,如果只是浅拷贝就会导致运算出错,所以我们需要进行深拷贝。
那么,在不依赖第三方包的情况下,我们都有哪些方法可以完成这一任务呢?
为需要拷贝的类新增一个拷贝构造函数,即设计一个传入参数为本类对象的构造函数,然后将自身的所有属性都设置为参数对应的属性(遇到引用类型需要调用属性的拷贝构造方法)。
序列化就是保存数据的时候,保存数据的值和数据类型
反序列化就是在恢复数据时,恢复数据的值和数据类型
需要让某个对象支持序列化机制,则必须让其类是可序列化的。为了让某个类是可序列化的,该类必须实现如下两个接口之一(他的每个属性也必须实现这两个接口之一)
Serializable
Externalizable
我们用序列化反序列化实现 clone 的原理就是我们相当于是把对象保存到了一个文件中,然后立刻又从文件中把这个对象读了出来,此时这个新的对象就是一个深度 clone 的副本了。
标准写法:
public class SerialCloneable implements Cloneable,Serializable
{
public Object clone()
{
try
{
//save the object to a byte array
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject(this);
out.close();
//read a clone of the object from the byte array
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream in = new ObjectInputStream(bin);
Object result = in.readObject();
in.close();
return result;
}
catch(Exception e)
{
return null;
}
}
}
——以上来自学长博客“钟鼓楼”
在本单元的学习和实践中,我意识到自己在面向对象设计方面还有更多需要学习的知识。本来觉得经过上学期先导课的训练,我已经对 OO 有了比较完备的理解,但是这单元的学习让我意识到我的代码中还有很多“不 OO”的元素存在,这需要我在未来的学习实践中更多地学习他人的架构,从学长的博客和讨论区多多吸收知识,不必一拿到题目想一想就开始动手写代码,可以在自己的腹稿初步成型后多多吸取别人的优秀想法,博采众长。
另外,我觉得荣老师上课经常强调的一个思想很有道理,即虽然你是在一个人完成整个工程,但应该认为是“自己雇佣自己”,即规定好每个模块的接口之后完全模块化设计,模拟一种合作式开发的感觉,我想这对于我们的代码向“高内聚,低耦合”方向发展很有好处,也利于我们未来参与多人合作,开发更大的项目。
如果可能的话希望每个强测点能有一个标准优化解,如果性能比这个解还要好计入加分而不是对别的同学扣分。