第三单元总结

第三单元总结

一. 作业综述

一如既往,本单元作业分为三次进行迭代开发,核心任务为按照JML规格实现指定接口的指定方法,从而构建出一个具有无向图数据结构的网络。具体来说,第一次作业的难点是判断两点是否可达,第二次的难点是计算最小生成树的权重之和,第三次的难点是计算最短路径长度。

二. 架构设计

1. UML类图

鉴于本单元作业从第二次作业开始,仅仅是在上一次的作业的基础上增加类和方法,因此只给出第三次作业的UML类图。同时考虑到本次作业接口众多,尤其是异常类接口和相应实现类,限于篇幅,以下UML类图只给出了非异常类的描述。

 

跟第二单元总结时一样,因为没给StarUML这个软件充值会员,没法导出大幅的UML类图,只能分开截图上传博客。

2. 维护策略

2.1 第一次作业

第一次作业的isCircle()方法需要判断两个Person(也就是图中的两个节点)是否联通,最朴素的想法是使用dfs或者bfs,但是当节点数量较多时,bfs和dfs显然都会带来超时的问题。于是我选择并查集算法进行维护,并压缩路径以进一步节约下次查询同一个节点的根节点的时间,使得算法时间复杂度精简到接近O(log2n)O(log2n)。这样做顺便解决了求连通分支数量的queryBlockSum()方法,具体做法是在新加入一个Person(也就是图中一个新的节点)时令分支总数blockSum加1,发现两个Person(也就是图中两个节点)根节点相同,则合并连通分支并让blockSum减1。可谓一箭双雕。

2.2 第二次作业

queryLeastConnection((ArrayList<Edge> miniSpanTreeEdges))的目标是寻找权值之和最小的Connection,本质为在连通分支中秋最小生成树的问题。常见的选择无外乎Prim算法和Kruskal算法。由于第一次的作业已经采用了并查集的数据结构,该数据结构很有利于Kruskal算法的表达,因此很自然地想到使用Kruskal算法。这里为了方便处理,我额外建立了一个Edge类表示两点及其对应的边的权值以辅助边的权值有关的计算。此外,在最小生成树的构建过程中,当构建的树的边的数量达到节点数减一时,结束对连通分支边的遍历,以进一步节约时间开销。

2.3 第三次作业

第三次的sendIndirectMessage(int id)是最需要维护优化的方法,实现的关键在于能够计算出两个Person(也就是图中两个节点)之间的最短路径长度。这里Floyd算法实在难以适用,dijkstra算法成为唯一选择。我选择java自带 的小顶堆进行堆优化,效果不错,代码简洁。

三. 性能问题和修复情况

如前所述,三次作业在核心算法都做足了优化,没有出现性能问题。但是第二次作业在不起眼的方法上阴沟翻船了。getValueSum()这个方法我原本使用了对全部节点二重循环,结果在互测阶段被人使用大数据规模的qgvs指令疯狂hack,一度惨不忍睹。随后吸取教训,对getValueSum()中二重循环的第二重缩小了遍历范围,从而解决了问题。第一次作业和第三次作业在强侧和互测阶段都没有出现性能问题。

四. 基于JML规格的测试数据准备

1. 对JML规格的理解

• JML相当于一份伪代码,对于编程者有严格的逻辑指导作用。

• 输入的指令是固定的,且是易理解的,严格的按照JML规格做一定不会错。如果自测时发现错误,很容易定位到错误。

2. JML对构造测试数据的指导意义

2.1 构造检查特定异常的数据

由于JML规格中常有对能抛出多种异常的方法的异常顺序有着严格的规定,并不是只要所有异常都能覆盖并且互斥就能正确完成本次作业,因而可以构造特定数据专门测试异常抛出是否正确。

比如第一次强侧就因为addToGroup(int id1, int id2)方法和delFromGroup(int id1, int id2)抛出异常的顺序不正确而WA了三个点,可惜事先构造自测数据没有考虑这个问题。

2.2 帮助快速定位易超时函数

JML限制往往是采用循环遍历+函数嵌套调用的说明逻辑, 可以很容易找出O(n^2)复杂度及以上的函数。

比如第二次作业互测阶段出现大面积的tlle时,就是根据这个思路找到了bug来源getValueSum(),同样很可惜事先的自测构造数据没有意识到这个问题。

2.3 构造的数据格式和类型固定

只需要随机生成各种询问指令,询问对象主要分为两类:

第一类指令询问对象不存在图里,测试自己的异常抛出是否正确;第二类指令是测试所有指令在正常情况下的操作,检验自己是否严格遵从JML规格完成。

3. 用于测试的代码

3.1 Python语言的随机数据生成器

以下为第二次作业的版本:

3.2 CPP语言的比较器

用于和同学的代码对随机数据生成器生成的测试数据的运行结果进行对拍,同样为第二次作业的版本。

五. Network扩展

1. 广告消息发送

新增一个继承自Message的AdvertisementMessage接口表示广告消息。

在接口Network中新增抽象方法sendAdvertisementMessage(int personId, int messageId),实现广告商向id为personId的Person对象发送一个id为messageId的广告消息。

具体接口方法的JML规格描述代码如下:

2. 查询商品销售额

新增抽象异常类ProductIdNotFoundException,用于没有找到对应id的商品时抛出异常。

在接口Network中新增字段productIdList[],用于存放各种类Product的id。

在接口Network中新增抽象方法querySaleVleue(int productId),实现返回id为productId的商品的销售额,特别地,若Producer不供应id为productId的商品,返回值应该为0。

在接口Network中新增抽象方法getSaleValue(int personId, int productId),实现获得id为personId的生产商所生产的id为productId的商品的销售额。

具体接口方法的JML规格描述代码如下:

3. 指定消费者偏好

在接口Customer中新增字段preference,表示消费者偏好的商品。

在接口Network中新增抽象方法setPreference(int personId, int productId),实现指定,实现指定id为personId的Person的偏好商品是id为productId的商品。

具体接口方法的JML规格描述代码如下:

六.学习体会

严谨的JML规格描述没有二义性,对于代码开发者是非常有意义的。我在阅读JML规格的描述时感受到了滴水不漏的逻辑的美妙,虽然不可否认,这样的描述有时候略显多余。

做本单元作业的过程中,深刻体会了大一没认真学图论带来的后果,欠的债迟早要还的,我充分意识到了图论在计算机科学中的重要性,今后一定好好钻研练习图论。

优化过程痛苦,结果美妙。本单元作业为了避免超时问题,我采用了并查集的数据结构、java自带的小顶堆方式尝试优化,效果显著。我因此惊叹算法的强大,也再一次体会了算法优化的神奇之处,对算法产生了浓厚兴趣。等学有余力,很想好好集中训练算法,我想这样自己的代码能力才能有质的飞跃。

posted @ 2022-06-06 04:25  cfmcyl1024  阅读(56)  评论(0编辑  收藏  举报