第一次作业总结

  • 类关系图

    • 总类图

      • 面向对象设计与构造第一单元博客作业
      • 群魔乱舞,眼花缭乱,这主要是因为学递归下降的时候发现这东西太霸道了
      • 加上第一周时间充裕,手感一来就顺手把判WF,表达式因子,三角函数嵌套之类的都写完了
    • 核心部分类图

      • 面向对象设计与构造第一单元博客作业
      • 把三角函数相关的东西,表达式因子相关的东西干掉之后,类图就清爽多了
      • 结构是比较清晰明显的,Reader类和Regex类为公用的读入类和正则类,若干Node类按照形式化定义层次化组织成多叉表达式树
    • 重要类/接口分析

      • Node

        • 各种Node的父类,维护一个Element,和一个BigIntegerElement用于判断两个类是否可合并,BigInteger储存Element以外的信息,对于不同的Node子类,有不同的意义
        • 例如,对于幂函数RegularNodeElement为空,BigInteger存储指数
        • 例如,对于表达式ExprNodeElement为表达式中全部的项,BigIntegerBigInteger.ONE
      • FactorNode

        • 逻辑上,是所有种类因子的父类
        • 功能上,作为工厂类,用于产生不同种类的因子
      • Element

        • 各种Element的父类,采用一个HashMap<Element, Node>存储这个节点的儿子节点,这样可保证“天然的”合并同类项
        • 每增加一个儿子,都会判断是否可以合并,当且仅当不能合并时,添加这个儿子
        • 判断两个HashMap是否相等的实现是A removeAll B为空且B removeAll A为空
      • Calculatable接口

        • 泛型接口Calculatable<T extends Calculatable>,定义了以下三个行为
        • 求导行为T diffIt()
        • 化简行为T optimize()
        • 合并行为void addMerge(BigInteger in)
        • Node类实现了这一接口
        • 通过<T extends Calculatable>可以强制求导/化简行为的返回类型,从而维护这一结构:表达式下能且只能是项,项下能且只能是某种因子
  • 基于度量的结构分析

    • 代码规模分析

      • 面向对象设计与构造第一单元博客作业
      • 代码量整体较大,但较均匀的分布在各个类,没有过于臃肿庞大的类
    • 方法复杂度分析

      • 面向对象设计与构造第一单元博客作业
      • 比较大的方法主要是针对表达式因子优化的方法,这次作业用到的方法都不怎么复杂
    • 类复杂度分析

      • 面向对象设计与构造第一单元博客作业
      • 平均操作复杂度较高的是FactorNode工厂类,因为就一个方法通过多个if创建多种因子
      • 加权方法复杂度较高的是ExprNode类、ExpfctrNode类和TermNode类,这些类中用于优化的方法拉高了加权方法复杂度
  • 优缺点分析

    • 优点分析

      • 采用递归下降解析,对求导/化简等操作层次化处理,有良好的可扩展性,化简功能较完善,加之本次作业比较简单,理论上可化简至最短形式
    • 问题分析

      • 针对本次作业没有明显问题,但针对后续作业的同类项合并等操作时间开销较大,需要辅以计时熔断等机制以保证在限定时间范围内得到结果
  • 优化总结

    • 乘0优化:

      • 0*0 化简为 0
      • x**2*0 化简为 0
    • 加0优化:

      • 0+0 化简为 0
      • x**2+0 化简为 x**2
    • 乘1优化:

      • 1*1 化简为 1
      • x**2*1 化简为 x**2
    • 简单合并同类项:

      • x**3*x**4 化简为 x**7
      • x*x*x+x**3 化简为 2*x**3
    • 正项输出提前:

      • -1+x 化简为 x-1
    • 平方输出优化:

      • x**2 化简为 x*x
    • 指数输出优化:

      • x**1 化简为 x
  • 测试数据构造

    • 数据构造目标

      • 针对基本要求构造数据

        • 正确的表达式,覆盖各种格式
        • 有若干项,每个项有若干因子
        • 分类:均衡/项比较多/因子比较多
      • 针对可能的优化构造数据

        • 可以合并同类项的数据
        • *0*1+0**0的数据
    • 数据构造手段

      • 基于递归下降按形式化定义随机生成
      • 递归下降解析通过读入的内容选择接下来的行为,递归下降生成通过随机选择接下来的行为
      • 把输入改成输出,就可以制造符合要求的数据了
      • 以生成项的c++代码为例
        int num=generator()%FACTNUM;
        oneFactor();
        for (int i=0;i<num;i++)
        {
        	someWhite();
        	putchar('*');
        	someWhite();
        	oneFactor();
        }
        
      • 为保证生成的项可合并,可进行如下处理,这样可以保证生成的项指数是tot
        void mergableTerm(int tot)
        {
        	int num=generator()%FACTNUM;
        	int toSub=generator()%100-30;
        	mergableFactor(toSub);
        	tot-=toSub;
        	for (int i=0;i<num;i++)
        	{
        		someWhite();
        		putchar('*');
        		someWhite();
        		if (generator()&1) constFactor();
        		else
        		{
        			toSub=generator()%100-30;
        			mergableFactor(toSub);
        			tot-=toSub;
        		}
        	}
        	someWhite();
        	putchar('*');
        	someWhite();
        	mergableFactor(tot);
        	return;
        }
        
    • 数据构造技巧

      • 使用常量池控制生成的常量,可能包括以下内容
        const char* constPool[]=
        {
        	"0","1","5","6","7",
        	"8","9","11","12","13",
        	"14","15","2147483647",
        	"5223333333","5423333333",
        	"9223372036854775807",
        	"23333333233333332333",
        	"23333333233333334666"
        };
        
      • 0 不解释
      • 一些比较小的数,用于稀释常量池,使生成的数据有大有小
      • 2147483647 刚好没爆int但随便操作一下就大概率爆int
      • 一些爆了int也爆了unsigned int但没爆long的数
      • 9223372036854775807 刚好没爆long但随便操作一下就大概率爆long
      • 一些爆了long也爆了unsigned long的数
      • 山不在高,有仙则名,水不在深,有龙则灵,数不在大,爆long就行

