第一单元总结——迭代,优化与数据生成

安琦22373295 学生 2024-03-20 09:05:37

 

第一次作业架构

第一次作业分为表达式解析,多项式计算,结果输出三部分 

表达式预处理

表达式在解析之前需要进行预处理,用于简化指导书中出现的复杂情况:

  • 表达式起始点为+/-时,在最前方补0
  • 去除多余的空白字符
  • 简化连续的正负号

 表达式解析

 Lexer类实现了对语义的提取,对运算符和自变量,我们返回其本身;而对于数字,我们会先提取符号,然后逐位提取数字,并在这一过程中去除前导零,最终返回一个表示数字的字符串。

Parser类实现了对表达式的逐级提取,在本单元的作业中,根据优先级不同,我使用了Expr,Term,Factor的三层解析结构,逐级下降解析。

  • 在初始化Expr和Term时,均会调用下一级的解析方法
  • Factor类的生成最为复杂,根据GetToken返回的语义类型,我们有不同的构造方法:(1)对于"(",我们会调用解析表达式的方法并构造表达式因子。(2)对于“x",我们会构造自变量因子。(3)对于数字,我们会构造数字因子。完成后,还需要检索之后可能存在的"^"运算符,若存在,则会调用乘方运算方法。需要强调的是这三种因子均属于Factor类,只是存储时的系数与指数不同
  • 在构建Expr和Term时,完成对起始的Term/Factor的解析之后,我们会进入(1)读取运算符 (2)解析下一个Term/Factor (3)进行数学运算  (4)更新运算结果 这一循环,直至读取不到合理的运算符,此时我们会返回最终运算结果
  • Term的分割运算符是+/-,Factor的分割运算符是*

我们可以注意到,运算结果是实时更新的,因此我放弃了构造后缀表达式/显式语法树的处理方法,这为我第二、三次作业的迭代方向奠定了基础。

多项式存储

针对本次作业的三个层级,我们定义了Expr,Term,Factor三个类,内部均包含一个以x项指数为索引,系数为存储数据的TreeMap,用于描述对象代表的多项式,为快速将Expr转为Factor,我们直接令Expr类继承Factor类

ps:其实这里就可以把三个类合成成一个Poly类了,只不过到了第三次作业我才进行修改

多项式运算

  • 针对加减法,我们只需遍历第二个对象的TreeMap并合并与第一个对象的同类项即可,代码如下:
  •     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);
                }
            }
        }

     

  • 对于乘法,我们分别遍历两个对象的TreeMap,令对应项指数相加,系数相乘并合并最终结果的同类项,代码如下
  •     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;
        }
    

     

  • 对于幂运算,只需循环调用乘法运算并对指数为0的情况特判即可,代码如下
  •  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):

  • 若系数为1,则输出x^n
  • 若系数为-1,则输出-x^n
  • 若指数为0,则输出C
  • 若系数为1,则输出C*x
  • 若系数为正且不处于第一项,要补”+“
  • 输出时要尽可能先输出正项

 整体实现逻辑

 

根据本次作业的类图,我们可以总结出运行逻辑:Main读取输入字符串并初始化LexerLexer调用handleLexer进行预处理并初始化第一个Token然后基于这个对象初始化ParserParser基于通过GetToken获得的语义逐级调用parseExpr,parseTerm,parseFactor并进行运算,最后对返回的对象调用tostring方法进行输出

复杂度分析如下:

src.Expr.Expr()0.01.01.01.0
src.Factor.copy()0.01.01.01.0
src.Factor.CreateTermFromFactor()0.01.01.01.0
src.Factor.Factor()0.01.01.01.0
src.Factor.InitializeFactor(TreeMap)0.01.01.01.0
src.Factor.MultiFactor(Factor)23.01.08.08.0
src.Factor.PowFactor(Factor, int)4.01.03.03.0
src.Factor.printCoe(int, BigInteger)39.01.016.016.0
src.Factor.PrintPolynomial()13.04.07.07.0
src.Lexer.GetNum(int)9.02.06.07.0
src.Lexer.GetToken()0.01.01.01.0
src.Lexer.handleLexer()21.01.09.010.0
src.Lexer.Lexer(String)0.01.01.01.0
src.Lexer.nextToken()15.02.07.07.0
src.Main.main(String[])4.01.03.04.0
src.Parser.parseExpr()4.01.03.03.0
src.Parser.parseFactor()6.01.05.05.0
src.Parser.Parser(Lexer)0.01.01.01.0
src.Parser.parseTerm()1.01.02.02.0
src.Term.addPolynomial(Term)12.01.06.06.0
src.Term.CreateExprFromTerm()0.01.01.01.0
src.Term.InitializeTerm(TreeMap)0.01.01.01.0
src.Term.subPolynomial(Term)12.01.06.06.0
Total163.028.091.094.0
Average7.0869565217391311.21739130434782623.95652173913043464.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,并将函数名作为索引。

