第一单元作业总结
注:本博客中的类图借助了IDEA的绘制工具,但是并不是由IDEA无脑生成的,而是自行绘制的,请助教明鉴
作业1
作业1框架构建
预处理方法与词法分析
预处理方法
为什么要预处理?
因为在通读完题目后,我们会发现,题目的输入规则和我们经验中的数学表达式存在非常明显的区别。比较明显的有:
- 允许一个数学概念上的项之前,存在多个+|-
- 在全表达式的各处都有可能插入不同长度的空白
- 允许数字有前导零
为了解决这些区别,避免在后续的分析中给自己添加思维负担,我们不妨先对输入的字符串进行一步预处理,解决掉前两个问题(第三个问题放在词法分析里比较容易解决)。
第二个问题很好解决,只需要用正则表达式[ |\t]+
替换掉输入字符串中的所有空白就可以了。那第一个问题怎么解决呢?第一个问题的本质是,输入的字符串可以被[+-]+
分割为若干子串。那就会有一个比较显然的想法,用正则表达式匹配输入字符串中所有的[+-]+
,同时获取输入字符串被[+-]+
分割后的结果,再把它们按照以下规则拼起来:
int index = 0;
while (matcher.find()) {
sb.append(parts[index]);
index++;
sb.append(characterTackleTool(matcher.group()));
}
其中,sb
存储答案,parts
存储输入字符串分割后的结果,matcher是匹配后的结果。
而characterTackleTool
方法,它的逻辑是这样的:判断给定的字符串中-
的个数,若minusNumber%2==1
,则返回-
,反正返回+
。
这样,字符串经过预处理,就比较接近我们理解中的数学表达式了。
词法分析
接下来做词法分析。本次作业的词法分析和助教学长提供的很像,只是多了一些符号,直接添加上去就可以了。
但是需要注意的是刚才提出的第三个问题:当读入数字时,可能会有前导零的问题。需要吃掉前导零,并判断是否整个数字全是0,若是则把0添加回去。
在我的代码结构中,Token一共有9类:
public enum Type {
NUMBER, VARIABLE, PLUS, MINUS, MUL, DIV, LPAREN, POWER, RPAREN
}
表达式乘法与合并同类项
表达式乘法
我选择先从表达式乘法讲起,这是因为为了适应这次作业的内容,我在递归下降获取Term时真的做了连乘。
表达式乘法的思路可以完全模仿人类进行表达式相乘时的思路:对两个表达式,分别枚举它们的项,乘起来枚举到的两个表达式的项,再把它们全部加起来。
和人类的思路不同的是:1、表达式的属性ops
有一个对应的term的正负号的属性,而term
里也有一个正负号的属性,term
的factor
里数字factor
也有一个正负号的属性(哎呀我的设计是这样的,为了适应定义,确实很烦),如何处理这些符号呢?2、项的乘法并不显然。
对第一个问题,解决办法是这样的:约定,对某一层次的运算,只考虑这个层次的符号的相互作用。例如,在表达式相乘时,除了得到枚举的两个项相乘的结果,还要运算一下两个项的 “表达式中存储的符号” 相互作用的结果(相同为正不同为负)
而对于项的乘法,同样要运算一下两个项的 “项中的正负号属性” 相互作用的结果。因子的乘法放到合并同类项,此处不讨论。
这样一来,运算前后符号的层级没有变,而且彼此层间的符号互不影响,极大避免了思考的心智负担,减少错误发生的概率(虽然我在这个上面栽了很多次)
对第二个问题,我的策略是,两个项相乘(此处是进入了项层面的运算),就把两个项拥有的因子全部合并起来,创造一个新的项(当然还得更新一下符号属性)。
为什么不在这一步合并同类项呢(指把幂函数和常数分别合到一起)?因为我们在后面,得到所有项之后,还要再做一次合并同类项,做两次合并同类项时间上不占优,代码量也大,还容易出错。
这样,两个问题就解决完了,我们实现了表达式乘法。
for (int i = 0; i < termsA.size(); i++) {
for (int j = 0; j < termsB.size(); j++) {
resultTerm.add(termMulti(termsA.get(i), termsB.get(j), opsA.get(i), opsB.get(j)));
resultOps.add(Token.getPlusToken());
//别学我,我这里自作聪明的把表达式存储的项符号传递进项的乘法里加入判断,导致代码非常臃肿
}
}
合并同类项
合并同类项的原理是,对于输入的表达式,
- 维护一个常数项,和一个记录不同幂次的系数的容器,枚举它的每一个项
- 再枚举项的每一个因子,对整数因子,用大整型来存储(乘起来),对于幂函数因子,记录下它的幂次(加起来)。遍历完毕后,若幂次等于0,则证明这个项是常数项,记录下来。否则根据幂次记录下对应的系数
感谢dhj巨佬的启发,我采用hashMap
来存储合并同类项时非常数项的信息。key
选择用VFactor
,Value
选择整型。这样做需要重载equals
和hashCode
两个方法。
用Map的好处很多,对于同一幂次的项,方便查询已经存储的系数大小,并直接把新计算出来的项的系数加/减上去。
需要注意,在取Map的结果的时候,若系数为0则直接跳过,减少答案长度。
递归下降魔改
学长提供的代码可读性很高,结构清晰,易于理解。我直接在他的代码的基础上,谈一谈我认为应该如何进行魔改。
首先,在parseExpr,parseTerm,parseFactor
这几个方法的开头,应当加上
Token character;
if (tokenNow().getType() == Token.Type.PLUS ||
tokenNow().getType() == Token.Type.MINUS) {
character = tokenNow();
move();
}
else {
character = new Token(Token.Type.PLUS, "+");
}
这是因为在本次作业中,Expr
、Term
和Facor
的开头,都有可能冒出来一个正负号,需要专门特判一下这个正负号是否存在。为了方便管理,对于没有正负号的,我们认为它前面有个正号。(这也是我存储了三层符号的原因,每一层都要做一次特判)
其次,是对parseExpr
相关的魔改。在得到一个表达式后,我们需要做一步合并同类项。这是因为,对于形如(a+b+c)^8
的式子,每做一次乘法,得到的项的总数都会有一次大飞跃,最终项的总数不是程序能承受的。因此每得到一个表达式就进行一次合并同类项,减少项的总数。
然后,是对parseTerm
相关的魔改。在代码中,Term被视作若干parseFactor
的连续乘积,考虑到展开表达式的需求,以及以下事实:
Expr
、Term
和Factor
间的层级关系决定了从下到上的转换是可能且比较容易的- 因此,单一的
Factor
,无论是常数、幂函数还是表达式,都可以向上转换为Expr。对于前两者,可以先转换成只有一个Factor
的Term
,然后再转换成只有一个Term
的Expr
,这可以通过重载构造函数实现。对于后者,它本身就是表达式 - 由此能得出,
Term
完全可以视作若干表达式的连续乘积。
这样,当我们按照顺序处理完Term时,得到的就会是一个没有括号的表达式。把表达式转化为因子,再转化为项,就能达到我们展开括号的目的:
Expr computeExpr = new Expr(parseFactor());
computeExpr.reverseOps(ops.get(0));//我犯蠢了,又一次用上层符号去参与下层符号的运算,不要学我
while (notArrayEnd() && tokenNow().getType() == Token.Type.MUL) {
move();
FactorInterface temp = parseFactor();
computeExpr = Tool.exprMulti(computeExpr, new Expr(temp));
}
对parseFactor
相关的魔改。因为在作业中,出现了新的factor幂函数,于是我们需要自己定义一个继承FactorInterface
接口的类VFactor
,然后在parseFactor
中,加上有关它的部分。这里还需要注意特别处理一下:
- 吸收掉
x^+(Number)
这种情况中的 + - 对于
x
,视作x^1
。 - 对于
x^0
等类似情况,当作CFactor
也就是数字处理
同时,因为表达式也可以有幂次,还需要对表达式因子部分做一下修改:
- 判断表达式后面是否有
^
,如果有,则继续吸收掉可能的 +,得到相应的幂次,然后做对应次表达式乘法。 - 同时,对于
(a+b)^0
这种情况,直接当作数字因子 1 处理。
这样,代码就魔改完了。当然,还剩下很多小细节没有提及,那就不是我所能穷举的了。
至此,程序基本上完成了,只剩下输出的一些优化。
作业1基于度量的结构分析
作业1的经典OO度量
针对方法的度量分析结果过于臃肿,且大部分方法的度量分析均符合标准,因此只列出类的度量分析结果。
以下对分析结果的解释引用自博客博客
-
ev(G)
基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。 -
iv(G)
模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。 -
v(G)
是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Main.main(String[]) | 0 | 1 | 1 | 1 |
analysis.Lexer.Lexer(String) | 24 | 11 | 15 | 17 |
analysis.Lexer.getTokens() | 0 | 1 | 1 | 1 |
analysis.Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
analysis.Parser.move() | 0 | 1 | 1 | 1 |
analysis.Parser.notArrayEnd() | 0 | 1 | 1 | 1 |
analysis.Parser.parseExpr() | 11 | 1 | 8 | 8 |
analysis.Parser.parseFactor() | 16 | 5 | 9 | 9 |
analysis.Parser.parseTerm() | 5 | 1 | 5 | 5 |
analysis.Parser.tackleExpr(Expr) | 14 | 4 | 6 | 6 |
analysis.Parser.tokenNow() | 0 | 1 | 1 | 1 |
analysis.Token.Token(Token) | 0 | 1 | 1 | 1 |
analysis.Token.Token(Type, String) | 0 | 1 | 1 | 1 |
analysis.Token.getMinusToken() | 0 | 1 | 1 | 1 |
analysis.Token.getMulToken() | 0 | 1 | 1 | 1 |
analysis.Token.getNumber() | 0 | 1 | 1 | 1 |
analysis.Token.getPlusToken() | 0 | 1 | 1 | 1 |
analysis.Token.getType() | 0 | 1 | 1 | 1 |
analysis.Token.reverseCharacter(Token) | 2 | 2 | 1 | 2 |
analysis.Token.toString() | 0 | 1 | 1 | 1 |
analysis.Tool.characterTackle(String) | 1 | 1 | 2 | 2 |
analysis.Tool.characterTackleTool(String) | 5 | 2 | 1 | 4 |
analysis.Tool.exprMergeOperate(ArrayList, ArrayList) | 17 | 1 | 7 | 8 |
analysis.Tool.exprMergeOperateTackle(Map<VFactor, BigInteger>, BigInteger) | 14 | 3 | 8 | 9 |
analysis.Tool.exprMulti(Expr, Expr) | 3 | 1 | 3 | 3 |
analysis.Tool.termMulti(Term, Term, Token, Token) | 6 | 1 | 4 | 4 |
composition.CFactor.CFactor(Token, Token) | 0 | 1 | 1 | 1 |
composition.CFactor.getNumber() | 2 | 2 | 2 | 2 |
composition.CFactor.getOp() | 0 | 1 | 1 | 1 |
composition.CFactor.toExpr() | 0 | 1 | 1 | 1 |
composition.CFactor.toString() | 2 | 2 | 2 | 2 |
composition.CFactor.toTerm() | 0 | 1 | 1 | 1 |
composition.Expr.Expr(ArrayList, ArrayList) | 0 | 1 | 1 | 1 |
composition.Expr.Expr(FactorInterface) | 3 | 1 | 4 | 4 |
composition.Expr.Expr(Term) | 0 | 1 | 1 | 1 |
composition.Expr.getOp() | 0 | 1 | 1 | 1 |
composition.Expr.getOps() | 0 | 1 | 1 | 1 |
composition.Expr.getTerms() | 0 | 1 | 1 | 1 |
composition.Expr.reverseOps(Token) | 0 | 1 | 1 | 1 |
composition.Expr.toExpr() | 0 | 1 | 1 | 1 |
composition.Expr.toString() | 7 | 5 | 4 | 6 |
composition.Expr.toTerm() | 0 | 1 | 1 | 1 |
composition.FactorInterface.characterOperate(Token, Token) | 2 | 2 | 2 | 2 |
composition.Term.Term(ArrayList, Token) | 0 | 1 | 1 | 1 |
composition.Term.Term(Expr) | 0 | 1 | 1 | 1 |
composition.Term.Term(FactorInterface) | 0 | 1 | 1 | 1 |
composition.Term.Term(FactorInterface, Token) | 0 | 1 | 1 | 1 |
composition.Term.getExprOps(Token) | 9 | 3 | 4 | 5 |
composition.Term.getExprTerms() | 3 | 1 | 3 | 3 |
composition.Term.getFactors() | 0 | 1 | 1 | 1 |
composition.Term.getOp() | 0 | 1 | 1 | 1 |
composition.Term.gettotalityToken() | 4 | 3 | 3 | 4 |
composition.Term.isExprOnly() | 1 | 1 | 2 | 2 |
composition.Term.size() | 0 | 1 | 1 | 1 |
composition.Term.toString() | 4 | 1 | 4 | 4 |
composition.VFactor.VFactor(Token) | 0 | 1 | 1 | 1 |
composition.VFactor.VFactor(Token, Token) | 0 | 1 | 1 | 1 |
composition.VFactor.VFactor(Token, Token, Token) | 0 | 1 | 1 | 1 |
composition.VFactor.equals(Object) | 3 | 3 | 2 | 4 |
composition.VFactor.getOp() | 0 | 1 | 1 | 1 |
composition.VFactor.getPower() | 0 | 1 | 1 | 1 |
composition.VFactor.hashCode() | 0 | 1 | 1 | 1 |
composition.VFactor.toExpr() | 0 | 1 | 1 | 1 |
composition.VFactor.toString() | 2 | 2 | 2 | 2 |
composition.VFactor.toTerm() | 0 | 1 | 1 | 1 |
分析方法的复杂度结果,不符合高内聚低耦合的方法有:
- Lexer的构造方法。该方法考虑了输入字符串的多种情况,因此是可以理解的。
- parseFctor方法,该方法分析了多种因子,可能创建多种因子对象,被多个因子类所束缚
分析结果可知,Tool
类、Lexer
类和Parser
类为低内聚类。这三个类分别为分析工具类、词法分析类和递归下降执行类,均为程序运行流程的直接体现,因此它们作为低内聚类是可以理解的。同时,其它类耦合度低,可以认为整份代码的抽象化程度比较合适。
作业1的类图与类设计考虑
- Main类:该类为主类,执行所有流程,但是本身代码量很少,不承担实际的实现。
- Lexer类:词法分析类
- Parser类:递归下降类,执行递归下降的具体流程
- Tool类:将所有需要重复用到的工具方法存放在此,包括对字符串的预处理、表达式乘法、合并同类项的方法等等
- FactorInterface类:接口
- VFactor、CFactor类:因子类,负责具体实现接口,本身只描述对应的因子信息以及提供对几个属性的访问和处理方法
- Expr:表达式类,本身描述了表达式所包含的信息和访问方法
- Term:项类,描述了项所包含的信息和访问方法
这么设计的优点和别的采用类似结构的同学相比,基本一致,但存在一些独有的缺点。
- 因为没有专门的化简类,所有的化简过程中因子、项、表达式依旧维持原来的类型,不会发生真正的合并,要考虑的情况很多,实现起来比较复杂
- 在第一次作业中,我采取边分析边合并的方式,但该方式很容易运行超时。
作业2
作业2的架构扩展
作业2在作业1的基础上新加了自定义函数需求和指数函数需求。对于我的作业一架构而言这两个的添加都不算难事。
自定义函数
我实现自定义函数的思路是这样的:首先读入n,根据n确定读入的自定义函数个数。对于每个自定义函数,用一个自定义类来储存它们的信息,并用一个hashMap来对应函数名和函数信息(比如形参个数、形参名、表达式等)。先读入它们的名字和形参,然后读入对应的表达式,将这些分门别类地存储好。
在读入总表达式时,每遇到一个自定义函数,都执行以下操作:先根据读到的函数名取出对应的自定义函数信息,然后根据函数信息进行函数表达式的字符串级别的替换(先把exp等做保护性替换,然后替换掉形参为实参,最后再还原exp等),将处理完后的字符串用新建的parser类进行分析,返回得到的表达式
指数函数
我的实现,是将指数函数、幂函数从VFactor里区分出来,将VFactor转变为一个接口。指数函数类本身存储了指数函数的内部因子和指数函数的幂次信息,而从外部来看,指数函数和幂函数并没有什么本质区别。因此,很好扩展。
作业2基于度量的结构分析
作业2的经典OO度量结果
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Main.main(String[]) | 0 | 1 | 1 | 1 |
analysis.FunctionMapValue.FunctionMapValue() | 0 | 1 | 1 | 1 |
analysis.FunctionMapValue.addArgument(Character) | 0 | 1 | 1 | 1 |
analysis.FunctionMapValue.getExpendExpr(ArrayList) | 2 | 1 | 3 | 3 |
analysis.FunctionMapValue.setExpr(String) | 0 | 1 | 1 | 1 |
analysis.Init.Init() | 0 | 1 | 1 | 1 |
analysis.Init.getFunctionMap() | 0 | 1 | 1 | 1 |
analysis.Init.getInput() | 0 | 1 | 1 | 1 |
analysis.Init.tackleInput() | 6 | 1 | 4 | 4 |
analysis.Lexer.Lexer(String) | 25 | 1 | 16 | 18 |
analysis.Lexer.getTokens() | 0 | 1 | 1 | 1 |
analysis.Lexer.tackleCharacter() | 7 | 5 | 7 | 7 |
analysis.Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
analysis.Parser.getFunctionMap() | 0 | 1 | 1 | 1 |
analysis.Parser.getPFactorPower() | 8 | 4 | 6 | 6 |
analysis.Parser.move() | 0 | 1 | 1 | 1 |
analysis.Parser.notArrayEnd() | 0 | 1 | 1 | 1 |
analysis.Parser.parseExpr() | 6 | 1 | 6 | 6 |
analysis.Parser.parseFactor() | 8 | 4 | 5 | 6 |
analysis.Parser.parseFactorTool() | 22 | 9 | 12 | 15 |
analysis.Parser.parseTerm() | 5 | 1 | 5 | 5 |
analysis.Parser.setFunctionMap(HashMap<String, FunctionMapValue>) | 0 | 1 | 1 | 1 |
analysis.Parser.tokenNow() | 0 | 1 | 1 | 1 |
analysis.Token.Token(Token) | 0 | 1 | 1 | 1 |
analysis.Token.Token(Type, String) | 0 | 1 | 1 | 1 |
analysis.Token.getMinusToken() | 0 | 1 | 1 | 1 |
analysis.Token.getMulToken() | 0 | 1 | 1 | 1 |
analysis.Token.getNumber() | 0 | 1 | 1 | 1 |
analysis.Token.getPlusToken() | 0 | 1 | 1 | 1 |
analysis.Token.getType() | 0 | 1 | 1 | 1 |
analysis.Token.hashCode() | 0 | 1 | 1 | 1 |
analysis.Token.reverseCharacter(Token) | 2 | 2 | 1 | 2 |
analysis.Token.toString() | 0 | 1 | 1 | 1 |
analysis.Tool.cfactorDiv(CFactor, BigInteger) | 2 | 2 | 2 | 2 |
analysis.Tool.cfactorMult(CFactor, CFactor) | 2 | 1 | 2 | 2 |
analysis.Tool.characterOperate(Token, Token) | 1 | 2 | 2 | 2 |
analysis.Tool.characterTackle(String) | 1 | 1 | 2 | 2 |
analysis.Tool.characterTackleTool(String) | 5 | 2 | 1 | 4 |
analysis.Tool.exprMergeBigintegerOperate(Token, Token) | 2 | 1 | 1 | 2 |
analysis.Tool.exprMergeExpOperate(FactorInterface) | 1 | 2 | 2 | 2 |
analysis.Tool.exprMergeOperate(ArrayList, ArrayList) | 34 | 7 | 12 | 13 |
analysis.Tool.exprMergeOperateTackle(Map<Term, BigInteger>, BigInteger) | 14 | 3 | 8 | 9 |
analysis.Tool.exprMulti(Expr, Expr) | 3 | 1 | 3 | 3 |
analysis.Tool.factorAdd(FactorInterface, FactorInterface) | 3 | 3 | 2 | 3 |
analysis.Tool.termMulti(Term, Term, Token, Token) | 6 | 1 | 4 | 4 |
analysis.Tool.zipExpr(Expr) | 3 | 2 | 4 | 4 |
composition.CFactor.CFactor(BigInteger) | 2 | 1 | 2 | 2 |
composition.CFactor.CFactor(Token, Token) | 2 | 1 | 2 | 2 |
composition.CFactor.equalZero() | 0 | 1 | 1 | 1 |
composition.CFactor.getNumber() | 0 | 1 | 1 | 1 |
composition.CFactor.getOp() | 0 | 1 | 1 | 1 |
composition.CFactor.getOps() | 0 | 1 | 1 | 1 |
composition.CFactor.hashCode() | 0 | 1 | 1 | 1 |
composition.CFactor.pow() | 2 | 1 | 2 | 2 |
composition.CFactor.setPower(Token) | 0 | 1 | 1 | 1 |
composition.CFactor.simplify() | 0 | 1 | 1 | 1 |
composition.CFactor.toExpr() | 0 | 1 | 1 | 1 |
composition.CFactor.toString() | 2 | 2 | 2 | 2 |
composition.CFactor.toTerm() | 0 | 1 | 1 | 1 |
composition.CFactor.toTerms() | 0 | 1 | 1 | 1 |
composition.CFunction.CFunction(ArrayList, Token) | 0 | 1 | 1 | 1 |
composition.CFunction.getName() | 0 | 1 | 1 | 1 |
composition.CFunction.getOp() | 0 | 1 | 1 | 1 |
composition.CFunction.getOps() | 0 | 1 | 1 | 1 |
composition.CFunction.getPower() | 0 | 1 | 1 | 1 |
composition.CFunction.pow() | 0 | 1 | 1 | 1 |
composition.CFunction.setPower(Token) | 0 | 1 | 1 | 1 |
composition.CFunction.simplify() | 0 | 1 | 1 | 1 |
composition.CFunction.toExpr() | 0 | 1 | 1 | 1 |
composition.CFunction.toTerm() | 0 | 1 | 1 | 1 |
composition.CFunction.toTerms() | 0 | 1 | 1 | 1 |
composition.EFunction.EFunction(Token, FactorInterface, BigInteger) | 0 | 1 | 1 | 1 |
composition.EFunction.equals(Object) | 1 | 2 | 2 | 2 |
composition.EFunction.gcdTackleExpr(Expr) | 18 | 5 | 6 | 8 |
composition.EFunction.getFactor() | 0 | 1 | 1 | 1 |
composition.EFunction.getGcd(Expr) | 13 | 4 | 5 | 7 |
composition.EFunction.getName() | 0 | 1 | 1 | 1 |
composition.EFunction.getOp() | 0 | 1 | 1 | 1 |
composition.EFunction.getOps() | 0 | 1 | 1 | 1 |
composition.EFunction.getPower() | 0 | 1 | 1 | 1 |
composition.EFunction.getString(FactorInterface, int) | 13 | 1 | 8 | 8 |
composition.EFunction.hashCode() | 1 | 1 | 2 | 2 |
composition.EFunction.pow() | 0 | 1 | 1 | 1 |
composition.EFunction.setPower(Token) | 0 | 1 | 1 | 1 |
composition.EFunction.simplify() | 1 | 2 | 1 | 2 |
composition.EFunction.toExpr() | 0 | 1 | 1 | 1 |
composition.EFunction.toString() | 1 | 1 | 2 | 2 |
composition.EFunction.toStringMethod1() | 0 | 1 | 1 | 1 |
composition.EFunction.toStringMethod2() | 1 | 1 | 2 | 2 |
composition.EFunction.toTerm() | 0 | 1 | 1 | 1 |
composition.EFunction.toTerms() | 0 | 1 | 1 | 1 |
composition.Expr.Expr(ArrayList, ArrayList) | 0 | 1 | 1 | 1 |
composition.Expr.Expr(FactorInterface) | 3 | 1 | 4 | 4 |
composition.Expr.Expr(Term) | 0 | 1 | 1 | 1 |
composition.Expr.getOp() | 0 | 1 | 1 | 1 |
composition.Expr.getOps() | 0 | 1 | 1 | 1 |
composition.Expr.getPower() | 0 | 1 | 1 | 1 |
composition.Expr.getTerms() | 0 | 1 | 1 | 1 |
composition.Expr.hashCode() | 1 | 1 | 2 | 2 |
composition.Expr.pow() | 1 | 1 | 2 | 2 |
composition.Expr.setPower(BigInteger) | 0 | 1 | 1 | 1 |
composition.Expr.setPower(Token) | 0 | 1 | 1 | 1 |
composition.Expr.simplify() | 6 | 2 | 4 | 5 |
composition.Expr.toExpr() | 0 | 1 | 1 | 1 |
composition.Expr.toString() | 8 | 5 | 5 | 7 |
composition.Expr.toTerm() | 0 | 1 | 1 | 1 |
composition.Expr.toTerms() | 0 | 1 | 1 | 1 |
composition.FactorInterface.characterOperate(Token, Token) | 2 | 2 | 2 | 2 |
composition.PFunction.PFunction(Token, Token) | 0 | 1 | 1 | 1 |
composition.PFunction.equals(Object) | 3 | 3 | 2 | 4 |
composition.PFunction.getName() | 0 | 1 | 1 | 1 |
composition.PFunction.getOp() | 0 | 1 | 1 | 1 |
composition.PFunction.getOps() | 0 | 1 | 1 | 1 |
composition.PFunction.getPower() | 0 | 1 | 1 | 1 |
composition.PFunction.hashCode() | 0 | 1 | 1 | 1 |
composition.PFunction.pow() | 0 | 1 | 1 | 1 |
composition.PFunction.setPower(Token) | 0 | 1 | 1 | 1 |
composition.PFunction.simplify() | 0 | 1 | 1 | 1 |
composition.PFunction.toExpr() | 0 | 1 | 1 | 1 |
composition.PFunction.toString() | 2 | 2 | 2 | 2 |
composition.PFunction.toTerm() | 0 | 1 | 1 | 1 |
composition.PFunction.toTerms() | 0 | 1 | 1 | 1 |
composition.Term.Term(ArrayList, Token) | 0 | 1 | 1 | 1 |
composition.Term.Term(Expr) | 0 | 1 | 1 | 1 |
composition.Term.Term(FactorInterface) | 0 | 1 | 1 | 1 |
composition.Term.Term(FactorInterface, Token) | 0 | 1 | 1 | 1 |
composition.Term.equals(Object) | 1 | 2 | 2 | 2 |
composition.Term.getExprOps(Token) | 9 | 3 | 4 | 5 |
composition.Term.getExprTerms() | 3 | 1 | 3 | 3 |
composition.Term.getFactors() | 0 | 1 | 1 | 1 |
composition.Term.getOp() | 0 | 1 | 1 | 1 |
composition.Term.gettotalityToken() | 3 | 3 | 3 | 3 |
composition.Term.hashCode() | 1 | 1 | 2 | 2 |
composition.Term.isExprOnly() | 1 | 1 | 2 | 2 |
composition.Term.simplify() | 1 | 1 | 2 | 2 |
composition.Term.size() | 0 | 1 | 1 | 1 |
composition.Term.sortFactors() | 0 | 1 | 1 | 1 |
composition.Term.toString() | 4 | 1 | 4 | 4 |
分析结果可知,有以下方法结果比较差:
- Lexer的构造方法,该方法直接进行词法分析,因需考虑的情况较多,所以低内聚是可以理解的。
- parseFactorTool,该方法用于处理分析因子时碰到的幂函数、自定义函数和指数函数。因考虑的情况多,且三个函数的处理方法有接近的地方但没有提取出新的方法,因此结果比较差。
- exprMergeOperate,该方法进行了合并同类项。因为我的设计中,各个因子在合并同类项这一步仍然没有转化为统一的一个类型,因此需要考虑多种情况,因此结果比较差。
这些方法的高内聚高耦合有部分是无法避免的,有部分是可以通过合理设计规避的,我会在接下来的单元中注意规范类似的设计。
作业2的类图分析与新增类的设计考虑
- Init:该类负责处理输入逻辑,从Main里面拆分出来,保证了Main不至于过于臃肿
- CFuntion:该类负责处理自定义函数,完成函数的实参替换并返回解析出的表达式
- PFunction:被拆分出来的幂函数类
- EFunction:指数函数类
- FunctionMapValue:存储自定义函数信息的类,该类存储自定义函数的全部信息,并在分析表达式遇到自定义函数因子时根据输入的实参因子进行字符串操作得到真正的自定义函数表达式,进行解析后返回解析完的Expr类。
相比于第一次作业,我在本次作业中对架构做了优化,将化简从分析过程中抽离出来,作为FactorInterface的一个方法而要求各个因子实现,这么做能比较显著的提升运行效率。
作业3
作业3架构改进
作业3的架构改进颇为简单,只需要模仿链式求导法则等实现一遍即可,不具备详细讨论的价值
作业3基于度量的结构分析
作业3经典OO度量结果
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Main.main(String[]) | 0 | 1 | 1 | 1 |
analysis.FunctionMapValue.FunctionMapValue() | 0 | 1 | 1 | 1 |
analysis.FunctionMapValue.addArgument(Character) | 0 | 1 | 1 | 1 |
analysis.FunctionMapValue.getExpendExpr(ArrayList) | 2 | 1 | 3 | 3 |
analysis.FunctionMapValue.setExpr(String) | 0 | 1 | 1 | 1 |
analysis.Init.Init() | 0 | 1 | 1 | 1 |
analysis.Init.getFunctionMap() | 0 | 1 | 1 | 1 |
analysis.Init.getInput() | 0 | 1 | 1 | 1 |
analysis.Init.tackleInput() | 6 | 1 | 4 | 4 |
analysis.Lexer.Lexer(String) | 25 | 1 | 16 | 18 |
analysis.Lexer.getTokens() | 0 | 1 | 1 | 1 |
analysis.Lexer.tackleCharacter() | 10 | 6 | 9 | 9 |
analysis.Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
analysis.Parser.getFunctionMap() | 0 | 1 | 1 | 1 |
analysis.Parser.getPFactorPower() | 8 | 4 | 6 | 6 |
analysis.Parser.move() | 0 | 1 | 1 | 1 |
analysis.Parser.notArrayEnd() | 0 | 1 | 1 | 1 |
analysis.Parser.parseExpr() | 6 | 1 | 6 | 6 |
analysis.Parser.parseFactor() | 8 | 4 | 5 | 6 |
analysis.Parser.parseFactorTool() | 23 | 10 | 13 | 16 |
analysis.Parser.parseTerm() | 5 | 1 | 5 | 5 |
analysis.Parser.setFunctionMap(HashMap<String, FunctionMapValue>) | 0 | 1 | 1 | 1 |
analysis.Parser.tokenNow() | 0 | 1 | 1 | 1 |
analysis.Token.Token(Token) | 0 | 1 | 1 | 1 |
analysis.Token.Token(Type, String) | 0 | 1 | 1 | 1 |
analysis.Token.getMinusToken() | 0 | 1 | 1 | 1 |
analysis.Token.getMulToken() | 0 | 1 | 1 | 1 |
analysis.Token.getNumber() | 0 | 1 | 1 | 1 |
analysis.Token.getPlusToken() | 0 | 1 | 1 | 1 |
analysis.Token.getType() | 0 | 1 | 1 | 1 |
analysis.Token.hashCode() | 0 | 1 | 1 | 1 |
analysis.Token.reverseCharacter(Token) | 2 | 2 | 1 | 2 |
analysis.Token.toString() | 0 | 1 | 1 | 1 |
analysis.Tool.cfactorDiv(CFactor, BigInteger) | 2 | 2 | 2 | 2 |
analysis.Tool.cfactorMult(CFactor, CFactor) | 2 | 1 | 2 | 2 |
analysis.Tool.characterOperate(Token, Token) | 1 | 2 | 2 | 2 |
analysis.Tool.characterTackle(String) | 1 | 1 | 2 | 2 |
analysis.Tool.characterTackleTool(String) | 5 | 2 | 1 | 4 |
analysis.Tool.exprMergeBigintegerOperate(Token, Token) | 2 | 1 | 1 | 2 |
analysis.Tool.exprMergeExpOperate(FactorInterface) | 1 | 2 | 2 | 2 |
analysis.Tool.exprMergeOperate(ArrayList, ArrayList) | 29 | 7 | 11 | 12 |
analysis.Tool.exprMergeOperateTackle(Map<Term, BigInteger>, BigInteger) | 14 | 3 | 8 | 9 |
analysis.Tool.exprMulti(Expr, Expr) | 3 | 1 | 3 | 3 |
analysis.Tool.factorAdd(FactorInterface, FactorInterface) | 3 | 3 | 2 | 3 |
analysis.Tool.termMulti(Term, Term, Token, Token) | 6 | 1 | 4 | 4 |
analysis.Tool.zipExpr(Expr) | 3 | 2 | 4 | 4 |
composition.CFactor.CFactor(BigInteger) | 2 | 1 | 2 | 2 |
composition.CFactor.CFactor(Token, Token) | 2 | 1 | 2 | 2 |
composition.CFactor.derivative() | 0 | 1 | 1 | 1 |
composition.CFactor.equalZero() | 0 | 1 | 1 | 1 |
composition.CFactor.getNumber() | 0 | 1 | 1 | 1 |
composition.CFactor.getOp() | 0 | 1 | 1 | 1 |
composition.CFactor.getOps() | 0 | 1 | 1 | 1 |
composition.CFactor.hashCode() | 0 | 1 | 1 | 1 |
composition.CFactor.pow() | 2 | 1 | 2 | 2 |
composition.CFactor.setPower(Token) | 0 | 1 | 1 | 1 |
composition.CFactor.simplify() | 0 | 1 | 1 | 1 |
composition.CFactor.toExpr() | 0 | 1 | 1 | 1 |
composition.CFactor.toString() | 2 | 2 | 2 | 2 |
composition.CFactor.toTerm() | 0 | 1 | 1 | 1 |
composition.CFactor.toTerms() | 0 | 1 | 1 | 1 |
composition.CFunction.CFunction(ArrayList, Token) | 0 | 1 | 1 | 1 |
composition.CFunction.derivative() | 0 | 1 | 1 | 1 |
composition.CFunction.getName() | 0 | 1 | 1 | 1 |
composition.CFunction.getOp() | 0 | 1 | 1 | 1 |
composition.CFunction.getOps() | 0 | 1 | 1 | 1 |
composition.CFunction.getPower() | 0 | 1 | 1 | 1 |
composition.CFunction.pow() | 0 | 1 | 1 | 1 |
composition.CFunction.setPower(Token) | 0 | 1 | 1 | 1 |
composition.CFunction.simplify() | 0 | 1 | 1 | 1 |
composition.CFunction.toExpr() | 0 | 1 | 1 | 1 |
composition.CFunction.toTerm() | 0 | 1 | 1 | 1 |
composition.CFunction.toTerms() | 0 | 1 | 1 | 1 |
composition.DFactor.DFactor(Expr) | 0 | 1 | 1 | 1 |
composition.DFactor.derivative() | 0 | 1 | 1 | 1 |
composition.DFactor.getExpr() | 0 | 1 | 1 | 1 |
composition.DFactor.getOp() | 0 | 1 | 1 | 1 |
composition.DFactor.getOps() | 0 | 1 | 1 | 1 |
composition.DFactor.pow() | 0 | 1 | 1 | 1 |
composition.DFactor.setPower(Token) | 0 | 1 | 1 | 1 |
composition.DFactor.simplify() | 0 | 1 | 1 | 1 |
composition.DFactor.toExpr() | 0 | 1 | 1 | 1 |
composition.DFactor.toString() | 0 | 1 | 1 | 1 |
composition.DFactor.toTerm() | 0 | 1 | 1 | 1 |
composition.DFactor.toTerms() | 0 | 1 | 1 | 1 |
composition.EFunction.EFunction(Token, FactorInterface, BigInteger) | 0 | 1 | 1 | 1 |
composition.EFunction.derivative() | 2 | 1 | 3 | 3 |
composition.EFunction.equals(Object) | 1 | 2 | 2 | 2 |
composition.EFunction.gcdTackleExpr(Expr) | 18 | 5 | 6 | 8 |
composition.EFunction.getFactor() | 0 | 1 | 1 | 1 |
composition.EFunction.getGcd(Expr) | 13 | 4 | 5 | 7 |
composition.EFunction.getName() | 0 | 1 | 1 | 1 |
composition.EFunction.getOp() | 0 | 1 | 1 | 1 |
composition.EFunction.getOps() | 0 | 1 | 1 | 1 |
composition.EFunction.getPower() | 0 | 1 | 1 | 1 |
composition.EFunction.getString(FactorInterface, int) | 13 | 1 | 8 | 8 |
composition.EFunction.hashCode() | 1 | 1 | 2 | 2 |
composition.EFunction.pow() | 1 | 2 | 1 | 2 |
composition.EFunction.setPower(Token) | 0 | 1 | 1 | 1 |
composition.EFunction.simplify() | 2 | 2 | 2 | 3 |
composition.EFunction.toExpr() | 0 | 1 | 1 | 1 |
composition.EFunction.toString() | 3 | 2 | 3 | 4 |
composition.EFunction.toStringMethod1() | 0 | 1 | 1 | 1 |
composition.EFunction.toTerm() | 0 | 1 | 1 | 1 |
composition.EFunction.toTerms() | 0 | 1 | 1 | 1 |
composition.Expr.Expr(ArrayList, ArrayList) | 0 | 1 | 1 | 1 |
composition.Expr.Expr(FactorInterface) | 4 | 1 | 4 | 5 |
composition.Expr.Expr(Term) | 0 | 1 | 1 | 1 |
composition.Expr.derivative() | 5 | 1 | 4 | 4 |
composition.Expr.getHashCode() | 1 | 1 | 2 | 2 |
composition.Expr.getOp() | 0 | 1 | 1 | 1 |
composition.Expr.getOps() | 0 | 1 | 1 | 1 |
composition.Expr.getPower() | 0 | 1 | 1 | 1 |
composition.Expr.getTerms() | 0 | 1 | 1 | 1 |
composition.Expr.hashCode() | 0 | 1 | 1 | 1 |
composition.Expr.pow() | 1 | 1 | 2 | 2 |
composition.Expr.setPower(BigInteger) | 0 | 1 | 1 | 1 |
composition.Expr.setPower(Token) | 0 | 1 | 1 | 1 |
composition.Expr.simplify() | 6 | 2 | 4 | 5 |
composition.Expr.toExpr() | 0 | 1 | 1 | 1 |
composition.Expr.toString() | 8 | 5 | 5 | 7 |
composition.Expr.toTerm() | 0 | 1 | 1 | 1 |
composition.Expr.toTerms() | 0 | 1 | 1 | 1 |
composition.FactorInterface.characterOperate(Token, Token) | 2 | 2 | 2 | 2 |
composition.PFunction.PFunction(Token, Token) | 0 | 1 | 1 | 1 |
composition.PFunction.derivative() | 0 | 1 | 1 | 1 |
composition.PFunction.equals(Object) | 3 | 3 | 2 | 4 |
composition.PFunction.getName() | 0 | 1 | 1 | 1 |
composition.PFunction.getOp() | 0 | 1 | 1 | 1 |
composition.PFunction.getOps() | 0 | 1 | 1 | 1 |
composition.PFunction.getPower() | 0 | 1 | 1 | 1 |
composition.PFunction.hashCode() | 0 | 1 | 1 | 1 |
composition.PFunction.pow() | 0 | 1 | 1 | 1 |
composition.PFunction.setPower(Token) | 0 | 1 | 1 | 1 |
composition.PFunction.simplify() | 0 | 1 | 1 | 1 |
composition.PFunction.toExpr() | 0 | 1 | 1 | 1 |
composition.PFunction.toString() | 2 | 2 | 2 | 2 |
composition.PFunction.toTerm() | 0 | 1 | 1 | 1 |
composition.PFunction.toTerms() | 0 | 1 | 1 | 1 |
composition.Term.Term(ArrayList, Token) | 0 | 1 | 1 | 1 |
composition.Term.Term(Expr) | 0 | 1 | 1 | 1 |
composition.Term.Term(FactorInterface) | 0 | 1 | 1 | 1 |
composition.Term.Term(FactorInterface, Token) | 0 | 1 | 1 | 1 |
composition.Term.derivative() | 7 | 1 | 5 | 5 |
composition.Term.equals(Object) | 1 | 2 | 2 | 2 |
composition.Term.getExprOps(Token) | 9 | 3 | 4 | 5 |
composition.Term.getExprTerms() | 3 | 1 | 3 | 3 |
composition.Term.getFactors() | 0 | 1 | 1 | 1 |
composition.Term.getOp() | 0 | 1 | 1 | 1 |
composition.Term.gettotalityToken() | 3 | 3 | 3 | 3 |
composition.Term.hashCode() | 1 | 1 | 2 | 2 |
composition.Term.isExprOnly() | 1 | 1 | 2 | 2 |
composition.Term.simplify() | 6 | 1 | 4 | 4 |
composition.Term.size() | 0 | 1 | 1 | 1 |
composition.Term.sortFactors() | 0 | 1 | 1 | 1 |
composition.Term.toString() | 4 | 1 | 4 | 4 |
观察结果可知,相比于作业1和作业2,并没有新增的不符合低耦合高内聚的方法,可以说明作业3的框架改造是合格的
作业3类图与新增类考虑
只有一个新增类:DFactor,该类用于表示、存储求导因子的信息,并在执行化简时,调用因子的求导方法。
自定义迭代场景的畅想
根据我的实现方式,天然支持对主表达式有多变量的情况。同时,对于其他函数,例如三角函数、求和函数等都比较容易扩展。以三角函数为例(不考虑输出优化):
- 对于输入而言,需要对Token做修改,新支持三角函数Token,并修改对应的词法分析器
- 在解析时,可以完全模仿exp函数的过程,对于三角函数,先读入内部的因子,再识别外部的幂次,然后组合成一个三角函数因子
- 化简时,因为三角函数多变的形式,得到最简的表达式会存在一些困难。不过,如果只是想保证正确性的话扩展起来特别简单,只需要完全模仿exp的思路即可——化简内部因子
- 而对于输出优化,我也有一些想法——可以用启发式搜索,或者根据经验公式进行化简。
bug分析
本单元作业处因效率问题被hack成功外,无其他bug。而我采用多次重构、吸取别人经验的方式,重构出了效率不错的代码。
第一单元额外技巧总结
测评机构建经验
评测机基本要求与一般性流程
评测机,机如其名,应该有以下功能:
- 准确无误地评判输入正确与否
- 根据第一条,它应该能够对不同的输入形式做出相同且正确的判断
- 同时,还应该能返回一定的错误信息,保存错误现场(保存能引起错误的数据、保存错误的输出)
而在本题中,因为输出自由而复杂的形式,对评测机的第二点功能提出了 严峻挑战 。本文介绍两种办法,来实现评测机的以上功能。
但是,无论采用什么方法搭建评测机,评测机的大体运行流程是不会变的。
第一步 运行数据生成器,并储存数据生成器得到的结果,或者令数据生成器的输出重定向到某个文本文件。
第二步 运行待评测的程序,将得到的结果也储存起来。
第三步 得到正确答案,并将其和待评测的程序的结果进行比对。
对于Java
而言,我们可以把待评测的程序打包成jar
文件,然后调用打包好的.jar
(不同语言调用方法不一样),用重定向得到储存在文本里的结果。
打包方法
以下是Java
中调用.jar
的一个实例:
public static void jarProcess(String jarPath,String outPath,String inPath) {
try {
ProcessBuilder pb = new ProcessBuilder("java", "-jar", jarPath);
pb.directory(new File(System.getProperty("user.dir")));
// 输入和输出文件的路径
File input = new File(inPath);
File output = new File(outPath);
// 重定向输入和输出
pb.redirectInput(input);
pb.redirectOutput(output);
// 启动进程
Process p = pb.start();
// 等待进程结束
int exitCode = p.waitFor();
pb.command().clear();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
python
中调用.jar
是可以用命令行做到,比较简单,这里不再赘述。
而对于数据生成,可以把它和比对程序合并到一个项目中,根据选择搭建评测机的环境(python
、Java
等),决定具体流程流程。
评测机的其他问题基本解决了,但还有个最重要的问题:如何评定某个输出是否正确?这里我想介绍两种方法,它们各有优劣。
第一种方法:sympy
使用方法简介
我们大可不必把目光局限在Java
中,在遥远的python
国度,强大的生态和多种多样的轮子让很多事情变得简单起来。我要介绍的sympy
就能让我们的评测机只需要几十行代码就能完成。
sympy
是python
中的一个库,能够提供丰富的数学计算功能。而本次作业中我们并不需要sympy
的那些高级功能,只需要最简单的一个:评判两个符合sympy
格式要求的多项式是否等价。sympy
的格式非常灵活,但也需要针对我们作业的形式做一定的调整:
sympy支持连续的多个正号或者负号所以这方面无需做处理。同时,sympy还会自动忽略各种空白字符,这方面也不用担心。但是,sympy不支持有前导零的整数,所以需要进行处理。无论是用正则表达式,还是用传统的枚举方法,这种程度的字符串处理都不算大问题。唯一需要注意的是去除前导零的时候不能把[0]+[!0-9]
这种纯粹由0构成的数给全部去掉,需要把它保留为单一数字0
在做完预处理后,就可以进行结果比对了。这一部分只用了sympy的非常浅显易懂的功能。话不多说,示例代码如下:
from sympy import symbols, simplify, sympify
# 声明变量
x = symbols('x')
# 多项式字符串
poly_str1 = 'x**2 + 2*x + 1'
poly_str2 = '(x + 1)**2'
# 将字符串形式的多项式转换为SymPy表达式
poly_expr1 = sympify(poly_str1)
poly_expr2 = sympify(poly_str2)
# 简化两个表达式的差,简化是必要的,如不简化,sympy无法正常判断
diff = simplify(poly_expr1 - poly_expr2)
# 判断是否等价
if diff == 0:
print("两个多项式等价")
else:
print("两个多项式不等价")
简单,明了,省时省力。那既然这样,就免不了有小伙伴问:sympy的效果已经这么好了,为什么还要提出来第二种办法呢?更进一步,(假设课程组没有相关禁令)我为什么不能把调用sympy进行简化的代码导出,用Java调用,当作我的作业呢?
这就要指出sympy的两个问题了(同时也是本贴标题部分来源):
慢 以及不如自己写的灵活以及我搓完轮子才发现还有sympy这种现成的轮子
其一
它的速度是非常致命的。在作业1中,因为一层括号的限制,我们并不容易构造出特别大的数据。但是哪怕只有四层括号,一个非常简单的多项式也会在指数的催化下变成一个恐怖的庞然大物。尝试以下数据:
((((x+x)^8)^8)^8)^8
它的最简输出有 1242 个字符!想象如果括号里的多项式不是简单无害的(x+x)
,而是(x^8 + sum(1,10,x^2 +x)+1+sin(x^2))
,那会有多么恐怖!事实上,仅仅是刚才给出的数据(x+x)那个
,就已经几乎超出了sympy的能力范围。你可以跑一跑试一试,看看sympy
需要多久才能给出判断是否相等的结果。
其二
在面对初等函数时,它表现出来了极强的兼容性。但是在面对更复杂的函数,比如求和函数时,它和课程组间的定义冲突就会导致预处理字符串变成一项艰难而容易出错的任务。
综上所述,使用sympy固然简单,但是具有非常差的可扩展性。这和我们OO课程的教学理念可以说是背道而驰。因此,sympy可以用作作业1的评测机核心,但绝对不是最优解。
第二种方法:项链算法
别查了,查不到的,这是我刚编的名字
前提
既然sympy靠不住,那我们就得找另一种方式来满足我们的需求。注意到以下事实:
- 评测机现在缺的就是评价输出正确与否的能力
- 评价输出是否正确,就是看它和正确答案是否等价
- OO作为迭代式开发的课程,三个人及以上所有bug全部相同的可能性可以忽略不记
- 由于题目的要求,待测输出具有一些良好的性质(例如没有括号)
因此,可以这么认为:
- 评测机可以不关心待评测输出和标准答案是否都正确,它只需要评价两者是否等价
- 标准答案是否正确不重要,两个人的输出在同一地方犯同样错误且别的地方不会犯不同错误的可能微乎其微
- 对拍能够起到评测机的作用
- 因为待测输出的优秀性质,手搓等价评判的难度并不算很大——不会成为第二个作业
是的,只需要按照对拍的思路,评价两个人的输出是否等价,就能够起到评测机的作用——高效地找出bug。
原理
项链算法,是基于输出具有的良好性质而进行表达式化简的一种算法。根据题目要求,输出是不带有括号的,因此可以把输出的表达式 (以下称作待测表达式) 看作[+|-term]+
的形式,即若干个项的累加/减。这像不像是在一条项链上线性串连起来的一溜珍珠?因此,我们很容易想到以下思路:
- 对待测表达式做预处理。具体的预处理有:如果表达式开头不是±号,则添加一个+去掉空白(
[ |\t]+
)、去掉^
后面的+
(replaceAll("\\^(\\+)", "^")
),对多余的±号做简化只保留一个(这一部分的做法请见jzy关于作业1框架的精品贴,质量有保证) - 把珍珠从线上拆解下来。用
[+-]+
来对表达式进行匹配和分割,来获得具体的每一项
// 创建Pattern对象
Pattern pattern = Pattern.compile("[+-]+");
// 创建matcher对象
Matcher matcher = pattern.matcher(input);
String [] list1 = input.split("[+-]+");
//需要注意的是,经过我们的预处理,list1里的第一项是空字符串,遍历它时需要从1开始。
-
维护一张map,为将来存储幂函数及其对应的系数做准备。
-
对每一项,做合并。具体的思路是,对某个项,记录一个系数变量,一个幂次变量,然后将该项按照
*
拆解开,得到因子。对每个因子,若它是整数,则(别忘了处理前导零和符号哦)把它乘到系数上,否则,分析出它的幂次,加到幂次变量上。在遍历完因子后,以幂次为key
,将系数加到map里。这里需要注意考虑项的符号,即前面拆解出来的matcher。
这样,我们就得到了一个记录表达式所有信息的map。很容易想到,对于等价的表达式,它们的map也是等价的,这个很好做判断。这样一来,我们就能评判两个表达式是否等价了。
评价
它的缺点无需多说,自己搓的轮子需要调试,且工作量比较大。但是它的优点让它面对更优雅、更方便的sympy时也有竞争的能力:
- 快。它很快,非常快,对于刚才提出的反例数据,它能在我们察觉不到的时间内评测出两个表达式的结果是否相等,和
sympy
的运行速度不在一个数量级上,甚至数量级都不在一个数量级上。 - 扩展性。读者如果有兴趣,不妨设想一下这个算法如何扩建,添加对别的函数的支持。小提示,善用课程组规定的输出要求,以及所有的函数都可以看作幂函数的底数,而Java可以重载HashMap的hashCode()。
- 能让我这种没发现sympy这种强大工具前就搓了轮子的人水一篇帖子并安慰自己搓的轮子并非毫无意义。
第三种方法:将对拍进行到底
这个思路其实说起来其实非常简单:
- 我们构建评测机的目的是测出来自己是否有bug
- 和别人对拍如果拍出来错,那么你和他至少有一个人犯错了,这就是说我们构建评测机的目的是对拍的子集
- 本homework性质特殊,输出也是符合输入规范的
那既然这样的话,我们为什么不这么做呢?
- 先调用两个人的jar,分别跑出来答案
- 把两个人的答案合并为
answer1-(answer2)
的形式 - 重新调用其中一个人的jar,重新跑出来答案
- 判断答案是否是字符串0
为什么这么做是对的?
- 我们不是测一组,是测几千组,如果有两个人不同时具有的bug,那么这个bug不被触发的概率是很小的,触发后因为另一个bug导致重新输出的结果变为0的概率更加渺小。
- 哪怕两个人可能有一样的bug,三个人如果还能有一摸一样的bug那只能说明需要进行查重了
- 哪怕第二次输出的不是0,而是0的等价形式,或本应判断相等但判断不相等,那么这意味着 1、合并同类项的部分出现了问题,这个问题不解决会比较麻烦。2、输出时不支持将某些等价零的项转化为0,这个问题解决起来比较简单,可以修改评测机的通过条件,或干脆修改作业代码,支持等价为0的项输出会转化成0——这可是个比较显然而有效的优化
- 如果还想增加这个评测机的健壮性,可以每次随机决定第二次调用哪个人的包,这样犯错误而疏漏掉某个bug的概率接近于无。
这个思路不需要自己搓轮子,跑的飞快(作业有多快就能跑多块,比sympy快了不知道多少倍),不会犯错(没有什么技术含量),代码量极低(相比别的评测机,就等于调用部分加上第二次调用的一点代码)
总而言之,好处很多(为什么我第一次作业没想到这个办法)
第四种方法:(没能派上用场的)蒙特卡洛方法
本方法本来是为三角函数的乱七八糟的化简准备的,但是本次作业并没有加三角函数,这个方法也就派不上用场了。
简述
还记得在第一篇精品帖子——一篇关于俄罗斯轮盘赌算法分享的帖子里,我介绍说俄罗斯轮盘赌算法来自于path tracing光线递归过程中因蒙特卡洛积分导致光线数量随递归层数成几何倍数级别增长吗?那,蒙特卡洛方法到底指的是什么呢?
蒙特卡洛积分,并不是指一类特殊的积分,而是指用蒙特卡洛积分方法,对满足一定性质(光滑等等)的积分求数值上的近似解。蒙特卡洛方法是随机的,它的其中一种简单实现方式可以概括如下:
- 指定一个概率分布(在评测机中可以指定为均匀分布或正态分布)
- 在被积函数的定义域上,按照概率分布随机生成若干个随机采样点
- 计算随机采样点的数值,并用计算出来的采样点数值,代替采样点附近的小邻域的均值
如果是在一般的蒙特卡洛方法里,接下来就是根据计算出来的小邻域均值,乘上小邻域的宽度(由采样点最近邻居距离代替),来估算积分的数值解。但是,在评测机中我们没必要这么做。
蒙特卡洛方法魔改评测机
假设现在homework的要求不再是支持指数函数,而是要求支持三角函数,那么三角函数那令人头疼的化简,和各位同学千差万别的化简方式会给评测机的构建带来巨大麻烦。既然很难从解析的角度判断两个式子是否相等(sympy太慢,自己造轮子就成了又一个作业了),那干脆就用蒙特卡罗方法拟合输出表达式对应的超平面,看不同人的输出对应的超平面是否贴近。更具体来说:
- 指定一个概率分布,和变量取值范围有关,干脆假设是均匀分布,分布在有符号整数的取值范围内吧
- 取若干个采样点,每个采样点对两个人的输出表达式分别计算结果,评估结果相差是否在一定范围内。因去年的hw2中,作业内并没有要求指数函数,因此该方式并不会因去年的样例而爆掉。不过今年就不一定了,很容易出现 e x p ( x ) 9999999 exp(x)^{9999999} exp(x)9999999的情况。
- 如果对足够多的采样点,两个人的输出均非常接近(考虑到浮点数的误差),那么可以认为,对该样例,两人输出一样。
- 事实上,这个方法的效率是可以接受的。因为作为输出的表达式,其天然具有一些良好性质。利用好这个良好性质,在几十秒以内能够得到较大样例的评测结果。再辅以大批次的数据测试,随机性带来的风险可以忽略不计
debug经验——如何简化混乱的随机数据
1.1 问题描述
在使用评测机评测程序的运行情况时,如果评测机的某个数据测了bug,那么我们就需要根据这个数据进行调试。但是,评测机的数据是随机生成的,这种数据对于我们人类而言很不友好,它的运行过程中往往会产生成百上千层的堆栈结构,足以恐吓住任何试图理解它的人。
但是,对于debug而言,一个数据有价值的部分往往占数据整体的一小部分。例如,因函数调用而产生的bug,不太可能与有符号整数的数值有关,999999和2在这种情况下是等价的。因此我们需要对这些数据进行简化,在保证能触发bug的情况下让数据简化到人类能理解的程度。这里我们就来讨论一下如何简化这些数据。
(这里顺带再提一个我遇到最离谱的bug,事后发现那个bug来自于哈希冲突,因此触发bug的条件非常苛刻,数据的化简非常艰难)
1.2 一个例子
假设以下数据测出来程序的bug了:
3
g(z,x)=- -+1-(--1*-2*exp(06)^+05* 8*+8++-6 *7* 2*9*z* 0*7*4 *+9* z^6*-7-+x^+01*(+++8*( 9*-8* x^0000007*+8*-0+z^003*8*3)^+000*9 * -68*+9*z*x^+3*-45+- z+6*5)* 7*+00*z^02- 1 *z+0*2*-0*+4
h(z,x,y)=-(z*-481--z*y*z+-8-x)+2*g(9,x)*(-x*y*0*(++2*4 )--0)^06*+3*-04--1 * 9--1 * g(x^8,8)*01*60
f(z,x)=-+(+-+0-+-4---1*5 *1++4*-0*x^008 )*8*6
+(4-(+1*x^+0005*-5* 9* x^+0*x^08*x^0000)+-+5 *dx(-7-x*8*+1+x+ x*0*6* 7*3*1*+5*x+-6* exp(x)^+4*x *x*-4 *-5*+5+-4)*+8)^1 *x^7*4*x^06
我的天哪,这么复杂的数据,如果直接放到idea里调试,那方法的调用关系足够爆掉任何一个试图理解的人的脑袋。
1、 但是仔细观察,可以发现三个自定义函数都没有在表达式中出现,直接去掉也不会有什么影响。去掉后发现,确实如此。
2、 再仔细思考一下,这个数据是针对第三次作业的,那么经过前两次的强测考验,比较平庸的部分是无法触发bug的。比如,对于一个项而言,它的常数因子和普通幂函数因子,以及所含因子没有导数和自定义函数因子的表达式都算比较平庸的部分。在这个思路指引下,我们试探着去掉这些部分:
0
+(4-(x^5*-5*9*x^08)+-+5 *dx(-7-x*8*+1+x+ x*0*6* 7*3*1*+5*x+-6* exp(x)^+4*x *x)*+8)
3、 经过测试确认bug依旧能被触发。这个数据对比最开始的数据已经很简化了,但是还不够简化。接下来,我们需要进行试探化简,即,根据第二步的思路,试探着去掉一些项和因子,看看数据是否还能触发bug。经过化简,我们得到以下数据:
0
dx(exp(x)^4)
和第一个数据对比,是不是天壤之别?但是它们的本质是一致的。顺带一提,被触发的bug是对带幂次的指数函数因子的错误求导。
在以上三步的指导下,我们能把一个很复杂的数据变得非常简单,非常易于理解,这对于我们debug而言有非常重要的意义。
hack经验——如何叉爆别人
还在为hack零成功率发愁吗?不用担心,不用着急,以下三步能让你在圣杯战争中争得一席之地。
2.1 评测机:你好
最简单最容易想到的方法,就是把所有人的代码下载下来,弄成jar,然后跑评测机。但是这个方法有一些问题:
- 对于有评测机的同学而言,他们房里的人应该也有自己的评测机,自己评测机能造出来的数据别人应该也能造。
- 评测机跑出来的数据一般都是上文一开始的那种形式,而且很容易出现在简化的过程中发现如果去掉某个指数则bug消失,但不去掉的话平台的const限制又会跟你玩寸止
所以,这个方法理论上可行,实际上没什么大用。
2.2 逻辑数据构造
第二个方法就是瞪眼,看别人的代码,结合第一种方法找出他们的逻辑缺陷,并构造不会被寸止的数据。但是这个方法也有很多缺点:
- 部分同学(比如我)深得防御性编程的精髓,哪怕迫于checkStyle的淫威不会有压行等行为,但是代码结构也不是一般人能轻易看懂的
- 能看懂的代码,则大部分不会有什么摆在明面上的逻辑bug,很容易好不容易用评测机找出了数据,把数据做了简化,然后跑代码找到了bug,这一套流程下来后面对bug的隐蔽和刁钻束手无策,最后不得不放弃。
2.3 暴力卡常数的数据构造
这个方法是我被人用数据狂暴鸿儒后才根据ta的思路总结出来的。因为强测对时间的要求其实是很宽松的,所以过了强测不一定代表代码不会有时间复杂度的问题。因此,可以利用这一点,构造出一些const很低但是对实现不好的同学而言跑的巨慢的数据。
(顺带一提,吐槽一下目前的互测重复bug评定机制,文档说的:针对重复bug提交三次及以上数据会受到处罚;同学们看到的:每找到一个bug可以得三次分,于是我被玉衡星刷了三次())
这个方法总结起来如下:
- 实际上,对于本次作业而言,如果想构造卡常数数据,那么指数和加、减、乘并不是好的选择,它们要么会导致const膨胀过快,要么虽然消耗了const配额但是对复杂度没什么贡献。
- 因此,我们需要多使用一些const增长是线性级别,但是却能提高语法树递归深度的因子,比如,表达式,比如,函数调用。
- 然后把把它们递归排列。这么做虽然看上去就能感觉到那令人绝望的语法树深度,但是const的增长是线性的,因此能在const要求范围内构造出特别容易卡掉实现不好的同学的数据
exp化简策略概述
exp的化简确实是个挺大的问题。考虑以下三个数据:
exp((1001*x+1000*x^2+1000*x^3+1000*x^4))
exp((999*x+1000*x^2+1000*x^3+1000*x^4))
exp((20*x+30*x^2+40*x^3))
这三个数据能够否定几乎所有的化简策略,证明它们不是最优的。因此,我们需要找到一个次优的策略。
首先能想到,针对第三个数据,有个化简策略是对exp内的表达式的每一项的系数做质因子分解。但是这么做的问题也挺大。最容易实现的分解质因数的方法是 O ( n ∗ n ) O(n*\sqrt{n}) O(n∗n)的,而打质数表的方法在本次作业中会因系数巨大被内存正义执行。所以这种策略不行。
那么,退而求其次,求最大公约数呢?这么做是可行的。在求最大公约数的过程中我们可以做一些面对随机数据比较有用的优化,比如如果当前枚举到的项的因数的最大公约数已经是1了就停止,不再继续枚举等等。
但是到这里还不算完,我们还可以想到两个优化方向:
一、枚举一些比较小的、我们指定的质因子,看看是否有质因子比最大公约数更优。这么做的时间复杂度是可以容忍的,它的运行时间应该和求最大公约数相差不大。
二、特判一下如果不提取公因数,而是把exp后面的幂次乘进去,看看哪个的长度更短
接下来可能还有别的乱七八糟的优化,比如启发式搜索等等,我个人认为这意义就不算很大了。事实上,观察评测机生成的随机数据,可以发现这些优化在大部分情况下都是够用的。一开始给出的三个数据是人为构造的数据,而随机生成的数据往往越强越没必要优化。
心得体会与未来方向——论OOpre
作为没有上过OOpre的同学,我完全有资格用分数论证OOpre的不必要性。但是实际上,我认为OOpre具有一定的必要性。首先,OOPre能够帮助git等工具链的学习和练习,这是非常有必要的。其次,OOpre的课程设置接近OO课程,这也有很大的用处。
抛开这个不谈,第一单元的OO确实给了我不少工作量上的压力。但是,OO这种在虚拟世界里以代码为触手实现一切的课程,让我深深地体会到代码的魅力。我在大一学年曾经开发过一些unity项目,并积累了一些不成体系的面向对象经验,但是在开始接受OO课程的磨砺后,我以更成熟的眼光审视之前的设计,还是发现了不少改进的空间。
而至于未来方向,我建议:1、无论同学们是否上过OOpre的课程,都至少在OO正式开始前发布一份相关的学习资料,例如基础语法、面向对象的概念等等。2、强烈建议将OOpre列为大二上的必修课。得益于C#的代码基础(C#和Java的语法几乎一样,只有继承等少数关键词不同),我并没有受困于语言学习的过程,但是对于别的懵懵懂懂的、没有选OOpre的同学而言,未必。我认为,无论如何改进OO m1,都不如强制性学习OOpre作用更大,