第二次作业总结

  • 类关系图

    • 总类图

      • 面向对象设计与构造第一单元博客作业
      • 比起第一次作业,更加群魔乱舞,眼花缭乱,这主要是因为代码实质上是基本没有耦合的两部分,即高速版和高性能版
      • 面向对象设计与构造第一单元博客作业
      • 这张图可以比较清晰的显示两部分的关系,main会先调用高速版计算得到正确结果存入Timer,再调用高性能版计算更优的结果,如果可以计算完成,输出高性能版结果,否则输出高速版结果
    • 核心部分类图

      • 面向对象设计与构造第一单元博客作业
      • 这张图展示了高速版程序的结构,相比于高性能版,高速版取消了Element而采用LinkedList维护每个节点的子节点,支持基本的优化但无法“天然的”合并同类项,没有了O(n)复杂度的hashCode方法和equals方法,运行时间不会超出限制
      • 面向对象设计与构造第一单元博客作业
      • 这张图展示了高性能版程序的结构,由于第一次就有对三角函数和表达式因子的支持,没有太大的改动,只修改了项 TermNode,表达式因子ExpfctrNode和表达式ExprNode以增加对三角化简的支持
    • 重要类/接口分析

      • Reader

        • 提供若干字符串解析功能,如匹配字符或字符串,读取整数或指数等
        • 读取出的结果为booleanintBigInteger等基本类型,可被两版代码共用
        • 读取解析过程中支持WF判断
      • Timer

        • 高速版程序将结果输入Timer保存,用于绝对避免Tle发生
        • 在程序开始时计时1600ms,计时完成则禁止main函数输出,输出存入的未优化结果后结束程序
        • 若高性能版计算在此之前完成,则会禁止Timer的上述行为,输出经过优化的结果
      • 由于高性能版相比第一次作业变化很小,接下来主要分析高速版的类与接口
      • ElementFast

        • 各种Element的父类,维护一个LinkedList用于存储其可能存在的子节点,定义抽象求导方法
        • 各种Element维护额外的信息,例如RegularFast维护幂函数的指数,ConstFast维护相应的常数
      • AddableFast接口

        • 定义了添加儿子的行为,实现此接口的类LinkedList非空,支持添加子节点,即进行嵌套
      • OptimizableFast接口

        • 定义了优化行为,实现此接口的类需要支持优化,对本单元作业仅ConstFast类不需要实现此接口
  • 基于度量的结构分析

    • 代码规模分析

      • 面向对象设计与构造第一单元博客作业
      • 代码量整体较大,但较均匀的分布在各个类,没有过于臃肿庞大的类
    • 方法复杂度分析

      • 面向对象设计与构造第一单元博客作业
      • 比较大的方法主要是针对表达式因子优化的方法,主要的尴尬之处在于明知Set里只有一个元素,还需要用for循环遍历来取这个元素,这样从表达式到项再从项到表达式因子,三层for循环三层if判断,极大的拉高了方法复杂度
      • 这样问题在其它优化的实现中也时有体现,一种可行的解决方案是单写一个方法,获得只有一个元素的Set中的元素,这样可以比较有效的减少嵌套层数从而降低代码复杂度
    • 类复杂度分析

      • 面向对象设计与构造第一单元博客作业
      • 平均操作复杂度较高的Factor工厂类,因为就一个方法通过多个if创建多种因子,此外ExprNode类和ExpfctrNode平均操作复杂度也较高
      • 加权方法复杂度较高的是ExprNode类、ExpfctrNode类和TermNode类,这些类中用于优化的方法因上述“方法复杂度分析”中的原因,复杂度较高,从而拉高了加权方法复杂度
  • 优缺点分析

    • 优点分析

      • 采用递归下降解析,对求导/化简等操作层次化处理,有良好的可扩展性,支持包括同类项合并/三角化简在内的优化,带有计时熔断机制,保证不会因为化简导致程序运行时间超出限制
    • 问题分析

      • 基于“对于随机数据拆括号大概率不优”考虑,选择不主动拆括号,但未意识到强测并不随机,导致一些针对拆括号的测试点性能分失分较为严重,比较好的策略是分别计算拆括号和不拆括号的结果,输出更优者
  • 优化总结

    • 表达式因子降级:表达式因子只有一个项,这个项只有一个因子,表达式因子降级为这个因子

      • 1+((x)) 化简为 1+(x)
      • 1+(sin(x)) 化简为 1+sin(x)
    • 表达式因子升级:表达式或表达式因子中的某一项只有一个因子,且仅有的这个因子是表达式因子,把这个表达式因子中所有的项提出

      • 1+(x*x+x) 化简为 1+x*x+x
      • 1-(x*x-x) 化简为 1-x*x+x
    • 进一步合并同类项:合并结构相同的任意项

      • sin(x)**2+sin(x)**2 化简为 2*sin(x)**2
      • sin(x)**5*x**2+sin(x)**5*x**2*2+sin(x)**5*x**2*3 化简为 sin(x)**5*x**2*6
    • 拆括号:拆一下括号试试,变短了就保留

      • (x-1)*(x+1)*(x+1) 化简为 x**3+x*x-x-1
      • (x+9)*(x+9)*(x+9) 化简为 (x+9)*(x+9)*(x+9) (因为x**3+27*x*x+243*x+729更长)
    • 简单三角优化:使用1-cos(x)**2替换sin(x)**21-sin(x)**2替换cos(x)**2使表达式最终长度变短

      • sin(x)**3+sin(x)*cos(x)**2 化简为 sin(x)
      • sin(x)**2-cos(x)**2 化简为 1-2*cos(x)**2
    • 因式分解:尝试构思实现未果,据说有大佬做了

      • x*x+699*x+108578 化简为 (x+233)*(x+466)
      • 18*x**4+45*x**3+82*x*x+67*x+40 化简为 (3*x*x+4*x+5)*(6*x*x+7*x+8)
  • 测试数据构造

    • 数据构造目标

      • 针对基本要求构造数据

        • 正确的表达式,覆盖各种格式
        • 有若干项,每个项有若干因子,表达式因子套若干层
        • 分类:项比较多/因子比较多/嵌套比较多
        • 针对四元组做法,大量表达式因子乘法卡Tle
        • 针对标准的递归下降/表达式树做法,大量嵌套卡Tle
      • 针对可能的优化构造数据

        • 可合并的数据测合并
        • 修改可合并的数据为不能合并的数据,测误合并
        • 三角函数群魔乱舞,测正确性/卡Tle
    • 数据构造手段

      • 随机生成,限制嵌套深度,留出宏定义便于分类

        • 可以解决项比较多/因子比较多/嵌套比较多的数据
        • 可以解决一部分三角函数群魔乱舞的数据
        • 在第一次作业的基础上进行迭代,以新增的三角函数为例
          if (generator()&1) printf("sin");
          else printf("cos");
          putWhite(); putchar('(');
          putWhite(); putchar('x');
          putWhite(); putchar(')');
          putPower();
          
        • 选择因子的时候根据层数进行限制
          int chooseFctr(int num)
          {
          	int idx=generator()%(sizeof(fctrPool)/sizeof(*fctrPool));
          	if (fctrPool[idx]==0 || num==0) constFctr();
          	else if (fctrPool[idx]==1) powFctr();
          	else if (fctrPool[idx]==2) triangle();
          	else if (fctrPool[idx]==3) expFctr(num);
          	return fctrPool[idx]==3;
          }
          
        • 另一种可行的构造策略是使总因子数为一给定值,亦可参考
      • 半手工构造,指定“原子”

        • 可以解决拆括号/合并同类项的数据
        • 可以解决少量卡Tle的数据
        • 可以解决一部分三角函数群魔乱舞的数据
        • “原子”形如(指定表达式因子)
          const char* expfctrPool[]=
          {
          	"(x+1)","(x-1)","(x**2+x+1)",
          	"(x**2-x+1)","(-x**2+x+1)","(-x**2-x+1)",
          	"(sin(x)**2+sin(x)+1)","(sin(x)**2-sin(x)+1)",
          	"(-cos(x)**2+cos(x)+1)","(-cos(x)**2-cos(x)+1)"
          };
          
        • “原子”形如(构造可合并连加)
          const char* specialPool1_1[]=
          {
          	"+(x+1)","+(x-1)","-(x+1)","-(x+1)",
          	"+(x+2)","+(x-2)","-(x+2)","-(x+2)",
          	"+2*(x+1)","+2*(x-1)","-2*(x+1)","-2*(x+1)",
          	"+4*(x+2)","+4*(x-2)","-4*(x+2)","-4*(x+2)",
          	"+2*(x+1)","+2*(x-1)","-2*(x+1)","-2*(x+1)",
          	"+4*(x+2)","+4*(x-2)","-4*(x+2)","-4*(x+2)"
          };
          
        • “原子”形如(构造可合并连乘)
          const char* specialPool3_2[]=
          {
          	"*(x**2+x+1)","*(x**2-x+1)","*2*(x**2+x+1)","*2*(x**2-x+1)",
          	"*(-x**2+x+1)","*(-x**2-x+1)","*2*(-x**2+x+1)","*2*(-x**2-x+1)",
          };
          
        • 在递归下降的过程中构造例如
          void expFctr(int num)
          {
          	if (EXPFCTR_FROMPOOL)
          	{
          		int tmp=generator()%(sizeof(expfctrPool)/sizeof(*expfctrPool));
          		printf("%s",expfctrPool[tmp]);
          	}
          	else
          	{
          		putchar('(');
          		getExpr(num-1);
          		putchar(')');
          	}
          	return;
          }
          
        • 直接构造例如
          void printSeveral(const char* s[],int maxn)
          {
          	buffer[0]=0;
          	for (int len=0;true;)
          	{
          		const char *tmp=s[generator()%maxn];
          		if (len+strlen(tmp)>=150) break;
          		strcat(buffer,tmp),len+=strlen(tmp);
          	}
          	puts(buffer+1);
          	return;
          }
          
      • 纯手工构造

        • 卡Tle的数据大部分基本需要手工构造,一些简单的示意如下
          (x+1)*(x+2)*(x+3)*(x-1)*(x-2)*(x-3)
          x*(x*(x*(x*(x*(x+9)+9)+9)+9)+9)
          sin(x)**2*(sin(x)**2*(sin(x)**2*(sin(x)**2+1)-1)+1)
          
        • (x+9)而不是(x+1)是考虑到贪心拆括号后(x+1)会被拆掉,从而数据强度丧失
        • 可以补全剩下的三角函数群魔乱舞的数据:该化简的化简,一些简单的示意如下
          3*cos(x)**2-2*sin(x)**2+x*(4*cos(x)**2-2*sin(x)**2)
          (x*(x*(x+9)+9)+9)*sin(x)**2+(x*(x*(x+9)+9)+9)*cos(x)**2
          
        • 可以补全剩下的三角函数群魔乱舞的数据:不该化简的不化简,一些简单的示意如下
          (sin(x)**2-x+1)*sin(x)**2+(sin(x)**2+x+1)*cos(x)**2
          3*cos(x)**2-3*sin(x)**-2+x*(4*cos(x)**2-3*sin(x)**-2)
          
    • 数据构造技巧

      • 控制生成的概率:简单方便的方式

        if (generator()&1 || generator()&1) // 75%
        if (generator()&1 || generator()&1 || generator()&1 || generator()&1) // 93.75%
        if (generator()&1 && generator()&1) // 25%
        if (generator()&1 && generator()&1 && generator()&1 && generator()&1) // 6.25%
        
      • 控制生成的概率:功能更强的方式,可方便的调整随机结果种类/概率

        const int pool[]={3,2,2,1,1,1};
        std::mt19937 generator(0x23333333);
        
        int randPool()
        {
        	int index=generator()%(sizeof(pool)/sizeof(*pool));
        	return pool[index];
        }
        

