本单元的三次作业均为表达式求导。所需处理的表达式从可略写的幂函数项和常数项简单相加,到首因子可略写的幂函数因子、常数因子与三角函数因子相乘所得的项相加,再到首因子可略写的幂函数因子、常数因子、三角函数因子、复合因子、表达式因子相乘所得的项相加,难度递增。
其中各因子完整形式如下:
- 常数因子
a
用正则表达式可表示为[+-]?\\d+
- 幂函数因子
sin(x)^a
和cos(x)^a
- 复合因子是在三角函数因子中复合任意一个因子
- 表达式因子是以括号包裹的表达式
可以看出第三次作业中有明显的递归嵌套结构,这一与前两次作业截然不同的特征也正是我当时所面临的一个难题。当时认为我的第二次作业的扩展性不足以支持第三次作业,但事后冷静分析(手动表情包)之后发现其实是自己太囿于指导书,失去了自己的思考。
本文接下来将从3个方面,即 基于度量分析程序结构、分析程序bug、分析发现他人bug所采用的的策略 对三次作业分别进行分析。俗话说当局者迷,对于第一二次作业玄妙地在构造边界数据、对拍和互测中都没有被刀,本当局者现在也无能为力,所以分析程序bug部分可能省略。
第一次作业
基于度量分析程序结构
![](https://img2018.cnblogs.com/blog/1615671/201903/1615671-20190326231623942-247185360.png) 长条状的类图看起来非常罪恶地面向过程。
其实主要是因为正则表达式不想重复定义,所以只在Term类中定义了static的正则,导致成员变量较多,同时get方法较多
(不然check style一言不合就报错)。实际上Ploy类中的成员变量只有ArrayList<Term> pp
, 用于存储表达式中的每一项;Term类中的成员变量只有系数coeff
和指数index
。 大致思路如下:
- WRONG FORMAT判断
对各个项的合法形式定义正则表达式,对读入的String进行逐项匹配。匹配到的两项中间要全部是空字符才合法。特别地,需要对字符串开头和结尾的空字符进行判断。
在细节上我默认项首有符号,所以在匹配首项的时候,先默认有符号进行匹配,若匹配不成功则尝试添加正号进行匹配,若还不成功,则判定为WRONG FORMAT。
- 求导
若输入字符串格式合法,则在进行WRONG FORMAT判断时已经将其处理为各项之和的形式存储在Ploy类的
ArrayList<Term> pp
中。求导操作只需要遍历该ArrayList,逐项求导。其中
(a*x^b)' = (a*b)*x^(b-1)
。- 输出及长度优化
输出同样遍历ArrayList,逐项输出并用加号相连。
为使输出长度最短,采取合并同类项和正项提前两种策略。
根据以上思路,创建了三个类。其中:
Enter类
控制类,只有一个main方法;
Poly类
表达式类,其成员变量
pp
中存储各相加的项,包含求导方法derivation、输出表达式方法printPoly和合并同类项方法merge;Term类
项类,按
coeff*x^index
存储各项,包含,,,似乎只包含输出项方法printTerm,居然连求导方法都没有?!果然年轻的时候写的东西都是黑历史。现在的,拥有一颗正常的脑子的我认为在Term中定义求导方法,返回一个求导后的Term,然后对Ploy的ArrayList中的每一个Term调用这个求导方法真香。
methods metrics
class metrics
分析他人bug所采用的策略
快乐狼人!第一次作业出于神奇的原因,被分在了神奇的组,于是只上了机枪。
以下是直播刀人:
在构造测试数据的时候一定要狠,不把自己的程序当亲生的,别人的程序嘛,嘿嘿嘿。狠的具体含义就是瞅准边界值可劲测,比如上图中的空串测试、大整数项测试等。
通过上图也可以看出我的测试策略是逐项功能测试,比如测试省略指数和省略系数的数据就十分具有针对性。这样做的优势在于对单项错误较好排查,但是对于玄妙的混合式错误就比较束手无策,还需要大量随机数据去刚。
然后是应对互测制度的一个小tip,就是在刀人的时候在每个数据上标注被它de出bug的人,这样可以优先提交包含人数多~~(你不顺眼的人)~~的数据;然后分两块分别记录已提交数据和未提交数据,这样再也不用为看不到自己交了哪些数据头秃了。**具体图示参见第三次作业分析。**
第二次作业
基于度量分析程序结构
不是长条了,看起来似乎有些美。虽然还是存在static正则占篇幅的问题。对于这个问题今天无意间看到同学的代码,发现她是在一个接口中定义了这些成员变量,然后需要用到的位置继承这个接口就好,妙啊!
这次作业增加了三角函数因子,同时允许因子相乘作为项。在这里其就被 **“第二次作业架构不好第三次作业一定完蛋”** 的言论恐吓着,于是苦思冥想写了个很彻底的构架。具体彻底表现在,对于一个表达式,先按同第一次的方法将其拆成项,然后对于每一项,直接考虑其化简之后的结果。这个结果一定是`coeff*x^index*sin(x)^sinIndex*cos(x)*cosIndex` 的形式。而单个三角函数求导时候一定是sin和cos相乘的形式,于是将上式分割成 `coeff*x^index` 和 `coeff*sin(x)^sinIndex*cos(x)*cosIndex` 分别建立**PowerFunc类**和**TriFunc类**。
每个步骤大致思路如下:
> - **WRONG FORMAT判断**
>
> 同第一次作业。
>
> 对各个因子的合法形式定义正则表达式,多个合法因子相乘组成合法的项。此处需注意首因子省略的问题。然后对读入的String进行逐项匹配匹配到的两项中间要全部是空字符才合法。特别地,需要对字符串开头和结尾的空字符进行判断。
>
> - **求导**
>
> 若输入字符串格式合法,则在进行WRONG FORMAT判断时已经将其处理为各项之和的形式存储在Exp类的 `ArrayList<Term> exp` 中。同时已经进行了同类因子合并。
>
> 求导操作只需遍历该ArrayList进行逐项求导。对于每一个Term分别调用三种因子的求导方法然后相乘合并即可。
>
> - **输出及长度优化**
>
> 输出首先遍历ArrayList逐项输出再相加。对于每一项再进行因子输出再相乘。
>
> 为使输出长度最短,在合并同类项和正项提前基础上增加首因子省略和三角函数化简两种优化。
>
> 三角函数化简采用贪心,即只化简`a*sin(x)^m*cos(x)^(n+2)+b*sin(x)^(m+2)*cos(x)^n`,因为只有这种方法能保证表达式一定变短。
根据以上思路,创建了六个类。其中:
- **Enter类**
控制类,只有一个main方法;
- **Exp类**
表达式类,其成员变量 `exp` 中存储各相加的项,包含**求导方法diff**、**输出表达式式方法print**和**合并同类项方法naiveMerge**和**三角函数化简方法addMerge**;
- **Term类**
项类,成员变量有幂函数因子 `powerFunc` 和三角函数因子 `triFunc` ,包含**求导方法diff**、**项输出方法print**和**因子合并方法mergeTerm**;
- **PowerFun类**
幂函数类,成员变量有系数 `coeff` 和指数 `index` ,包括**求导方法diff**、**幂函数输出方法printPower**和**幂因子合并方法megerPower**;
- **TriFunc类**
三角函数类,成员变量有系数 `coeff` 、sin因子指数 `sinIndex` 和cos因子指数 `cosIndex` ,包括**两个求导方法diffSin和diffCos**、**两个三角函数输出方法printSin和printCos**以及**两个三角因子合并方法megerSin和mergeCos**;
- **FuncKey类**
用于合并同类项的自定义key。为了使用HashMap,同时重写其了equals方法和hashCode方法。
现在想想sin和cos的方法完全不同~~(我打字好累呀)~~,TirFunc应该分成单独的两个类才对。
**methods metrics**
**class metrics**
分析他人bug所采用的策略
这次主要学习了一个对拍器的写法,然后写了一个丑陋的时不时会罢工的评测机。
**make.py**
**check.py**
pai
首先在make.py文件中中利用xeger包将自己的大正则生成合法的表达式,然后判断是否符合从小伙伴处骗来的正则。若符合,则是一个合法的表达式,可以作为输入。然后将该表达式修改一下格式输入到待测试程序中。接着将计算结果和原始表达式同时输入到check.py丑陋评测机中,按照指导书上的方法随机取点并计算数值。若在误差超出精度范围则将该样例附在BUGS后,然后,天亮了请睁眼,昨晚不可能是平安夜。
第三次作业
基于度量分析程序结构
top-level
operator package
factor package
这次作业是大乱炖,在第二次作业的基础上又增加了嵌套因子及表达式因子,看上去过于头秃以至于我们当天先肥宅了一波冷静一下。
指导书上提供了这样的思路:
- 这次作业,看上去似乎很难,其实找对了方法后并不难。关键思想是,化整为零,可以这样考虑
- 对于每一种函数(常数、幂函数、三角函数),建立类
- 对于每一种函数组合规则(乘法、加减法、嵌套),建立类
- 对于上述的两种类,均实现一个求导接口
- 其中,第一种类,做法显而易见
- 其中,第二种类,做法一样显而易见
- 通过上述两种类及其求导接口,把整个表达式构建为树结构,也就是讨论区大佬们说的,链式求导
- 对于秒掉正确性部分后,想要最大限度优化性能的
大佬同学,一样可以将上述的化整为零思想作为可行思路之一,设计算法。
于是我就乖巧地这样做了,同时尝试了package打包,然而发现并不太理想。根据以上思路,创建了operator package,其中包含Add、Multiply和Compound三个运算法则类;factor package,其中包含Number、Power、Sin和Cos四个基本因子类。
所有类均继承Operator接口:
public interface Operator {
public String print(); // 输出方法
public Operator derivation(); // 求导方法
public Operator clone(); // 克隆方法
// 以下方法在脑子正常的构架中并不必要
public BigInteger getCoeff(); // 获取系数方法
public BigInteger getIndex(); // 获取指数方法
public void setCoeff(BigInteger coeff); // 置系数方法
public void setIndex(BigInteger index); // 置指数方法
}
methods metrics
class metrics
分析程序bug
我的bugs:
// TLE
sin((x+cos((sin((x^2-cos(x)^3+sin(x)^4))+cos((sin(x)-x))))))sin((x+cos((x^2+sin((x^3+cos((x^4+sin(x)))))))))+x*(x)*x*(x)
((((((((((((((((((((-4*cos(x)^3+3*cos(x)))))))))))))))))))))
((((((sin((((x))))^2)))+(((cos((((x))))^2))))))
(1+x*(1+x*(1+x*(1+x*(1+x*(1+x*(1+x*(1+x*(1+x*((x)))))))))))
(x+(x+(x+(x+(x+(x+(x+(x+(x+(x+(x+(x+(x+cos(x)^2)))))))))))))
sin((sin((-x^2))))++sin((((((((x*sin((2*x+1))))))))))
// space
sin(((x-+ x)*x ^ +1 + + +1))
++ x// negate
+-(x^2) 这次作业让我明白一件事不写优化一时爽,一直不写一直爽。做一个没有感情的商人,先写正确性,再优化。
首先由于妄图优化掉加法中的加0和乘法中的乘1而疯狂调用print方法忘记考虑复杂度导致TLE,痛。其次在不知道还有南湖捞人这种操作的时候爆肝,正则乱写,笔误遍地,在小于等于的关系中少写一个等号、在幂函数项中少加一个space。最后在知道了南湖捞人这种计划之后怒弃代码,没有进行任何额外的正确性测试是这次爆炸的终极原因。
总之,这次的bug一部分归咎于代码的架构:应该将正则表达式常量进行统一处理,同时对于不合理的架构要当机立断地重构;一部分归咎于心态问题:在有充足的剩余时间的情况下还不进行充分的测试是一种愚蠢的行为,是在别人对你不负责的前提对自己的不负责。
分析他人bug所采用的的策略
**构造部分测试数据备份**
上图所示即互测的数据备份方式。
经过这几次作业,发现测试数据还是应该既有自己精心构造的边界测试,这里的边界不仅指数据范围的边界,也指性能的边界;又要有大量随机数据进行验证。