BUAA_2022面向对象_第三单元总结
BUAA_2022面向对象_第三单元总结
O、写在前面
正如助教所说,本单元的设计与执行,难度不大,这得益于JML的功劳;但是,对细心的要求尤其的高。如何理解、高效和全面的实现JML,是本单元的关键。在该单元中,我们完成了一个简单聊天系统的实现,与其说实现,不如说是补全。关键架构,是将各个消息收发者和群组的关系用图的形式表现出来,从而能够通过各种算法进行多样查询操作。
一、架构设计
除常规实现以外(依据JML规格的完全对应实现),笔者主要针对qci,qbs,qlc,sim
的性能进行了算法优化。由于问题主体是图论问题,因此我们可以单独用一个类进行图的建模与算法应用,可以大大缩短查询时间。
除此之外,还有一个细节,就是算法选择上,要朝着拓展性的方向,寻找与之前或者可能的之后实现的东西,具有较好契合性的算法。例如下面的并查集和kruskal算法就是很好的栗子。
1、qci与qbs——并查集的运用
并查集这个名称的产生,是来源于并查集中的两个主要步骤:合并、查找。它的原理其实很简单,即令每个结点记录所在组。单凭这一句话,我们就能理解,为什么并查集可以几乎在复杂度内查询两个节点是否相连——只需要判断其记录的所属组是否相等。
在具体实现中,需要建立一个一一对应关系的HashMap
,来记录节点与父节点即可。最初,所有节点的根节点均为自身id。每当建立了一条边(两节点建立联系),则按照尽可能减小树高度的规则合并两个节点所在树。在进行查询两个节点是否在一棵树上,只需要比较二者的根节点id即可。
但是利用并查集实现存在一个问题,那就是删除问题,当我们想从图中删除一个结点,需要综合考虑该节点联结的多条关系。所幸,题目要求指令只会将该节点从group中移除,但并不会在整个Network中移除。想想实际情况也能理解这样做的缘由,想必将一个人完全切断与所有人的关系是不可能的吧,但是“退群”现象可是时常存在的。
2、qlc——最小生成树的应用
该指令是求能够链接某节点所在群组所有人能花费的最小queryValue和。对本问题显然与最小生成树问题契合,因此我在此选用较好理解且与并查集相得益彰的算法,kruskal算法。
kruskal算法是一种典型的贪心算法,我们稍微阐述一下其基本思想。既然我们想要得到最小的路径权值和,那么不妨先将所有的边按照权值排序,然后依次从上往下查询,如果加入该边不会构成回路,则加入到当前有效集合。最终得到的有效集合的边集,即为最小生成树的边集,这对于我们的目的是一个极其直接的算法。
具体实现时,最主要是能够构造“边”对象,并实现一个排序接口,这样一来就能便捷地排序;除此之外还需要内部new一个并查集,用以查询是否构成回路。
3、sim——最短路径的应用
该指令要求找到最短的发送路径,并返回最短路径权值和。对于本算法,我们采用dijkstra算法。
从本质上来讲,dijkstra算法很好的利用了最短路径的递推特征,即该路径上任何一点到起点的路径仍然是一条最短路径。所以该算法再求出目的节点的最短路径的过程中,也顺带求出了起点到沿路各点的最短路径,如果允许,还可以计算出到所有节点的最短路径。
为了提升算法性能,这里采取了堆优化策略,所谓优化不过是优化了其中的排序过程。通过用优先队列(排序规则套用已实现的“边”排序接口)保存新加入的边,就能利用堆排序提升算法性能到了。
二、自测体会与bug修复
1、有关自测
本单元着实意识到自测的重要性,其缘由与作业的架构有较大的关系。因为本单元的设计,架构尤其明显,根据前面两章的体会,架构越是清晰,代码量与层次就越复杂,可以理解为抽象性守恒。
因此有许多细小的bug也许藏在了很深很深的角落,不做大量测试是无法找出的。
本单元的自测,其实主要还是要依靠xh同学的评测机,在保证他的实现正确性的条件下(已经进行了大量对拍),我们可以通过大量黑盒测试找到问题,然后自己依据错误点,自行构造数据测试(因为测试数据数量实在多,不易debug)。
2、有关bug
本单元的bug其实没有什么好讲的,如果必须要说的话,我只想说两点。
首先,能用ArrayList或者HashMap一定不要用数组。在并查集的实现,我是利用一个数组记录节点与父节点的映射关系,但是由于数组设置不够大,最终造成数组越界。只能说,非常可惜!
第二点是性能相关,除了利用算法优化的那几条指令,其中有一个qgvs指令着实让人哭笑不得。最初实现,每次查询都要遍历所有人之间的queryValue然后相加,但是如果有人构造足够多的人和足够多的qgvs,就会造成超时......解决方案是构造“缓存”,简单来说就是拿个数记录每一次的ValueSum。不得不说能找出这样bug的人,脑洞还是很大的。
其余bug基本都是按照JML照抄都能抄错的地方,不过黑箱测试大概是这类bug的克星吧。还是要感谢xh的评测机。
三、本单元体会
在经历第一、二单元的折磨后,第三单元应对起来更加得心应手,可耐仍然存在大意失荆州。细节定成败的真谛愈发明晰,与此同时,测试的重要性尤其凸显。除了这些在技术上的警醒外,最大的收获莫过于JML的学习了。
收获了require,ensure
等JML理解,领悟了契约化编程的思想,重要从一个人的编程上升到团队、工程系统级别的编程。尽管依据JML写代码是简单的,但是如果组织一群人从0开始共同完成一个代码任务的初衷是可敬的。
四、三个核心方法的JML
总结而言,撰写JML需要从:normal_behavior,require,assignable,also,exceptional_behavior,signals
这些关键字,全面考虑所有情况。
/*@ public normal_behavior
@ requires contains(personId) && getPerson(personId) instanceof Advertiser &&
@ containsMessage(messageId) && getMessage(messageId) instanceof AdvertisementMessage;
@ assignable people[*].messages;
@ ensures (\forall int i; 0 <= i < people.length;
@ \old(people[i].getMessages().size) + 1 == people[i].getMessages().size() &&
@ (\forall int j; 0 <= j < \old(people[i].getMessages().size());
@ \old(people[i].getMessages()).get(i) == people[i].getMessages().get(i+1)) &&
@ people[i].getMessages().get(0).equals(getMessage(messageId)));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(personId) ||
@ (contains(personId) && !(getPerson(personId) instanceof Advertiser));
@ signals (ProductIdNotFoundException e) contains(personId) && getPerson(personId) instanceof Advertiser &&
@ (!containsMessage(messageId) ||
@ (containsMessage(messageId) && !(getMessage(messageId) instanceof AdvertisementMessage)));
@*/
public int sendAdvertisementMessage (int personId, int messageId)
throws PersonIdNotFoundException, MessageIdNotFoundException;
/*@ public normal_behavior
@ requires containsProductId(ProductId);
@ ensures \result == getProduct(ProductId).getSalesAmount();
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(ProductId);
@*/
public int queryProductSales(int ProductId) throws ProductIdNotFoundException;
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < products.length; products[i].equals(product)) && (\exists int i; 0 <= i && i < preProducts.length; preProducts[i].equals(product))
@ assignable products,producer.getProducts()
@ 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(product));
@ ensures producer.getProducts().length == \old(producer.getProducts().length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(producer.getProducts().length);
@ (\exists int j; 0 <= j && j < producer.getProducts().length; producer.getProducts()[j].equals(\old(producer.getProducts() @ [i]))));
@ ensures (\exists int i; 0 <= i && i < producer.getProducts().length; producer.getProducts()[i].equals(product));
@ also
@ public exceptional_behavior
@ signals (EqualProductException e) (\exists int i; 0 <= i && i < products.length;
@ products[i].equals(product));
@*/
public void addProduct(Product product,Producer producer)throw EqualProductException