(1)梳理JML语言的理论基础、应用工具链情况
Java建模语言(JML)是一种行为接口规范语言,可用于指定Java模块的行为 。它结合了Eiffel的契约方法设计 和Larch 系列接口规范语言的基于模型的规范方法 ,以及 细化演算的一些元素 。
也就是说。JML能让我们对某个JAVA的行为进行规范,而不用真正地陷入到实现的细节中去。
JML通过将注释添加到代码之中,以确定代码执行的规范。有了JML,我们可以描述方法预期的功能,而无需关心其具体实现。
JML引入了许多构造,包括模型字段、量词、断言、可见度范围、后置条件、不变量等规范。而配合上SMT Solver,可以对我们的代码进行形式化验证、
• (2)部署SMT Solver,至少选择3个主要方法来尝试进行验证,报告结果
• 有可能要补充JML规格
• (3)部署JMLUnitNG/JMLUnit,针对Graph接口的实现自动生成测试用例, 并结合规格对生成的测试用例和数据进行简要分析
这两个东西我弄了4、5个小时了,实在是弄不动啊。各种运行的时候出错,想吐了
赞美伦佬,我把这玩意儿弄出来了!
Failed: racEnabled()
Passed: constructor MyPath(null)
Passed: constructor MyPath({})
Passed: <<MyPath@1>>.compareTo(null)
Passed: <<MyPath@1>>.compareTo(null)
Passed: <<MyPath@1>>.containsNode(-2147483648)
Passed: <<MyPath@1>>.containsNode(-2147483648)
Passed: <<MyPath@1>>.containsNode(0)
Passed: <<MyPath@1>>.containsNode(0)
Passed: <<MyPath@1>>.containsNode(2147483647)
Passed: <<MyPath@1>>.containsNode(2147483647)
Passed: <<MyPath@1>>.equals(null)
Passed: <<MyPath@1>>.equals(null)
Passed: <<MyPath@1>>.equals(java.lang.Object@490d6c15)
Passed: <<MyPath@1>>.equals(java.lang.Object@449b2d27)
Passed: <<MyPath@1>>.getDistinctNodeCount()
Passed: <<MyPath@1>>.getDistinctNodeCount()
Failed: <<MyPath@1>>.getNode(-2147483648)
Failed: <<MyPath@1>>.getNode(-2147483648)
Failed: <<MyPath@1>>.getNode(0)
Failed: <<MyPath@1>>.getNode(0)
Failed: <<MyPath@1>>.getNode(2147483647)
Failed: <<MyPath@1>>.getNode(2147483647)
Passed: <<MyPath@1>>.hashCode()
Passed: <<MyPath@1>>.hashCode()
Passed: <<MyPath@1>>.isValid()
Passed: <<MyPath@1>>.isValid()
Passed: <<MyPath@1>>.iterator()
Passed: <<MyPath@1>>.iterator()
Passed: <<MyPath@1>>.size()
Passed: <<MyPath@1>>.size()
===============================================
Command line suite
Total tests run: 31, Failures: 7, Skips: 0
===============================================
上面是对MyPath的测试。可以看出,他测试了一些很丧心病狂的数据。
7个Failures主要的问题在于,我们作业的getNode没有描述异常的情况。他默认这个getNode都是正常运行的。然而他测试了一些非法数据……
我尝试过发现范围不对就抛出异常,但是仍然算我Failed,估计要修改JML才行了?
构造函数之前也出现了Fail的情况。因为我没有处理过传入参数是NULL的情况。(正常情况下不会出现)
然后这个东西居然也测出来了。实在是厉害了
• (4)按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构
这三次作业,都是与图相关的。我现在按照三次作业的顺序来说一下我做作业的流程。
第一次作业:
要求我们实现Path和Pathcontainer。
Path
对于Path而言,通过阅读其JML,我观察发现Path是一个一经构造,就再也不改变的类。因此,在设计这个类的时候只需要考虑如何存储方便,如何高效地存储就行了。
其要求实现以下几个函数:
构造,通过下标获取结点,获取迭代器,获取不同节点数,判断是否相等,判断是否有效,两个路径字典序比较。
由于其一经创建,不会再改动,所以存储的时候,我就优先想到了ArrayList这个容器。直接把节点按顺序存储就可以了。而ArrayList这样的存储方式,很轻松能解决下面几个函数的实现:
- 通过下标获取结点
- 获取迭代器
- 判断是否相等
- 判断是否有效
而剩下的几个函数,路径的字典序比较可以直接用两个Path中的iterator进行比较。构造函数也很轻松实现。
唯一需要解决的函数就是获取不同的节点数。在这里我用了HashSet去解决这个问题。HashSet听这个名字就比较快(笑)
PathCotainer
要求我们实现以下几个函数:
构造函数,加入路径,判断路径是否存在,判断路径id是否存在,获取路径总数,获取不同节点个数,通过路径获取ID,通过ID获取路径,删除路径,通过ID删除路径。
其实就是加强版的一个Path。由于这个函数里面需要获取路径,而路径这个东西不太好用数组去存,于是我们使用HashMap来进行从路径到ID和从ID到路径的映射。
同时维护一个HashMap,这个是记录某个节点在图中的出现次数。在添加或者删除Path的时候,记得遍历一次这个Path,进行结点在增删即可。
第二次作业
Graph
此次作业比上次多的一个类就是Graph,需要继承PathCotainer的接口。也就是说,PathCotainer能做的,他都必须要能做。
而多出来的函数有:
- 判断两个点是否连接
- 求两点的最短路(默认边的权是1)
这两点,我最初想的是,很容易解决啊。利用冰茶几来求联通性,再用Floyd来求最短路,美滋滋。
但是在数据结构或者我们的OI竞赛中,一般都是用数组去存点存距离的。然而这一次很显然不能开数组,因为这个结点的范围是int,所以直接开数组肯定凉凉。
第一版的时候我过于相信HashMap的速度,把HashMap<Integer,Integer>当作一维数组,加上一个Pair当作二维数组用。结果写出来的程序直接TLE了……
于是后来修改的时候,只能用一个映射表的方法,类似于OS的LAB2里面用的映射方法。把每个点映射出一个虚拟ID出来,然后开一个数组,对虚拟ID进行最短路的求解什么的。
第三次作业
地铁系统
比上次多出来的要求有:
- 找联通块的个数
- 找最少换乘次数
- 找最少票价
- 找最低不满意度
可以看出,第一个问题就是冰茶几再弄一下就好了。而其他三个问题都是和最短路有关。但是有一个问题是,这个最短路涉及到了换乘。如何表示这个换乘呢?
第一种方法是WJY在讨论区里面说的,连边法。就是把一条Path给密密麻麻地连起来,先求好Path内的最短路。然后把每条最短路的权加上换乘的代价。这样子,走多少条这样的最短路,就代表了在几个Path上走过,最后对结果稍做处理,就能得到我们想要的答案了。
不过这个方法有一个问题是,操作有点过于复杂了。因为这个操作需要在内部疯狂加边,最后的边数量会很多。边的数量多的时候,最好用Floyd算法,而Floyd我在第二次作业的时候被坑过一次了。所以不敢采用。
第二种方法是所谓的菊花点法。也就是说,认为每个Path上的点都是“虚拟点”,而所有相同Id的虚拟点都连到一个菊花点上。这样任意两个不同的Path进行换乘的时候,必定经过菊花点。只需要修改这个菊花点到其他虚拟点的边的权值,还有虚拟点之间的权值,即可实现对那三个问题的求解。
第三次作业我写了两个版本。第一个版本是屎山,后面XSY大佬教我一个更好的架构。
这个是第一次的架构:
- 用染色法对图进行染色,求连通块。
- 用Bfs求出最短路,同时能解决连通性问题。
- MyGraph是直接继承了上一次写的MyContainer
- 通过getNodeHashMap之类的方法,获取父类的nodeHashMap之类的成员,方便进行对图的信息的读取修改。不然的话,得多存一份图的信息,效率太低了。
- 重载父类的addPath,delPath之类的方法。先调用父类的方法。调用完之后再进行本类所需要的一些操作。比如更新needCal变量,把边加入到Bfs和Color的Map里面之类的
- 最短路问题,连通块问题由第二次作业(Graph)的部分去解决,然后铁路系统里面,内置三个不同的图,分别对应三个不同的问题,换乘,最低不满意度,最低票价。铁路系统去求解这三个问题。
然后这个架构的问题是。有许多东西在许多层都用得上。比如Path2Id。这个东西从Container一直用到System。并且重复写了三个图,也就是开了三份数组。每份数组对应一种图。很混乱很头疼。
要写3次迪杰(因为我java的引用没有c语言里面的指针学得好,不敢乱来),非常冗杂。
并且染色的过程中,可能出现染了不在图上的点的情况(这个点已经被del了,id被回收)。
第二次的架构:(赞美SXY!!!)
- 在System中维护Path集还有点集,用HashMap来实现
- System提供一个HashMap去存放虚拟Id映射关系
- System提供一个Queue去存放回收的虚拟id,保证虚拟id不超过120
- System中边集采用的HashMap,把一个点与一个ArrayList进行关联,然后这个ArrayList就代替了在C中的链表用。里面是存一个存放to和Val的类
- System中内置了一个数组实现的冰茶几用于实现求连通块
- System中内置了一个needCal,还有dist,用于实现最短路与是否相连相关。
- System实现三种最小花费,是通过调用成员Graph实现。直接把Path集,Graph类型,虚拟节点映射关系给传入Graph,然后Graph进行重构。(Path有更新的时候进行重构)
- Graph中,构造函数基本没做什么事情。大部分的事情放在了重构函数进行。也就是把各个标记变量给初始化,然后把传入的Path集给导入。
- Graph中,采用了算数计算的方法,把Path,Node进行唯一地映射成一个虚拟节点。方便菊花链的操作。
- Graph中,迪杰斯特拉的计算采用了类似Lazy的操作。只有在需要计算的时候才开始,而不是每次重构都开始。并且对出发点进行标记。由于迪杰实际上是单源多终点的最短路,所以计算一次之后,以后再次询问相同起点的最短路,就可以直接得出结果。
- Graph中,获取最短路的时候,根据tag来判断是否需要重新计算。得到计算出来的结果之后,再根据不同的图类型,对结果进行不同的调整,以得到正确的答案。
虽然这个写法一点都不OO,直接跳过Container,Graph(课程要求的Graph),而是直接一步到位写了System。这个架构里面的Graph是自己定义的一种类型,专门用来求迪杰斯特拉。每次增删Path的时候重构一次。需要求三种问题的时候,就调用不同的图,计算一下迪杰就好。
• (5)按照作业分析代码实现的bug和修复情况
第一次作业:无bug
第二次作业:TLE,直接重构了。还是老老实实虚拟Id映射来的好,不要想着全程HashMap莽过去了……
第三次作业,无bug
其实主要是有一个关键点是虚拟id与真实点的映射问题。题目和我们说了,不同的点最多同时只会存在120个,但是历史上可能存在远大于这个数量的点。如果要开数组的话,可能会开得很大。并且数组一大了,如果老做无用的遍历数组,就会浪费很多时间。java有多慢相信大家是有体会的。
我一开始感觉这个问题十分难解决。后来XSY大佬教我回收虚拟id的方法。也就是说,删除一个node的时候,可以把他的虚拟id给塞到一个队列里面。而一个node需要申请虚拟id的时候,优先从这个队列里面取id。如果这个队列是空的,才考虑idcnt++这种情况。
而另一个问题是,菊花点如何快速寻找的问题。
菊花点法里面,所有的出发和到达都是从菊花点开始。而如何快速从Path上的一个Node得到Node本身的虚拟编号,以及其对应的菊花点的虚拟编号,就是一个很大的问题。这个编号的获得,需要快速,否则效率就会极慢,因为求最短路的时候会大量需要这个操作。
还是XSY大佬教的方法。由于最大点数固定,所以可以采用类似于数组的方法获得新id。即排数*每排元素个数得到唯一地址。而在这里,排数和每排元素个数就可以用Path的id以及最大不同点数(120)来代替。再加上120的偏移,就可以保证计算出来的虚拟id与“原生虚拟id”不同。
菊花点的虚拟id就用原生虚拟id表示,在Path上的某点就用新的虚拟id来表示。这样就解决了映射的问题。
而至于寻找连通块个数。有两种方法,一种是染色法,一种是冰茶几。
染色法:
- 类似于BFS,难度适中
- 可以直接用最后染色的id去判断有几块染色块
- 但是对一个点是否在图上没判断好的话,得到的结果容易出事
冰茶几:
- 特别好写,短小精悍
- 需要一个一个比对fa是否等于自身,以此来判断这是否是根节点
- 获取连通快需要遍历一遍点,不过好在点少,才120个(最多)
由于染色法会需要多写一个图。多张图就容易出bug,所以还是用了冰茶几去写。毕竟冰茶几又短又快,某次OS的课上Extra用冰茶几就可以直接秒了。
• (6)阐述对规格撰写和理解上的心得体会
其实对于个人来说,规格撰写很像我们学的离散数学1。特别是==》这个推理符号,基本上就是在复习之前离散的内容。比如存在XXX,不存在XXX。
基本只要离散没忘光的同学写这些东西都没问题吧?
并且最好的一点是能“调用”其他的函数进行辅助,没有纯离散那么恶心了。
但是,没有纯离散那么恶心,不代表就一点都不恶心了。
请欣赏一下下面的东西:
/*@ ensures (\exists int[] arr; (\forall int i, j; 0 <= i && i < j && j < arr.length; arr[i] != arr[j]);
@ (\forall int i; 0 <= i && i < arr.length; (\exists Path p; this.containsPath(p); p.containsNode(arr[i])))
@ &&(\forall Path p; this.containsPath(p); (\forall int node; p.containsNode(node); (\exists int i; 0 <= i && i < arr.length; node == arr[i])))
@ &&(\result == arr.length));
@*/
public /*@pure@*/int getDistinctNodeCount(); //在容器全局范围内查找不同的节点数
这个规格实在是有点让人看得不舒服……
其实,个人感觉,写规格能让人比较清楚地知道自己这个函数要做什么。可能有哪些坑,因为写这个规格要很严谨的思路才能完成。不过对于复杂的函数来说,写规格描述就非常令人头大了。
但是反过来说,如果一开始就写规格,鉴于这个令人头大的程度。说不定写出来的程序都十分短小,不会出现一个方法复杂度爆炸的情况?(笑