函数调用

本次作业在预处理之前进行函数调用,并采用由外而内代换的逻辑,具体思路描述如下:

  1. 找到函数名与相邻的“(",记录此时"("的位置tem1
  2. 利用括号匹配思路,找到对应的”)",记录位置为tem2
  3. 此时字符串分割成三段:[0,tem1]  (tem1,tem2) [tem2,end),我们将中间段传入Function内的代换函数
  4. 将返回的结果与前后两端进行拼接,继续循环

代码如下:

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方法可描述如下:

  1. 遍历expDataTreeMap,逐个获取每个元素的coe(系数),与对应的索引拼接成字符串,作为exp内部的字符串(若结果为空串,则不含exp部分)。此时对系数的优化与第一次作业一致。
  2. 判断是否需要补充括号
  3. 根据polyExp,在前方补充x项
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内的系数(这里可以看到不把系数加入字符串索引的合理性)

对于乘法,在进行单项的乘法时需要遵循以下步骤:

  1. 系数相乘
  2. x上的指数相加
  3. exp上的表达式相加

代码总结如下:

    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.01.01.01.0
src.Factor.copy()1.01.02.02.0
src.Factor.CreateTermFromFactor()0.01.01.01.0
src.Factor.Factor()0.01.01.01.0
src.Factor.getFactorMap()0.01.01.01.0
src.Factor.InitialFactor(TreeMap)0.01.01.01.0
src.Factor.MultiFactor(Factor)12.01.05.05.0
src.Factor.PowFactor(int)4.01.04.04.0
src.Factor.toString()54.04.016.016.0
src.Function.addFunctionBody(String)3.01.03.04.0
src.Function.CallFunction(String)2.01.02.03.0
src.Function.Function()0.01.01.01.0
src.Function.getSplitParameters(String)8.01.05.06.0
src.Lexer.AnalysisFunction()15.01.06.07.0
src.Lexer.GetNum(int)9.02.06.07.0
src.Lexer.GetToken()0.01.01.01.0
src.Lexer.handleLexer()21.01.09.010.0
src.Lexer.InitialLexer(String)0.01.01.01.0
src.Lexer.Lexer()0.01.01.01.0
src.Lexer.NextToken()15.02.07.07.0
src.Lexer.SaveFunction(String)0.01.01.01.0
src.Main.main(String[])3.01.03.04.0
src.Parser.parseExpr()4.01.03.03.0
src.Parser.parseFactor()7.01.06.06.0
src.Parser.Parser(Lexer)0.01.01.01.0
src.Parser.parseTerm()1.01.02.02.0
src.SingleData.addAndSubSingleData(SingleData, String)4.02.02.03.0
src.SingleData.copySingle()4.02.03.03.0
src.SingleData.dataMapToString()40.03.015.015.0
src.SingleData.getCoe()0.01.01.01.0
src.SingleData.multiPoly(SingleData)8.01.04.04.0
src.SingleData.set()0.01.01.01.0
src.SingleData.SingleData(BigInteger, BigInteger, TreeMap)0.01.01.01.0
src.SingleData.toString()30.05.013.015.0
src.Term.addTerm(Term)8.01.04.04.0
src.Term.CreateExprFromTerm()0.01.01.01.0
src.Term.InitialTerm(TreeMap)0.01.01.01.0
src.Term.subTerm(Term)8.01.04.04.0
src.Term.Term()0.01.01.01.0
Total261.052.0141.0151.0

Average

6.69230769230769251.33333333333333333.61538461538461543.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;

这种重构存在以下优点: 

  1. 实现单项之间的乘法时,可以直接调用Poly内的加法
  2. 实现对SingleData的深克隆时,可以调用Poly的深克隆方法,而Poly的深克隆方法会调用SingleData的深克隆方法,直至expPoly为空时才会返回
  3. 实现SingleDatatoString方法时,可以调用PolytoString方法,形成的递归关系与2一致

同时针对运算方法(包括求导),我们均会返回一个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.01.03.04.0
src.Function.CallFunction(String)2.01.02.03.0
src.Function.Function()0.01.01.01.0
src.Function.getSplitParameters(String)8.01.05.06.0
src.Lexer.AnalysisFunction()15.01.06.07.0
src.Lexer.GetNum(int)9.02.06.07.0
src.Lexer.GetToken()0.01.01.01.0
src.Lexer.handleLexer()21.01.09.010.0
src.Lexer.InitialLexer(String)0.01.01.01.0
src.Lexer.Lexer()0.01.01.01.0
src.Lexer.NextToken()15.02.07.07.0
src.Lexer.SaveFunction(String)0.01.01.01.0
src.Main.main(String[])3.01.03.04.0
src.Parser.parseExpr()4.01.03.03.0
src.Parser.parseFactor()8.01.07.07.0
src.Parser.Parser(Lexer)0.01.01.01.0
src.Parser.parseTerm()1.01.02.02.0
src.Poly.addExtraBracket(String, BigInteger)11.03.08.010.0
src.Poly.addPoly(Poly)8.01.04.04.0
src.Poly.CoeToString(BigInteger, String, int)28.01.012.012.0
src.Poly.copy()1.01.02.02.0
src.Poly.diffPoly()1.01.02.02.0
src.Poly.GetGcd(BigInteger, BigInteger)1.01.02.02.0
src.Poly.InitialPoly(TreeMap)0.01.01.01.0
src.Poly.isEmpty()0.01.01.01.0
src.Poly.MultiPoly(Poly)12.01.05.05.0
src.Poly.Poly()0.01.01.01.0
src.Poly.PowPoly(int)5.03.04.04.0
src.Poly.SeprateString()22.03.011.012.0
src.Poly.subPoly(Poly)8.01.04.04.0
src.Poly.ToBestString()46.08.012.015.0
src.Poly.ToBestStringOutOfExp()12.04.06.06.0
src.Poly.toString()12.04.06.06.0
src.SingleData.addAndSubSingleData(SingleData, String)4.02.02.03.0
src.SingleData.copySingle()2.02.02.02.0
src.SingleData.diffExpPart()2.02.02.02.0
src.SingleData.diffParaPart()2.02.02.02.0
src.SingleData.diffSingleData()0.01.01.01.0
src.SingleData.getCoe()0.01.01.01.0
src.SingleData.multiPoly(SingleData)0.01.01.01.0
src.SingleData.set()0.01.01.01.0
src.SingleData.SingleData(BigInteger, BigInteger, Poly)0.01.01.01.0
src.SingleData.ToBestString()10.02.06.06.0
src.SingleData.ToFactor()0.01.01.01.0
src.SingleData.toString()10.02.06.06.0
Total286.072.0166.0180.0
Average6.3555555555555551.63.6888888888888894.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时,我们会生成一个随机数,据此判断生成因子的种类,伴随着当前括号嵌套层数增大,会下降生成函数因子/表达式因子/求导因子的概率,并降低下层括号内表达式项和因子的最大个数与表达式的最大指数。

生成函数因子时,我们只需在括号内新生成表达式,并适当降低嵌套层数/项和因子个数/最大指数,并将结果用逗号分隔

生成自定义函数时,我们会修改数据种类,并随机生成形参。

不足与bug

在第二次作业中进行了不合理的优化:为避免在exp上的指数为一个负数时输出括号,我对代码进行了修改,但是忽略了形如"exp((-x))"情况,导致没有补充所需的括号,造成形式化表达错误

对于exp的优化过于复杂,没能将文中的两种优化情况合成更泛用的情况,导致一个强测点丢失性能分

Poly类过长,理论上可以将所有运算新建成新的运算类,只保留字符串构造与优化方法

互测思路

本次互测我主要使用自己的数据生成器与简陋版评测机对代码进行联合测试,同时,我会对一些特殊的数据点进行重点检查,例如"exp(0)","(0)^0","exp((-x))",还包括一些函数嵌套调用的情况,但是我对于卡TLE的方法并未进行很好的测试,所以没找到bug)

心得体会

经过这一单元的学习,我对递归下降法,语法树等内容有了新的认识。我也认识到了一个良好架构的重要性,应当在设计初期更多地使用面向对象的思路进行设计,考虑可能的拓展方向,提高代码的可扩展性。同时对于过长的类和方法,应当对其进行拆分,降低内部耦合度

...全文
128 1 打赏 收藏 转发到动态 举报
写回复
用AI写文章
1 条回复
切换为时间正序
请发表友善的回复…
发表回复
安琦22373295 学生 2024-03-20
  • 打赏
  • 举报
回复

很好 收藏了

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