301
社区成员
发帖
与我相关
我的任务
分享
第一次作业分为表达式解析,多项式计算,结果输出三部分
表达式在解析之前需要进行预处理,用于简化指导书中出现的复杂情况:
Lexer类实现了对语义的提取,对运算符和自变量,我们返回其本身;而对于数字,我们会先提取符号,然后逐位提取数字,并在这一过程中去除前导零,最终返回一个表示数字的字符串。
Parser类实现了对表达式的逐级提取,在本单元的作业中,根据优先级不同,我使用了Expr,Term,Factor的三层解析结构,逐级下降解析。
我们可以注意到,运算结果是实时更新的,因此我放弃了构造后缀表达式/显式语法树的处理方法,这为我第二、三次作业的迭代方向奠定了基础。
针对本次作业的三个层级,我们定义了Expr,Term,Factor三个类,内部均包含一个以x项指数为索引,系数为存储数据的TreeMap,用于描述对象代表的多项式,为快速将Expr转为Factor,我们直接令Expr类继承Factor类
ps:其实这里就可以把三个类合成成一个Poly类了,只不过到了第三次作业我才进行修改
public void addTerm(Term input) {
Set<String> inputStringSet = input.termMap.keySet();
for (String str : inputStringSet) {
SingleData inputSingle = input.termMap.get(str);
if (termMap.containsKey(str)) {
SingleData originSingle = termMap.get(str);
SingleData newSingle = originSingle.addAndSubSingleData(inputSingle, "+");
if (newSingle == null) {
termMap.remove(str);
} else {
termMap.replace(str, newSingle);
}
} else {
termMap.put(str, inputSingle);
}
}
}
public void MultiFactor(Factor inputFactor) {
TreeMap<Integer, BigInteger> polyMultiRes = new TreeMap<>();
Set<Integer> set1 = polynomial.keySet();
Set<Integer> set2 = inputFactor.polynomial.keySet();
if (!polynomial.isEmpty() && !inputFactor.polynomial.isEmpty()) {
for (Integer i : set1) {
for (Integer j : set2) {
int expRes = i + j;
BigInteger coeRes = polynomial.get(i).multiply(inputFactor.polynomial.get(j));
if (polyMultiRes.containsKey(expRes)) {
BigInteger tem = coeRes.add(polyMultiRes.get(expRes));
if (tem.signum() != 0) {
polyMultiRes.replace(expRes, tem);
} else {
polyMultiRes.remove(expRes, polyMultiRes.get(expRes));
}
} else {
if (coeRes.signum() != 0) {
polyMultiRes.put(expRes, coeRes);
}
}
}
}
}
polynomial = polyMultiRes;
}
public void PowFactor(Factor inputFactor, int exp) {
Factor tem = inputFactor.copy();
if (exp == 0) {
TreeMap<Integer, BigInteger> res = new TreeMap<>();
res.put(0, BigInteger.valueOf(1));
polynomial = res;
} else {
for (int i = 1; i < exp; i++) {
MultiFactor(tem);
}
}
}
注:考虑到表达式解析的具体过程,我们将乘法与幂运算定义在Factor内,将加减法运算定义在Term内,且方法内只会对调用该方法的对象内容进行修改,不会返回数据。这样在所有的乘法/加减法计算完成后,只需将最新的Factor/Term转成Term/Factor即可
在表达式的解析和运算全部完成之后,我们会得到一个Expr类对象,包含着整个表达式的信息,此时需要调用toString方法进行输出,以下为具体的简化原则(设系数为C):

