OO第三单元总结
OO第三单元总结
1. 测试数据准备
1.1 如何利用JML规则
-
在进行自测的过程中,JML规格可以帮助我们快速找出容易超时的函数。因为作业中给出的JML限制往往是采用循环遍历+函数嵌套调用的说明逻辑, 可以很容易的找出
O(n^2)
复杂度及以上的函数.- 例如:第二次作业的中的
getValueSum()
函数. 很显然的看到了两个循环遍历:\sum int i ...
和\sum int j ...
所以,自测的时候应该专门构造含有大量qgvs
指令的数据. 即:先构建一个组,向这一个组内加入大量的Person
,然后紧跟着大量的qgvs
,检测是否超时.
- 例如:第二次作业的中的
-
重视JML规格中的反常识设定. 生成对应数据来检查实现的逻辑.反常识设定也包括对于一些及特殊情况的规定.
- 反常识设定例如:
addToGroup()
指令最多只能加入1111
个Person
,不注意到这点一定会被hack. 自测的时候也要加以用心.- 比如,虽然我在实现这个函数的时候注意到了1111这个限制条件,但最开始把"组内人数小于1111"误写成了"当前存在的组数小于1111",这个bug是在自测中才发现的.
- 特殊情况规定,例如:
Psrson
中的isLinked()
方法规定了与自己是默认链接的.queryValue()
方法,规定对于不在acquaintance[]
中的人结果返回0. 这两条行为直接影响着"查询最小生成数权值和"这个操作的逻辑. 因此,可以构造数据,查询单个节点最小生成数的权值, 检查实现逻辑是否正确.
- 反常识设定例如:
-
注意JML的精度问题. 这是我觉的第三单元作业最让我难受的一个点,明明有更高精度且更简洁的写法,却因为了精度问题导致写法变得更加复杂.这种问题如果懒得动脑子,只能构造大量数据进行测试.
-
例如:计算方差时,最好的方法应该是平方的平均减去平均的平方,但为了精度问题,只能采用更麻烦的写法.
-
例如:发送群发红包时,JML中是让发出者的红包直接变为最终数额.但其实还有一种更简单的写法,就是先让发出者的
money
减少红包的钱数, 然后再让组内的所有人(包括发出者)都获得对应份额的红包. 如果懒得从数学角度分析精度,那就要构造一些数据验证精度是否等同.-
\old(getMessage(id)).getPerson1().getMoney() == \old(getMessage(id).getPerson1().getMoney()) - i *(\old(getMessage(id)).getGroup().getSize() - 1)
-
-
1.2 如何确定测试的重点
在三次作业的迭代过程中, 实现的方法越来越多, 因此不可能每一次都对全部指令进行覆盖性测试(即便是自动化测试,随机生成数据也可能强度不够). 因此, 我们要想办法确定测试的重点方法.而确定测试重点主要有三个方法.
向同学和学长问易错点. 这个其实是最简单也最有效的方式- 根据JML找测试重点. JML循环层数多的,要构造大量数据重点测试时间. JML规格特别冗长的, 应该多构造不同的边缘数据进行多人对拍测试.
- 对于之前作业中实现的函数,且当次作业没有变化的,就不需要多加测试,随机生成即可.
1.3 获取测试数据的方式
对于自动化生成的数据, 方法数量一旦变多,对于某些特定指令的测试强度就会下降. 因此, 在写随机生成数据脚本的时候, 应该为每一类指令单独设置出现频率. 比如, 涉及group的指令,涉及Message的指令等设为多个类, 统一调节某些类别指令的出现频率,从而达到强测的目的.
2. 图模型构建和维护的策略
2.1 基本数据结构的维护
-
在这三次作业中,维护的重点结构就是路径压缩后的并查集. 并查集同时也可以便于我们找到某个连通分支内的所有节点.
private final HashMap<Integer, ArrayList<Integer>> set = new HashMap<>();
-
基于这个并查集的父节点, 还可以维护联通分支的父节点到该连通分支内部所有边的映射. 这样在求最小生成树权值的时候,可以用O(1)的复杂度找到该连通分支内涉及道德所有边. 这样在使用
kruskal
的时候可以更加方便,减少遍历的次数private final HashMap<Integer, ArrayList<Edge>> block2Edges = new HashMap<>();
-
缓存与脏位. 对于所有会被查询到的数据,都可以进行缓存,并设置脏位维护. 比如,Person会被查询所在最小生成树的权值,那么如果他所在的连通分支没有加入边,脏位会始终为false, 这样下次查询的时候,会直接返回上一次缓存的
leastConnection
的值.#class MypERSON private int leastConnection; private boolean lcDirty;
2.2 图的构建策略
-
加边/加人的缓冲
-
提升程序的局部性,不要每次加人或加边都去更新并查集等数据结构。
-
不查询就不更新图
-
-
尽可能减少查找的复杂度
- 大量的使用
HashMap
构建映射,避免使用ArrayList
的查找. 因为一旦查找的复杂度达到了O(n)
其在被调用函数中的复杂度很容易达到O(n^2)
- 利用堆优化,便于在使用迪杰斯特拉算法时,查找权值最小的边
- 大量的使用
2.3 图的维护策略:非必要不计算,应缓存尽缓存,应更新尽更新
- 非必要不计算:指如果一个值被计算过,且这次查询时,该值并没有发生改变,那么就不要在进行重复的计算.
- 应缓存尽缓存:作业的测试数据中,必然含有大量的查询指令, 把之前计算过的值缓存起来,可以便于查询,减小时间消耗.
- 应更新尽更新:比如,在计算最小生成树权值时,连通分支内部每一个节点的
leastConnection
值都应该被更新为最新计算的结果. 因为同一个联通分支内,最小生成树的权值只有一个.
3. 性能问题与修复情况
- 因为提前听取了建议,知道本单元不好好写会TLE,因此设计之初就十分重视性能.因此本单元三次作业中均没有出现BUG.
- 唯一的性能问题是:在第三次作业查询最短路径的指令中, 只缓存并更新了A->B的路径值, 而没有缓存B->A的路径权值, 导致程序可能变慢. 而且最短路径需要缓存的数据过多,在对拍小伙伴的电脑上出现过不可复现的堆溢出BUG(当然此时的数据规模其实远大于互测和公测的限制), 个人感觉这里的优化容易适得其反.
- 解决办法:取消了对最短路径的数据缓存.
4. 模型拓展
-
对于不同Person的拓展总体来说类似于Message的拓展. 大概需要实现下列类.
-
顾客会偏爱某种
Type
的产品,每种不同Type
的产品对应许多具体的Product
,其具有独立的idclass Product{ private int id; // 每一个产品的唯一标识 private int price; // 产品的价格 private int type;// 表明是何种类型的产品 private Producer producer;// 产品的生产者 private int status; //产品的状态 } class Person{ private int money; } class Producer extends Person{ private Product[] produced; public Product makeProduct(){} } class Advertiser extends Person{ public void sendAdvertisement(){} } class Customer extends Person{ private int typePreference; //偏爱某种类型的产品,受到对应的广告后会购买 public void recvAdvertisement(){} } class purchaseMsg extends Message{ } class advertiseMsg extends Message{ } class produceMsg extends Message{ }
-
查询某种类型产品的销售额:位于Network接口
/*@ public normal_behavior @ requires (\exists int i; 0 <= i && i < products.length; groups[i].getType() == productType); @ ensures \result == (\sum int i; 0 <= i && i < products.length && @ products[i].getType == productType && products[i].getStatus == Status.SOLD; @ products[i].getPrice()); @ also @ public exceptional_behavior @ signals (ProductTypeNotFoundException e) !(\exists int i; 0 <= i && i < products.length; @ products[i].getType == productType); */ public /*@ pure @*/ int queryProductSales(int productType) throws ProductTypeNotFoundException;
-
生产某个产品: 位于Network接口
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == personId &&
@ people[i] instanceof Producer);
@ requires !(\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
@ assignable products;
@ ensures newProduct.getType()==type && newProduct.getId()==id && newProduct.getProducerId==personId
@ ensures products.length == \old(products.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(products.length);
@ (\exists int j; 0 <= j && j < products.length; products[j].equals(\old(products[i]))));
@ ensures (\exists int i; 0 <= i && i < products.length; products[i].equals(newProduct));
@ also
@ public exceptional_behavior
@ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length;
@ products[i].getId() == id)
@ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == personId && people[i].getType == personType.PRODUCER);
*/
public void makeProduct(int type, int id, int personId) throws EqualProductIdException, PersonIdNotFoundException;
- 查询销售路径:
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < products.length; groups[i].getId() == productId);
@ ensures (Person[] array; array.length == 3;
@ array[0].equals(getPerson(getProduct(productId.getProducer()))) &&
@ array[1] instancef Advertiser && array[2] instanceof Customer &&
@ (\forall int i; 0 <= i && i < array.length - 1; array[i].isLinked(array[i + 1]) == true)));
@ ensures \results == array;
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !(\exists int i; 0 <= i && i < products.length;
@ products[i].getId == productId);
*/
public /*@ pure @*/ int querySalePath(int productId) throws ProductIdNotFoundException;
5. 感受
第三单元应该是最简单的一个单元了. 难度比第一第二单元低, 题目也不想第四单元谜语人. JML清晰明了, 只需要做好优化就能轻松过关. 唯一有点难的就是实验写JML, 这玩意写的真的眼晕, 而且其实语法之类的也不是绝对规范, 可以有不同表述.