第三次作业总结

  • 类关系图

    • 总类图

      • 面向对象设计与构造第一单元博客作业
      • 相比第二次作业,除了新增三角函数化简类之外基本没有太大变化,代码依旧是基本没有耦合的两部分,即高速版和高性能版
      • 面向对象设计与构造第一单元博客作业
      • 同第二次作业,main会先调用高速版计算得到正确结果存入Timer,再调用高性能版计算更优的结果,如果可以计算完成,输出高性能版结果,否则输出高速版结果
    • 核心部分类图

      • 面向对象设计与构造第一单元博客作业
      • 这张图展示了高速版程序的结构,就是第二次作业用的,一点都没改,故不再赘述
      • 面向对象设计与构造第一单元博客作业
      • 这张图展示了高性能版程序的结构,新增了贪心拆括号和更强的三角函数化简,但变化依旧不大,故不再赘述
    • 重要类/接口分析

      • 相比第二次作业,将三角函数优化从TermNode中拆分单成一类TriangleSimplifyer,按照下文“优化总结”中给出的公式依次尝试平方和化简,倍角公式化简,和角公式化简,其它类基本没有变化,故不再赘述
  • 基于度量的结构分析

    • 代码规模分析

      • 面向对象设计与构造第一单元博客作业
      • 代码量整体较大,有两个较为庞大的类,分别是TermNode类和TriangleSimplifyer
      • TermNode类较大的主要原因是增加了功能较为复杂的贪心拆括号方法,可以考虑将若干拆括号的方法,包括表达式因子升降级,贪心拆括号等拆分,单独成一类,这样可以使ExprNodeTermNodeExpfctrNode等类的规模有较大程度的下降
      • TriangleSimplifyer类较大的主要原因是有较多的三角函数化简公式,可以考虑对TriangleSimplifyer类按照和角/倍角等进行进一步的拆分
    • 方法复杂度分析

      • 面向对象设计与构造第一单元博客作业
      • 除了第二次作业中提到的方法外,三角函数化简方法optmTriangleMerge和拆括号方法dfsFk**复杂度较高
      • 对于前者,主要原因在于用于化简的三角公式较多,如“代码规模分析”中所述,可对这些三角公式进一步拆分,层次化化简以降低复杂度
      • 对于后者,原因依旧是从表达式到项再从项到因子需要多层for循环if判断的问题
    • 类复杂度分析

      • 面向对象设计与构造第一单元博客作业
      • 相比于第二次作业变化不大,依旧是Factor工厂类一骑绝尘,随后是若干有复杂化简行为的类,在此不再赘述
      • 为支持和差角优化,在TriangleNode类中新增了相应方法,没想到把平均操作复杂度拉低了
  • 优缺点分析

    • 优点分析

      • 采用递归下降解析,对求导/化简/判断WF等操作层次化处理,有良好的可扩展性,支持包括同类项合并/三角化简/贪心拆括号在内的优化,带有计时熔断机制,保证不会因为化简导致程序运行时间超出限制
    • 问题分析

      • 优化复杂度较高,但在本地测试的时候因计算机性能较好和单进程跑点较快没重视这一问题,认为熔断是小概率事件,熔断时的输出基本没有任何优化,而服务器并行跑点性能较差,强测时熔断机制被多次触发,性能分受到了较大影响
  • 优化总结

    • 进一步三角函数优化:使用诱导公式化简(第三次作业截止了才意识到,肠子都悔青了)

      • sin(x)+sin((-x)) 化简为 0
      • cos(x)+cos((-x)) 化简为 2*cos(x)
    • 深度合并同类项:合并递归结构相同的任意项

      • sin(cos(x))**2+sin(cos(x))**2 化简为 2*sin(cos(x))**2
      • sin(cos(cos(x)))**5+sin(cos(cos(x)))**5+sin(cos(cos(x)))**5 化简为 3*sin(cos(cos(x)))**5
    • 深度三角函数优化:类似简单三角优化,使用和角公式/倍角公式化简

      • 正余弦倍角公式的变形:
        • 2*cos(x)**2=1+cos((2*x))
        • 2*sin(x)**2=1-cos((2*x))
        • 2*sin(x)*cos(x)=0+sin((2*x))
      • 正余弦和差角公式的变形:
        • sin(x)*cos(y)=sin((x-y))+cos(x)*sin(y)
        • sin(x)*cos(y)=sin((x+y))-cos(x)*sin(y)
        • cos(x)*cos(y)=cos((x-y))-sin(x)*sin(y)
        • cos(x)*cos(y)=cos((x+y))+sin(x)*sin(y)
        • sin(x)*sin(y)=cos((x-y))-cos(x)*sin(y)
        • sin(x)*sin(y)=-cos((x+y))+cos(x)*sin(y)
      • 具体化简实现如下:
        • 对每个项t,搜索符合上述变形等号左边的三角因子f
        • t中被找到的三角因子f用等号右边的东西替换,如果有k种变形可以达到,会得到一个2k个项的队列q
        • 从原表达式中移除原来的项t,再从队列q中出队两个项加入表达式
        • 对表达式进行一次包括合并同类项的化简,按照toString后的len存入优先队列pq
        • 如此循环,直到队列q为空,比较优先队列pqtop与当前项ttoString后的len
        • 如果toptoString后的len更小就用top替换当前项t,重复上述过程,否则跳出循环
    • 优化的一些问题分析

      • 很多优化的本质是贪心,以三角化简为例,8*sin(x)**3*cos(x)**3这种形式是无法化简的,因为4*sin(x)**2*cos(x)**2*sin((2*x))更长,对于这一点,或可以考虑用退火处理
      • 倍角是无限的,上述优化无法处理n倍角化简,对于这一点,或可以考虑用欧拉公式处理
  • 测试数据构造

    • 数据构造目标

      • 针对基本要求构造数据

        • 错误的表达式(虽然互测不能卡人WF,但为了自测还是要造)
        • 正确的表达式,覆盖各种格式
        • 有若干项,每个项有若干因子,表达式因子套若干层,三角函数套若干层
        • 分类:项比较多/因子比较多/嵌套比较多
        • 多层嵌套数据卡Tle
      • 针对可能的优化构造数据

        • 可合并的数据测合并
        • 修改可合并的数据为不能合并的数据,测误合并
        • 三角函数群魔乱舞,测正确性/卡Tle
    • 数据构造手段

      • 正确数据与第二次作业差别很小,基于第二次作业的数据生成器简单修改即可,在此不再赘述
      • 需要额外补充一些多层三角嵌套的数据,一些简单的示意如下
        cos((1+sin((1+cos((1+sin((1+x))))))))
        cos(sin(cos(sin(x)**50)**50)**50)**50
        sin((x*(x*(x+9)+9)))**2+cos((x*(x*(x+9)+9)))**2
        
      • 如果实现了三角优化,需要额外补充一些针对三角优化的数据,一些简单的示意如下
        sin(cos(x))*cos(sin(x))+cos(cos(x))*sin(sin(x))
        sin(cos(x))*cos(sin(x))-cos(cos(x))*sin(sin(x))
        -cos(cos(x))*cos(sin(x))+sin(cos(x))*sin(sin(x))
        -cos(cos(x))*cos(sin(x))-sin(cos(x))*sin(sin(x))
        
      • 错误数据:覆盖各种原子问题,在此基础上组合
      • 原子错误类型(部分示例)
        x**-51 //坑没有abs()的
        x**98765432109876543210 //坑用long存指数的
        cin(x)**1 //坑用首字母判断三角函数的
        sin(x**2+x(+1)**0 //不同位置的括号不匹配
        sin(x**2+(x+x+1)**0 //不同位置的括号不匹配
        sin((x**2+x+1)**0 //不同位置的括号不匹配
        
      • 组合错误:递归下降+错误向量
        wrongVector[generator()%5]=1;
        int idx=0; getExpr(buffer,idx,2); buffer[idx]=0;
        int len=strlen(buffer);
        if (30<=len && len<=500) puts(buffer);
        
      • 错误向量使用一次后清除,保证复杂数据错误单一
        void getConst()
        {
        	if (wrongVector[CONST_INDEX])
        	{
        		wrongVector[CONST_INDEX]=false;
        		int num=generator()%(sizeof(wrongConst)/sizeof(*wrongConst)-1)+1;
        		printf("%s",wrongConst[num]); // it is wrong
        	}
        	else
        	{
        		putSign(buffer,idx);
        		printf("%s",wrongConst[0]); // it is right
        	}
        	return;
        }
        
    • 数据构造技巧

      • 关于非法字符的选择
      • \s匹配[ \f\n\r\t\v],其中空格和\t合法,\n\r换行,因此可以考虑注入\f\v
        void insChar(char buffer[],int idx)
        {
        	int top=0,len=0;
        	for (int i=0;i<buffer[i];i++) if (buffer[i]==32 || buffer[i]=='\t') stk[top++]=i;
        	if (top==0)
        	{
        		buffer[idx++]=generator()&1?'\f':'\v';
        		buffer[idx]=0; return;
        	}
        	for (int i=0;i<top;i++) std::swap(stk[i],stk[generator()%top]);
        	for (int i=0;i<idx;i++)
        	{
        		temp[len++]=buffer[i];
        		if (i==stk[0]) temp[len++]=generator()&1?'\f':'\v';
        		if (top>1 && i==stk[1] && generator()&1 && generator()&1 && generator()&1)
        			temp[len++]=generator()&1?'\f':'\v';
        	}
        	temp[len]=0,strcpy(buffer,temp);
        	idx=strlen(temp);
        	return;
        }
        

程序Bug分析

  • 本地测试方式

    • 本地测试以黑箱测试为主

    • 关于自动测试搭建

      • 自动测试程序使用c++实现
      • 结果格式验证程序使用java实现,手写递归下降解析
      • 结果正确性验证程序使用python实现,利用了sympy
      • 自动测试工具在第一次作业时基本完成,之后进行的修改较少
    • 关于测试数据构造

      • 在构造数据的时候考虑不同的侧重点
      • 在构造数据的时候考虑自己程序可能的易错点
      • 在构造数据的时候尝试预判他人程序可能的易错点
    • 对于测试的一些补充

      • sympy提供的equals比较玄学,有的时候(尤其三角函数比较多的时候)会卡死或返回None,但更多的时候可以很快的得到结果,手写取点验证不会卡死但平均速度比较慢
      • 一种可以考虑的方式是先开一个线程用equals尝试判断,如果5秒没有获得结果就干掉这个线程改为取点判断
      • 可以考虑将评判信息以md的形式格式化输出,方便进一步的查看和分析
  • 本人程序Bug

    • 本地测试还算充分,在三次强测和互测中均没有被发现Bug
  • 他人程序Bug

    • 第一次作业发现房间中三人五个Bug,主要问题在于化简出错,没有保留原始测试结果
    • 第二次作业发现房间中四人四个Bug,原始测试结果和Bug分析如下
      1650 TestPoints
      1	362 False	0 T	569616 Len	// (1)
      2	020 False	0 T	463667 Len	// (2)
      3	000 False	0 T	462879 Len
      4	000 False	0 T	707560 Len
      5	892 False	0 T	264206 Len	// (3)
      6	000 False	0 T	312061 Len
      7	172 False	0 T	211175 Len	// (4)
      8	000 False	0 T	433039 Len
      
      (1) : x*(-(0)) +1 but not 0
      (2) : (0+0)*x 1 but not 0
      (3) : cos(x ) RE but not -sin(x)
      (4) : (x-1)*(x-1)*(x-2)*(x-2) 4*(x-1)*(x-2)*(x-2) but not 3*(x-2)*(x-1)*(x-1)+(x-1)*(x-1)*(x-1)
      
    • 第三次作业发现房间中六人九个Bug,原始测试结果和Bug分析如下
      2980 Normal TestPoints
      1	0000 False	0 T	1173331 Len
      2	0000 False	0 T	0715569 Len
      3	0002 False	0 T	1133987 Len	// (1)
      4	0162 False	0 T	0633269 Len	// (2)
      5	0000 False	0 T	0813824 Len
      6	0026 False	0 T	1066099 Len	// (3)
      7	0856 False	0 T	1238625 Len	// (4) (5) (6)
      8	0010 False	0 T	0653502 Len	// (7)
      
      (1) : cos((0))*x 0 but not 1
      (2) : x*(x**4+x)*(x**4+x) x*(1+x**3)**2+(8*x**3+2)*x*x*(1+x**3) but not (x**4+x)*(x**4+x)+8*x**8+10*x**5+2*x*x
      (3) : sin(1)*x*sin(1) WF but not sin(1)**2
      (4) : cos((x) ) WF but not -sin(x)
      (5) : cos(x)*cos((x+1)) -2*sin(x)*(1)*cos((1+x)) but not -sin((2*x+1))
      (6) : sin(x**2)+sin(x**3) but not 2*cos(x**2)*x+3*cos(x**3)*x*x
      (7) : cos(x)**2 2*cos(x)*-sin(x) but not -sin((2*x))
      
      3000 Optimize TestPoints
      1	0000 False	0 T	1579011 Len
      2	0000 False	0 T	0991504 Len
      3	0000 False	0 T	1760292 Len
      4	0036 False	0 T	0936714 Len	// (1)
      5	0049 False	0 T	1133117 Len	// (2)
      6	0001 False	0 T	1343965 Len	// (3)
      7	1757 False	0 T	1256134 Len	// (4) (5) (6)
      8	0000 False	0 T	0950765 Len
      
      (1) : x*(x**4+x)*(x**4+x) x*(1+x**3)**2+(8*x**3+2)*x*x*(1+x**3) but not (x**4+x)*(x**4+x)+8*x**8+10*x**5+2*x*x
      (2) : x*cos(x**2)**2+cos(x**2)*sin(x**2)**2 cos(x**2)**2-2*x*sin(x**2)**3 but not cos(x**2)**2-2*x*x*sin((2*x*x))-2*sin(x**2)**3*x+2*cos(x**2)*x*sin((2*x*x))
      (3) : (1+1)*(x+x) WF but not 4
      (4) : cos((x) ) WF but not -sin(x)
      (5) : cos(x)*cos((x+1)) -2*sin(x)*(1)*cos((1+x)) but not -sin((2*x+1))
      (6) : sin(x**2)+sin(x**3) but not 2*cos(x**2)*x+3*cos(x**3)*x*x
      
    • 第二次作业存在一份代码,针对第二次作业准备的数据没有将其Hack出来,使用针对第一次作业准备的数据可以Hack出来,应该是因为重构的原因
    • 由于精力原因,未结合被测程序的代码结构设计测试用例,从互测房间最终结果来看,存在一些人只被我刀了,不存在没被我刀但被别人刀了的人,说明这种测试方式是有一定有效性的
    • 第三次作业不让出WF的数据,不然可以Hack的更爽

重构经历总结

  • 在本单元开始前阅读了一些博客园上的往届博客,对可能的内容心里有数
  • 在本单元之初充分考虑了扩展性,三次作业没有重构
  • 对于除法/表达式因子乘方/反三角函数求导等均可十分方便的增加支持
  • 对于求偏导等更高的要求亦不需要进行较大修改

心得体会

  • 本次作业在以下方面均有相当的挑战,需要投入较多精力去体会和感悟

    • 抽象能力和面向对象思想
    • 编写维护有一定规模项目的能力
    • 可扩展意识与迭代开发能力
    • 构造尽可能全面测试的能力
    • 程序运行时间,程序性能和个人精力三方面的权衡
  • 本次作业对我能力的提升是显著的,除上述内容外,还包括但不限于

    • 本地对拍的过程中python能力的提高
    • java部分机制、容器特性与命令行操作的的熟练度
    • 了解了递归下降,简单使用递归下降解析形式化语言
  • 一些补充

    • 建议提高强测数据强度,比如第三次作业的WF,强测不够狠互测不让测,漏网之鱼怕是不少(小声)
    • 建议提高强测数据区分度,比如比较丰富的包括纯三角优化/纯拆括号/纯提公因式/在此基础上的组合的测试点,让各种优化的付出-回报比比较一致(小声)

相关文章:

  • 2021-05-20
  • 2022-01-23
  • 2021-11-18
  • 2022-01-16
  • 2021-11-28
  • 2021-10-13
  • 2021-12-02
  • 2021-11-12
猜你喜欢
  • 2022-12-23
  • 2021-07-05
  • 2021-09-06
  • 2021-07-16
  • 2021-09-04
  • 2021-05-24
  • 2021-12-08
相关资源
相似解决方案