根据本次作业的类图,我们可以总结出运行逻辑:Main读取输入字符串并初始化Lexer,Lexer调用handleLexer进行预处理并初始化第一个Token,然后基于这个对象初始化Parser,Parser基于通过GetToken获得的语义逐级调用parseExpr,parseTerm,parseFactor并进行运算,最后对返回的对象调用tostring方法进行输出
复杂度分析如下:
| src.Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.copy() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.CreateTermFromFactor() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.Factor() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.InitializeFactor(TreeMap) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.MultiFactor(Factor) | 23.0 | 1.0 | 8.0 | 8.0 |
| src.Factor.PowFactor(Factor, int) | 4.0 | 1.0 | 3.0 | 3.0 |
| src.Factor.printCoe(int, BigInteger) | 39.0 | 1.0 | 16.0 | 16.0 |
| src.Factor.PrintPolynomial() | 13.0 | 4.0 | 7.0 | 7.0 |
| src.Lexer.GetNum(int) | 9.0 | 2.0 | 6.0 | 7.0 |
| src.Lexer.GetToken() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Lexer.handleLexer() | 21.0 | 1.0 | 9.0 | 10.0 |
| src.Lexer.Lexer(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Lexer.nextToken() | 15.0 | 2.0 | 7.0 | 7.0 |
| src.Main.main(String[]) | 4.0 | 1.0 | 3.0 | 4.0 |
| src.Parser.parseExpr() | 4.0 | 1.0 | 3.0 | 3.0 |
| src.Parser.parseFactor() | 6.0 | 1.0 | 5.0 | 5.0 |
| src.Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Parser.parseTerm() | 1.0 | 1.0 | 2.0 | 2.0 |
| src.Term.addPolynomial(Term) | 12.0 | 1.0 | 6.0 | 6.0 |
| src.Term.CreateExprFromTerm() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Term.InitializeTerm(TreeMap) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Term.subPolynomial(Term) | 12.0 | 1.0 | 6.0 | 6.0 |
| Total | 163.0 | 28.0 | 91.0 | 94.0 |
| Average | 7.086956521739131 | 1.2173913043478262 | 3.9565217391304346 | 4.086956521739131 |
相较于第一次作业,第二次新增了自定义函数与指数函数,以下将对这两个方面进行重点论述
本次作业新增了Function类,包含了函数的参数列表与函数体。
在分割参数与函数体之前,我们需要做一些预处理:将exp替换为e,将x替换为u。
String[] splitInput = tem.split("=");
functionBody = splitInput[1].replaceAll("[ \t]", "");
if (functionBody.charAt(0) == '-' || functionBody.charAt(0) == '+') {
functionBody = "0" + functionBody;
}
//函数体与main中的与处理方法保持一致
String stringPattern = "([uyz])";
Pattern pattern = Pattern.compile(stringPattern);
Matcher matcher = pattern.matcher(splitInput[0]);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
sb.append(matcher.group());
}
parameters = sb.toString();
这一段代码描述了分别提取参数列表与函数体的具体操作。即把“=”作为分隔符,等号之前的字符串利用正则表达式匹配参数列表,等号之后的字符串做补0操作得到函数体。
同时,在Lexer类中,我们也增加了存储函数对象的HashMap,并将函数名作为索引。
本次作业在预处理之前进行函数调用,并采用由外而内代换的逻辑,具体思路描述如下:
代码如下:
while (anaFuncLocation < lexer.length()) {
if (functionHashMap.containsKey(lexer.charAt(anaFuncLocation))) {
Function function = functionHashMap.get(lexer.charAt(anaFuncLocation));
String before = lexer.substring(0, anaFuncLocation);
int searchFuncEnd = anaFuncLocation + 1;
int bracketCount = 1;
while (bracketCount != 0) {
searchFuncEnd++;
if (lexer.charAt(searchFuncEnd) == '(') {
bracketCount++;
} else if (lexer.charAt(searchFuncEnd) == ')') {
bracketCount--;
}
}
String end;
if (searchFuncEnd + 1 >= lexer.length()) {
end = "";
} else {
end = lexer.substring(searchFuncEnd + 1);
}
String mid = lexer.substring(anaFuncLocation + 2, searchFuncEnd);
String processedMid = function.CallFunction(mid);
lexer = before + "(" + processedMid + ")" + end;
}
anaFuncLocation++;
}
注:这个方法完美适配第三次作业新增的函数定义方式 天才的做法!
而对于Function内的代换,我们只需要利用统计左右括号数目的方法,找到最外层","作为实参的分隔符,再调用replaceAll方法逐个替换对应形参即可。特别需要注意的是,代换时要在实参外套上一层括号
加入exp之后,单项的统一表现形式为:C*x^k*exp(expr),对此,我们新增了SingleData类描述单项
private BigInteger coe;
private BigInteger polyExp;
private TreeMap<String, SingleData> expDataTreeMap;
它们分别对应C,k,expr
相对的,Expr,Term,Factor类对多项式的存储也进行了对应更新:
private TreeMap<String, SingleData> factorMap;
这里选用String作为索引的原因在于,按照相同规则转化而成的字符串与单项存在一一对应关系,便于进一步合并同类项,而TreeMap的使用维护了单项之间的相应顺序,确保了字符串转化规则的唯一性。
每生成一个SingleData对象,我们都需要调用一次toString方法获取索引,toString方法可描述如下:
public String toString() {
if (coe.signum() == 0) {
return "";
} else {
StringBuilder sb = new StringBuilder();
if (polyExp.equals(BigInteger.ONE)) {
sb.append("x");
} else if (!polyExp.equals(BigInteger.ZERO)) {
sb.append("x^");
sb.append(polyExp);
}
//将x项构造为字符串
if (!expDataTreeMap.isEmpty()) {
if (!polyExp.equals(BigInteger.ZERO)) {
sb.append("*");
}
sb.append("e(");
String expStr = dataMapToString();
int expStrLen = expStr.length();
int bracketCount = 0;
int bracketFlag = 0;
for (int i = 0; i < expStrLen; i++) {
if (expStr.charAt(i) == '(') {
bracketCount++;
} else if (expStr.charAt(i) == ')') {
bracketCount--;
}
if ((expStr.charAt(i) == '+' || expStr.charAt(i) == '*'
|| expStr.charAt(i) == '-') && bracketCount == 0) {
bracketFlag = 1;
break;
}
}
if (bracketFlag == 1) {
sb.append("(");
}
sb.append(expStr);
if (bracketFlag == 1) {
sb.append(")");
}
sb.append(")");
}
return sb.toString();
}
}
注:这里的dataMapToString方法表示第一步,关于括号补充和优化的细节已经在代码中呈现。
对于加减法,运算思路与第一次不变,只不过合并同类项的标准变成了字符串索引,并且需要修改合并后SingleData内的系数(这里可以看到不把系数加入字符串索引的合理性)
对于乘法,在进行单项的乘法时需要遵循以下步骤:
代码总结如下:
public SingleData multiPoly(SingleData inputSingle) {
BigInteger newCoe = coe.multiply(inputSingle.coe);
BigInteger newXExp = polyExp.add(inputSingle.polyExp);
TreeMap<String, SingleData> resTreeMap = copySingle().expDataTreeMap;
TreeMap<String, SingleData> inputDataTreeMap = inputSingle.expDataTreeMap;
Set<String> inputDataSet = inputDataTreeMap.keySet();
for (String inputStr : inputDataSet) {
if (resTreeMap.containsKey(inputStr)) {
SingleData tem = resTreeMap.get(inputStr);
SingleData resSingle = tem.addAndSubSingleData(inputDataTreeMap.get(inputStr), "+");
if (resSingle == null) {
resTreeMap.remove(inputStr);
} else {
resTreeMap.replace(inputStr, resSingle);
}
} else {
resTreeMap.put(inputStr, inputDataTreeMap.get(inputStr));
}
}
//令exp中的指数相加 确保只有一个exp
return new SingleData(newCoe, newXExp, resTreeMap);
}
注:这里的addAndSubSingleData方法用于合并单项同类项
这一方法存在返回值,为避免误修改原对象的值,我们需要对调用这一方法的对象进行深克隆,也就是我们需要遍历TreeMap,对包含的每一个对象进行深克隆,形成递归克隆关系。代码实现如下:
public SingleData copySingle() {
if (expDataTreeMap.isEmpty()) {
TreeMap<String, SingleData> empty = new TreeMap<>();
return new SingleData(coe, polyExp, empty);
} else {
Set<String> strSet = expDataTreeMap.keySet();
TreeMap<String, SingleData> copyTreeMap = new TreeMap<>();
for (String str : strSet) {
SingleData tem = expDataTreeMap.get(str);
copyTreeMap.put(str, tem.copySingle());
}
return new SingleData(coe, polyExp, copyTreeMap);
}
}
对于幂运算,我们仍然是循环进行乘法运算,只不过在克隆原对象并传入乘法运算方法时,需要使用深克隆。
与第一次作业相似,我们最后只会得到唯一的Expr对象,对其调用toString方法即可得到最终结果,此时的具体实现与SingleData中对exp上字符串的构建完全一致。
在Lexer类中,我们新增了语义"e"作为exp的标记
在构造Factor时,我们新增了exp的情况:根据括号内的表达式结果新建SingleData对象,再将其放入新建的TreeMap中,并据此初始化Factor对象

