BUAA_OO_第三单元作业总结-JML
一、利用JML规格来准备测试数据
本单元是最好搭建评测机并完成自测的了,原因如下:
- JML相当于一份伪代码,对于编程者有严格的逻辑指导作用。
- 输入的指令是固定的,且是易理解的,严格的按照JML规格做一定不会错,如果自测时发现错误,很容易定位到错误。
- 要求搭建的是一张具有权值的无向图,那么在生成数据的时候先构造结点,然后往图里加带权边就好了。
- 指令格式固定,可以按照要求随机生成各种询问指令,询问对象主要分为两类,第一类指令询问对象不存在图里,测试自己的异常抛出是否正确;第二类指令是测试所有指令在正常情况下的操作,检验自己是否严格按照JML规格完成。
- 不用自己写检验程序,和同学对拍就好,节省时间。
二、架构设计
严格按照JML规格设计带权无向图,Person
就是结点,而边存在每个Person
类里,即是JML规格里的acquaintance
,对于边专门建了一个Edge
类,里面保存了其权值和两个端点。Group
就是一个集合,这个集合理论上只需要我们保存结点,实际也是需要保存边的。只是这个结点带了一堆属性。
设计架构不得不考虑的时间复杂度问题:
- 为了避免\(O(n^2)\)复杂度统计
Group
里边的权值总和,我们需要在Group
里也维护边。维护Group
里的边就需要针对三条指令。1.addToGroup
:将结点加入集合里,此时我们需要\(O(n)\)遍历Group
里已有的结点,将边加入Group
里。2.addRelation
:在Network
里添加边后,我们也要\(O(n)\)遍历所有的Gruop
,在包含两个结点的Group
里添加边。3.delFromGroup
:要在Group
里\(O(n)\)遍历所有边,把含被删除结点的边删掉。 - 有个
qbs
的指令,实际是求联通分支的数量,有个isCircle
指令问两个结点是否联通,最简单的想法就是DFS,但这样时间肯定会炸,所以我们采用并查集来维护,并查集的复杂度接近\(O(log_2n)\)。有多少个结点的父亲是自己就带了多少个联通分支。当然,我们在求最小生成树的时候用Krusal算法时也需要用上并查集。 - 第三次作业里要求算两点间的最短路径,我们需要用上Dijkstra算法,常规的Dijkstra算法是\(O(n^2)\),在互测中绝对会被卡T,被疯狂hack。所以我们要用堆优化,用上
priority queue
这个类,并重写compareTo()
方法,就是比较加入的边的距离啦。优先队列可以让我们\(O(logn)\)找到最近的结点,因此可以\(O(nlogn)\)实现Dijkstra算法。
三、性能和修复问题
首要奥义:严格按照JML规格定义实现功能。
次要奥义:不能出现\(O(n^2)\)复杂度的方法。
- 第一次作业强测拿了满分,在互测阶段果然有人疯狂用qbs指令hack,部分实现了并查集的同学没有想到联通分支的数量等于自己的父亲是自己的结点的个数(
感觉这话怪怪的)。于是仍然用的\(O(n^2)\)复杂度的方法找的联通分支。 - 第二次作业的互测有大量用qgvs指令hack那些时间复杂度为\(O(n^2)\)统计
Group
里的所有边的权值总和的方法。不少同学都被卡T了,所以这个单元不光要按照JML格式来,也要学会自己优化。 - 第三次也是强测和互测都没扣分,这次有同学没有堆优化Dijkstra算法被卡T了,果然大家都在找\(O(n^2)\)算法的人卡啊。
四、扩展Network,撰写JML规格
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
我选择实现购买商品,设置偏好,查询销售额三种功能
购买商品:
/*@ public normal_behavior @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id1 && people[i] instanceof Advertiser); @ requires (\exists int i; 0 <= i && i < products.length; products[i].getId() == id2); @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id3 && people[i] instanceof Producer); @ ensures getPerson(personId).money = \old(getPerson(personId).money) - getProduct(productId).getValue; @ ensures getSaler(salerId).getProduct(productId).getLeftNum() = \old(getSaler(salerId).getProduct(productId).getleftNum()) - 1; @*/ public /*@ pure @*/void purchaseProduct(int id1, int id2, int id3);
设置偏好:
/*@ public normal_behavior @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id1); @ requires (\exists int i; 0 <= i && i < products.length; products[i].getId() == id2); @ ensures getPerson(personId).isFavorable(productId) == true; @*/ public /*@ pure @*/void setFavorite(int id1, int id2);
查询销售额:
/*@ public normal_behavior @ requires (\exists int i; 0 <= i && i < products.length; products[i].getId() == id); @ ensures \result == GetProduct(id).getSale(); @*/ public /*@ pure @*/void querryProductValue(int id);
五、学习心得体会
- JML编写和写代码还是有很多相似之处的,比如我们熟悉的循环操作\forall、存在操作\exists等。规格化语言消除了自然语言的歧义,提供了一个统一的规范,有利于检测代码的正确性。但是,我们不能直接把简单的JML语言理解为真正的代码实现,因为真正的代码实现需要权衡算法、数据结构等。一定要认识到规定前因后果和实现过程是不同的,比如一定要考虑时间复杂度,\(O(n^2)\)以上的复杂度达咩。另外,中测等于没测,别信。在规格化测试的基础上,需要我们手动构造极端数据,并且利用测评机加以测试,才能更好地验证代码的正确性。
- 这个单元对同学们十分友好,不用花费太多精力,有JML规格帮你正确性的检验,所以第一要义是细心认真。虽然我也延续了摆烂,没hack过,互测结束了都没参与过hack,不知为啥,还有些许的遗憾。