面向对象设计与构造第三单元博客作业
JML规格的前置条件本就保证不重不漏,所以顺着JML规格的思路就可以构造覆盖性较好的数据,测试时赋予正常和异常以不同的权重,以正常运行为主,异常只要各分支覆盖到即可。
对拍的话由于本单元不像前两个单元,大家的输出结果在正确的情况下一定是相同的,找同学对拍而不是去自己写程序是最简单有效的方式了。
由于有了规格的限制,大家的结构与实现基本类似,大大降低了阅读代码的难度,所以这一单元的互测直接阅读代码不失为一种很好的方法。
二、架构设计与性能问题
基于规格的代码实现,只要正确理解规格,功能正确性上基本不会出问题,且整体架构跳脱不开课程组给的框架,至于说图模型构建和维护策略,则主要都是出于性能考虑,二者密不可分,所以本单元的主要问题在于性能,需要保证每一条指令的时间复杂度低于O(n^2)才能通过强测和互测。要实现这一目的主要有三种方法,选择合适的容器,维护变量保存计算结果,算法优化。
选择合适的容器
除第一次作业错误理解规格的实现,完全按照规格使用ArrayList外,后面的实现涉及到查询的均采用HashMap以id为key进行储存,将诸多查找的操作由O(n)降为O(1)。此外,还要根据特殊用途选择特殊的容器,比如使用PriorityQueue优化最小生成树和最短路径算法,也能降低操作的时间复杂度。
维护变量保存计算结果
维护变量可以使原本查询为O(n^2)的操作变为O(1),难点在于全面考虑什么时候需要更新变量。如第二次作业互测时qgvs由于复杂度过高出现了CTLE,采用的优化是在MyGroup中维护valueSum变量,除MyGroup中的ap和dp时需要更新外,MyNetwork中的ar时也需要进行更新。
算法优化
并查集
第一次作业中的qci和qbs,若不采用并查集,前者的时间复杂度为O(n),后者更是由于调用前者,时间复杂度可以达到O(n^3);而采用并查集后,则可以将前者降为接近O(1),后者则为O(n),效果显著。具体来说,在MyPerson中新增变量fa保存父节点,同时设置setter和getter,最后在MyNetwork中实现合并与查找即可。
private void merge(int id1, int id2) {
int f1 = getF(id1);
int f2 = getF(id2);
if (f1 != f2) {
((MyPerson) getPerson(f2)).setF(f1);
}
}
private int getF(int id) {
if (((MyPerson) getPerson(id)).getF() == id) {
return id;
}
int ans = getF(((MyPerson) getPerson(id)).getF());
((MyPerson) getPerson(id)).setF(ans);
return ans;
}
更进一步的话,可以实现路径压缩,让小的子树的根节点指向大的子树,降低查找的复杂度。
同时,使用并查集也可以优化Kruskal算法,方便判断是否成环。
Kruskal和Dijkstra的堆优化
两者实现过程中均采用PriorityQueue容器,方便寻找权值最小的边和与起点距离最小的节点。
PriorityQueue<Edge> pq = new PriorityQueue<>((o1, o2) -> {
if (o1.getValue() == o2.getValue()) {
return o1.getDes() - o2.getDes();
}
return o1.getValue() - o2.getValue();
});
PriorityQueue<Node> pq = new PriorityQueue<>((o1, o2) -> {
if (o1.getValue() == o2.getValue()) {
return o1.getId() - o2.getId();
}
return o1.getValue() - o2.getValue();
});
三、扩展任务
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
-
advertise:
/*@ public normal_behavior
@ requires contains(id1) && getPerson(id1) instanceof Advertiser && containsMessage(id2);
@ assignable people[*].messages;
@ ensures (\forall int i; 0 <= i && i <= people.length &&
@ people[i].isLinked(getPerson(id1)) && people[i].getId() != id1;
@ \old(people[i].messages.length) == people[i].messages.length - 1);
@ ensures (\forall int i; 0 <= i && i <= people.length &&
@ !people[i].isLinked(getPerson(id1));
@ \old(people[i].messages.length) == people[i].messages.length);
@ ensures (\forall int i; 0 <= i && i <= people.length &&
@ people[i].isLinked(getPerson(id1)) && people[i].getId() != id1;
@ (\forall int j; 0 <= j && j <= \old(people[i].messages.length);
@ (\exists int k; 0 <= k && k <= people[i].messages.length;
@ \old(people[i].messages[j]) == people[i].messages[k])));
@ ensures (\forall int i; 0 <= i && i <= people.length &&
@ people[i].isLinked(getPerson(id1)) && people[i].getId() != id1;
@ (\exists int k; 0 <= j && j <= people[i].messages.length;
@ getPerson(id1).messages[j] == getMessage(id2)));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(id1) ||
@ contains(id1) && !(getPerson(id1) instanceof Advertiser);
@ signals (MessageIdNotFoundException e) contains(id1) && getPerson(id1) instanceof Advertiser &&
@ !containsMessage(id2);
@*/
public void advertise(int id1, int id2) throws PersonIdNotFoundException, MessageIdNotFoundException;除了最基础的功能(id为id1的Advertiser将id为id2的ProductMessage发送给所有与之关联的人),还可以设置商品热度等属性的变化,还可以向Group发送,或者整合进sendMessage函数中。
-
produce:
/*@ public normal_behavior
@ requires contains(id1) && getPerson(id1) instanceof Producer;
@ requires getPerson(id1).containsProduct(id2);
@ ensures getPerson(id1).productList.length == \old(getPerson(id1).productList.length);
@ ensures getPerson(id1).getProduct(id2).amount =
@ \old(getPerson(id1).getProduct(id2).amount) + 1;
@ also
@ public normal_behavior
@ requires contains(id1) && getPerson(id1) instanceof Producer;
@ requires !getPerson(id1).containsProduct(id2);
@ ensures getPerson(id1).productList.length == \old(getPerson(id1).productList.length) + 1;
@ ensures getPerson(id1).getProduct(id2).amount = 1;
@*/
public void produce(int id1, int id2);
-
purchase:
/*@ public normal_behavior
@ requires contains(id1) && getPerson(id1) instanceof Customer;
@ requires containsProduct(productId);
@ requires contains(id2) && getPerson(id2) instanceof Producer;
@ requires getPerson(id2).getProduct(productId).amount > 0;
@ ensures getPerson(id1).money = \old(getPerson(id1).money) - getProduct(productId).getValue;
@ ensures getPerson(id2).money = \old(getPerson(id2).money) + getProduct(productId).getValue;
@ ensures getPerson(id2).getProduct(productId).amount =
@ \old(getPerson(id2).getProduct(productId).amount) - 1;
@*/
public void purchase(int id1, int id2, int productId);
四、心得体会
这一单元给人的感觉就是一个图论单元,重点在于实现规格的同时进行性能上的优化,尽管是基于规格去写代码,但相较于前两个单元,无非是将题目描述从自然语言换成了JML语言,理想中的样子是实验那样,不过考虑到作业要落脚到代码上,似乎也没有更好的方案了。
在JML规格的描述下,代码的需求变得十分明晰,不会像自然语言那样有二义性。这是JML让我感受到的最大的好处。除此之外,JML的书写和阅读都略显困难,对我而言,写JML比写代码难多了,这个难不在于设计,而是如何用规格表示想法。在正确的JML规格指示下,确实可以很好地实现代码,但是一旦规格错误,代码需求就不得而知了。考虑到JML的特点,写错JML反倒不是一件难事(bushi)。