本次作业的总体思路与第一次一致,只不过在读取表达式之前需要先存储可能存在的函数。
复杂度分析如下:
| src.Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.copy() | 1.0 | 1.0 | 2.0 | 2.0 |
| src.Factor.CreateTermFromFactor() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.Factor() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.getFactorMap() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.InitialFactor(TreeMap) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Factor.MultiFactor(Factor) | 12.0 | 1.0 | 5.0 | 5.0 |
| src.Factor.PowFactor(int) | 4.0 | 1.0 | 4.0 | 4.0 |
| src.Factor.toString() | 54.0 | 4.0 | 16.0 | 16.0 |
| src.Function.addFunctionBody(String) | 3.0 | 1.0 | 3.0 | 4.0 |
| src.Function.CallFunction(String) | 2.0 | 1.0 | 2.0 | 3.0 |
| src.Function.Function() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Function.getSplitParameters(String) | 8.0 | 1.0 | 5.0 | 6.0 |
| src.Lexer.AnalysisFunction() | 15.0 | 1.0 | 6.0 | 7.0 |
| src.Lexer.GetNum(int) | 9.0 | 2.0 | 6.0 | 7.0 |
| src.Lexer.GetToken() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Lexer.handleLexer() | 21.0 | 1.0 | 9.0 | 10.0 |
| src.Lexer.InitialLexer(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Lexer.Lexer() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Lexer.NextToken() | 15.0 | 2.0 | 7.0 | 7.0 |
| src.Lexer.SaveFunction(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Main.main(String[]) | 3.0 | 1.0 | 3.0 | 4.0 |
| src.Parser.parseExpr() | 4.0 | 1.0 | 3.0 | 3.0 |
| src.Parser.parseFactor() | 7.0 | 1.0 | 6.0 | 6.0 |
| src.Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Parser.parseTerm() | 1.0 | 1.0 | 2.0 | 2.0 |
| src.SingleData.addAndSubSingleData(SingleData, String) | 4.0 | 2.0 | 2.0 | 3.0 |
| src.SingleData.copySingle() | 4.0 | 2.0 | 3.0 | 3.0 |
| src.SingleData.dataMapToString() | 40.0 | 3.0 | 15.0 | 15.0 |
| src.SingleData.getCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.multiPoly(SingleData) | 8.0 | 1.0 | 4.0 | 4.0 |
| src.SingleData.set() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.SingleData(BigInteger, BigInteger, TreeMap) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.toString() | 30.0 | 5.0 | 13.0 | 15.0 |
| src.Term.addTerm(Term) | 8.0 | 1.0 | 4.0 | 4.0 |
| src.Term.CreateExprFromTerm() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Term.InitialTerm(TreeMap) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Term.subTerm(Term) | 8.0 | 1.0 | 4.0 | 4.0 |
| src.Term.Term() | 0.0 | 1.0 | 1.0 | 1.0 |
| Total | 261.0 | 52.0 | 141.0 | 151.0 |
|
Average | 6.6923076923076925 | 1.3333333333333333 | 3.6153846153846154 | 3.871794871794872 |
第三次作业中,我对历史遗留的问题架构进行了重构,实现了新添加的求导功能,并尝试对输出长度进行优化
我们可以注意到,Factor类与Term类有着完全相同的数据,但方法有很大不同,对此,我用Poly类代替了Expr,Term,Factor这三个类,并将这三个类的所有方法迁移至Poly中。
public class Poly {
private TreeMap<String, SingleData> polyMap;
而对于SingleData类,我们将exp上的表达式用一个Poly对象表示
public class SingleData implements TransToFactor {
private BigInteger coe;
private BigInteger paraExp;
private Poly expPoly;
这种重构存在以下优点:
同时针对运算方法(包括求导),我们均会返回一个Poly对象,配合深克隆的使用,可以避免因运算对象被修改而导致的奇怪错误
针对表达式,我们只需将表达式内的每个单项进行求导在相加。针对单项,我们将其分为两部分:C*x^k 和 exp(……),只需对这两个部分分别求导并待入求导公式即可(注:需要特判k为0和exp内为0的情况)
前一项:
public Poly diffParaPart() {
if (paraExp.compareTo(BigInteger.ZERO) == 0) {
return new Poly();
} else {
Poly emptyPoly = new Poly();
SingleData tem = new SingleData(coe.multiply(paraExp),
paraExp.subtract(BigInteger.ONE), emptyPoly);
return tem.ToFactor();
}
}
而对于后一段,我们会得到 后一项自身*exp内表达式的求导结果,此时会调用Poly内对表达式的求导方法,实现递归关系:
public Poly diffExpPart() {
if (expPoly.isEmpty()) {
return new Poly();
} else {
SingleData tem = new SingleData(BigInteger.ONE, BigInteger.ZERO, expPoly);
Poly trans = tem.ToFactor();
return trans.MultiPoly(expPoly.diffPoly());
}
}
最后调用乘法公式
public Poly diffSingleData() {
SingleData tem1 = new SingleData(coe, paraExp, new Poly());
SingleData tem2 = new SingleData(BigInteger.ONE, BigInteger.ZERO, expPoly);
Poly firstPart = tem1.ToFactor().MultiPoly(diffExpPart());
Poly secondPart = tem2.ToFactor().MultiPoly(diffParaPart());
return firstPart.addPoly(secondPart);
}
预处理时,把所有的dx替换为d
在Lexer类中,我们新增了语义"d"作为dx的标记
在构造Poly时,我们新增了dx的情况:对括号内的表达式调用求导方法,得到返回的Poly对象
作业新增了Num,Var两个类由于描述数字因子和变量因子,并使用接口重写了ToFactor方法,便于Poly的初始化

复杂度分析:
| src.Function.addFunctionBody(String) | 3.0 | 1.0 | 3.0 | 4.0 |
| src.Function.CallFunction(String) | 2.0 | 1.0 | 2.0 | 3.0 |
| src.Function.Function() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Function.getSplitParameters(String) | 8.0 | 1.0 | 5.0 | 6.0 |
| src.Lexer.AnalysisFunction() | 15.0 | 1.0 | 6.0 | 7.0 |
| src.Lexer.GetNum(int) | 9.0 | 2.0 | 6.0 | 7.0 |
| src.Lexer.GetToken() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Lexer.handleLexer() | 21.0 | 1.0 | 9.0 | 10.0 |
| src.Lexer.InitialLexer(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Lexer.Lexer() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Lexer.NextToken() | 15.0 | 2.0 | 7.0 | 7.0 |
| src.Lexer.SaveFunction(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Main.main(String[]) | 3.0 | 1.0 | 3.0 | 4.0 |
| src.Parser.parseExpr() | 4.0 | 1.0 | 3.0 | 3.0 |
| src.Parser.parseFactor() | 8.0 | 1.0 | 7.0 | 7.0 |
| src.Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Parser.parseTerm() | 1.0 | 1.0 | 2.0 | 2.0 |
| src.Poly.addExtraBracket(String, BigInteger) | 11.0 | 3.0 | 8.0 | 10.0 |
| src.Poly.addPoly(Poly) | 8.0 | 1.0 | 4.0 | 4.0 |
| src.Poly.CoeToString(BigInteger, String, int) | 28.0 | 1.0 | 12.0 | 12.0 |
| src.Poly.copy() | 1.0 | 1.0 | 2.0 | 2.0 |
| src.Poly.diffPoly() | 1.0 | 1.0 | 2.0 | 2.0 |
| src.Poly.GetGcd(BigInteger, BigInteger) | 1.0 | 1.0 | 2.0 | 2.0 |
| src.Poly.InitialPoly(TreeMap) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Poly.isEmpty() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Poly.MultiPoly(Poly) | 12.0 | 1.0 | 5.0 | 5.0 |
| src.Poly.Poly() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.Poly.PowPoly(int) | 5.0 | 3.0 | 4.0 | 4.0 |
| src.Poly.SeprateString() | 22.0 | 3.0 | 11.0 | 12.0 |
| src.Poly.subPoly(Poly) | 8.0 | 1.0 | 4.0 | 4.0 |
| src.Poly.ToBestString() | 46.0 | 8.0 | 12.0 | 15.0 |
| src.Poly.ToBestStringOutOfExp() | 12.0 | 4.0 | 6.0 | 6.0 |
| src.Poly.toString() | 12.0 | 4.0 | 6.0 | 6.0 |
| src.SingleData.addAndSubSingleData(SingleData, String) | 4.0 | 2.0 | 2.0 | 3.0 |
| src.SingleData.copySingle() | 2.0 | 2.0 | 2.0 | 2.0 |
| src.SingleData.diffExpPart() | 2.0 | 2.0 | 2.0 | 2.0 |
| src.SingleData.diffParaPart() | 2.0 | 2.0 | 2.0 | 2.0 |
| src.SingleData.diffSingleData() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.getCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.multiPoly(SingleData) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.set() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.SingleData(BigInteger, BigInteger, Poly) | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.ToBestString() | 10.0 | 2.0 | 6.0 | 6.0 |
| src.SingleData.ToFactor() | 0.0 | 1.0 | 1.0 | 1.0 |
| src.SingleData.toString() | 10.0 | 2.0 | 6.0 | 6.0 |
| Total | 286.0 | 72.0 | 166.0 | 180.0 |
| Average | 6.355555555555555 | 1.6 | 3.688888888888889 | 4.0 |
优化的本质是缩短exp括号内表达式的长度,核心思路为利用局部的最优实现全局的最优。在通用的正项提前,合理去括号等方法之外,还存在一些更好的优化思路。
对于exp内为单项的情况,若系数大于一且该项不为常数项,则需要将系数提出exp,这样可以在内部不剩余乘号的情况下去除一层括号且保持其余部分的总长度不变,代码如下
StringBuilder sb = new StringBuilder();
StringBuilder expStringBuilder = new StringBuilder();
if (polyMap.size() == 1) {
SingleData first = polyMap.get(polyMap.firstKey());
BigInteger firstCoe = first.getCoe();
if (!polyMap.firstKey().isEmpty()) {
if (firstCoe.signum() == 1) {
expStringBuilder.append(first.ToBestString());
} else if (firstCoe.signum() == -1) {
expStringBuilder.append("-").append(first.ToBestString());
}
String expString = addExtraBracket(expStringBuilder.toString(),
firstCoe.abs());
sb.append(expString);
} else {
sb.append("e(").append(firstCoe).append(")");
}
return sb.toString();
}
对于内部为多项式的情况,最常用的是提取出所有系数绝对值的公因数,若想追求最优解,还需要考虑公因子的所有因数,但考虑到时间复杂度的问题(实际上这种优化是只能减少一到两个字符的长度),本次作业只做了提取最大公因数的处理,并将结果与未处理的原表达式比较,代码如下。
String positiveStr = "Aww Man!";
BigInteger gcd = polyMap.get(polyMap.firstKey()).getCoe();
for (String str : polyMap.keySet()) {
BigInteger tem = polyMap.get(str).getCoe();
gcd = GetGcd(gcd, tem);
if (gcd.equals(BigInteger.ONE)) {
break;
}
}
for (String str : polyMap.keySet()) {
BigInteger coe = polyMap.get(str).getCoe().divide(gcd);
if (coe.signum() == 1) {
expStringBuilder.append(CoeToString(coe, str, 1));
expStringBuilder.append(polyMap.get(str).ToBestString());
positiveStr = str;
break;
}
}
for (String str : polyMap.keySet()) {
BigInteger coe = polyMap.get(str).getCoe().divide(gcd);
if (!str.equals(positiveStr)) {
expStringBuilder.append(CoeToString(coe, str, 0));
expStringBuilder.append(polyMap.get(str).ToBestString());
}
}
String before = addExtraBracket(toString(), BigInteger.ONE);
String expString = addExtraBracket(expStringBuilder.toString(), gcd);
String res = expString.length() > before.length() ? before : expString;
第二种优化方式是对exp内部的表达式进行拆分。考虑表达式:exp((114514+114514*x+114514*x^2+114514*x^3+1919*x^4)),此时提取公因子的方法失效,我们选择将系数相同的项拆分出来,得到exp((1+x+x^2+x^3))^114514*exp(x^4)^1919810,代码如下
public String SeprateString() {
TreeMap<String, SingleData> copyMap = copy().polyMap;
HashMap<BigInteger, TreeMap<String, SingleData>> sameCoeMap = new HashMap<>();
Set<String> strSet = polyMap.keySet();
for (String str : strSet) {
SingleData tem = polyMap.get(str);
BigInteger coe = tem.getCoe().abs();
if (!sameCoeMap.containsKey(coe)) {
TreeMap<String, SingleData> newMap = new TreeMap<>();
newMap.put(str, polyMap.get(str));
sameCoeMap.put(coe, newMap);
} else {
sameCoeMap.get(coe).put(str, polyMap.get(str));
}
}
if (sameCoeMap.size() == 1) {
return null;
}
ArrayList<BigInteger> deleteList = new ArrayList<>();
for (BigInteger big : sameCoeMap.keySet()) {
int size = sameCoeMap.get(big).size();
int deltaLength = (size - 1) * (big.toString().length() + 1) - 8;
if (deltaLength > 0) {
TreeMap<String, SingleData> tem = sameCoeMap.get(big);
for (String str : tem.keySet()) {
copyMap.remove(str);
}
} else {
deleteList.add(big);
}
}
for (BigInteger big : deleteList) {
sameCoeMap.remove(big);
}
if (copyMap.size() != polyMap.size()) {
StringBuilder sb = new StringBuilder();
int factorCount = 0;
if (!copyMap.isEmpty()) {
Poly poly = new Poly();
poly.InitialPoly(copyMap);
sb.append(poly.ToBestString());
factorCount++;
}
for (BigInteger big : sameCoeMap.keySet()) {
Poly tem = new Poly();
tem.InitialPoly(sameCoeMap.get(big));
if (factorCount != 0) {
sb.append("*");
}
sb.append(tem.ToBestString());
factorCount++;
}
return sb.toString();
} else {
return null;
}
}
}
在这种方式中,我们只需提出系数绝对值相同数目超过两个的项,减小长度=(系数绝对值长度+1)*(出现系数-1)-8,8来自新产生的“(*exp(()))”,考虑到其他复杂因素,误差范围是-2到+3。完成后,只需在两种方式选出最优结果即可
对于分组提取或拆分的思路,本次作业未作探讨,已经有大佬发表过对应内容了
与递归下降法类似,我们采用递归生成数据的方式,也就是GenExpr,GenTerm,GenFactor三层嵌套思路,逐层递归生成。对于每一个Generate对象,我们用以下参数进行调控:
生成时最底层的Factor时,我们会生成一个随机数,据此判断生成因子的种类,伴随着当前括号嵌套层数增大,会下降生成函数因子/表达式因子/求导因子的概率,并降低下层括号内表达式项和因子的最大个数与表达式的最大指数。
生成函数因子时,我们只需在括号内新生成表达式,并适当降低嵌套层数/项和因子个数/最大指数,并将结果用逗号分隔
生成自定义函数时,我们会修改数据种类,并随机生成形参。
在第二次作业中进行了不合理的优化:为避免在exp上的指数为一个负数时输出括号,我对代码进行了修改,但是忽略了形如"exp((-x))"情况,导致没有补充所需的括号,造成形式化表达错误
对于exp的优化过于复杂,没能将文中的两种优化情况合成更泛用的情况,导致一个强测点丢失性能分
Poly类过长,理论上可以将所有运算新建成新的运算类,只保留字符串构造与优化方法
本次互测我主要使用自己的数据生成器与简陋版评测机对代码进行联合测试,同时,我会对一些特殊的数据点进行重点检查,例如"exp(0)","(0)^0","exp((-x))",还包括一些函数嵌套调用的情况,但是我对于卡TLE的方法并未进行很好的测试,所以没找到bug)
经过这一单元的学习,我对递归下降法,语法树等内容有了新的认识。我也认识到了一个良好架构的重要性,应当在设计初期更多地使用面向对象的思路进行设计,考虑可能的拓展方向,提高代码的可扩展性。同时对于过长的类和方法,应当对其进行拆分,降低内部耦合度
很好 收藏了