BUAAOO_第三单元总结:JML与优化的辛酸历程

BUAAOO_第三单元总结:JML与优化的辛酸历程

写在前面

本单元JML的使用,让我爱恨交织。爱,是爱它的没有歧义,你所想到的所有可能产生歧义的方法行为,都会被JML解构的清清楚楚明明白白;恨,是恨它的纷繁复杂,一个简单的方法尚且还要用个十来行JML才能描述清楚,更别说复杂的方法,使用JML来描述就是灾难。满眼的ensures,极易给人带来生理不适,虽然在看多了之后,大概瞟一眼就能理解每一条语句大体是什么意思,但在入门过程中的确十分痛苦。

实现规格所采取的设计策略

由于给了JML语言描述的方法规格,并且已经搭好了接口的方法框架,因此大部分的工作就是翻译JML来实现具体的方法。具体的实现流程大体如下:

1、阅读自己认为最基层的类(如Person,Message)的属性规格,认真分析使用什么容器。千万不要无脑用ArrayList存放所有属性,很多属性明显用HashMap存放是更优的。比如:

/*@   @ public instance model non_null Person[] acquaintance;
      @ public instance model non_null int[] value;
      @*/

在看到这两句规格之后,千万不要无脑的把acquaintance与value直接用ArrayList存起来,否则你写代码的时候很潇洒,改代码的时候却很狼狈。

2、阅读官方包中的异常类,并完成。异常类相对其它而言较为简单,也较为独立,更容易把握。

3、遍历所有的JML规格,先完成较为简单,写法唯一的方法。比如:

//@ ensures \result == id;
    public /*@pure@*/ int getId();

    //@ ensures \result.equals(name);
    public /*@pure@*/ String getName();

    //@ ensures \result == age;
    public /*@pure@*/ int getAge();
	
	/*@ public normal_behavior
      @ assignable \nothing;
      @ ensures \result == (\exists int i; 0 <= i && i < acquaintance.length; 
      @                     acquaintance[i].getId() == person.getId()) || person.getId() == id;
      @*/
    public /*@pure@*/ boolean isLinked(Person person);

这种小方法有的可以直接写甚至可以不用分析,有的只需要分析一点点。

4、最后完成较为复杂的方法,不要单纯按照JML进行翻译,而要考虑到代码的时间复杂度与简洁程度等问题。

但在每一次作业中,都有一两个方法是需要花心思进行设计的,以下便列出需要设计的代码:

第九次作业

本次作业中最难的方法是Network 中的isCirclequeryBlockSum 方法,在大体估计了使用dfs遍历的程序耗时之后,我果断使用了并查集进行优化。增加并查集,需要给类增加容器来记录每个结点的根节点,每次在添加结点时都要对这个容器进行维护,核心代码如下:

private int getRoot(int id) {
        int root = roots.get(id);
        if (id != root) {
            roots.put(id, this.getRoot(root));
        }
        return roots.get(id);
    }

通过并查集,使isCircle 变为o(1)操作, queryBlockSum 变为o(n)操作(其实也可以优化为o(1),可以但没必要)时间复杂度降下来了,也就不怕被人刀了。

第十次作业

本次作业增加的难点是Group 中的 valueSum 属性。不要被getValueSum的JML蒙蔽了双眼:

/*@ ensures \result == (\sum int i; 0 <= i && i < people.length; 
      @          (\sum int j; 0 <= j && j < people.length && 
      @           people[i].isLinked(people[j]); people[i].queryValue(people[j])));
      @*/

虽然描述中是二重循环,但实际上是可以优化成O(1)复杂度的,只需要在加人、删人、删关系的时候对valueSum进行维护即可。

第十一次作业

本次作业增加的难点是sendIndirectMessage 方法,需要寻找最短路径。我采取的方法是堆优化的迪杰斯特拉算法,在保证正确性的基础上,获得一个较好的性能,防止tle。

这里就不放核心代码了。

测试的方法和策略

Junit正确性测试

本单元课程组是推荐我们使用Junit来进行正确性测试的,这样可以充分利用基于JML规格编程的好处。

使用了JML规格,我们可以通过前置条件、副作用和后置条件,依照一定的语法规则,描述清楚该方法在调用前后使整个程序发生了什么改变,实现了什么,而如何实现则是程序员应当考虑的。而Junit则可以根据这种特性,忽略中间完成的步骤,只检查方法调用前后整个程序发生的变化是否与规格相同便可得知是否正确。

然而Junit的确是太难用了...(也可能是因为自身使用不熟练)导致使用体验极差。而且据我所知本次的程序出错的可能性不是太大,而绝大多数错误是由于tle造成的,因此Junit进行正确性测试便被舍弃了。

肉眼测试

因为有了JML,大家的程序中可能出问题的点在自己写的时候也就注意到了,这时候着重去看可能出错的点,便有很大几率刀到人。比如观察Group 中是否规定了人数上限是1111,观察可能tle的方法的优化方式等。

对拍测试

这种方法应该也是绝大多数人使用的方法,本单元的评测机搭建并不困难(至少比电梯的评测机简单不少)。在自动生成大量测试数据后,很多正确性bug也就浮出水面了。但是这种方法有两个很大的弊端,一是无法保证自动生成样例的强度,二是无法保证与你一起对拍的同学的代码正确性,对拍人数越多越容易找到bug。

容器选择和使用的经验

完成第一次作业时,选择容器的心路历程:

这JML规格,咋还用的数组呢,太low了吧!看我换成ArrayList

这属性 有点多呀...诶,为啥觉得很多属性都是对应关系呢...比如acquaintance和value...这用个HashMap存不香吗?

可是规格用的是数组诶,我如果改成了HashMap,这改动也太多了吧...算了,就先用着ArrayList吧

等等...这查询方法每次都是o(n)操作???如果再嵌套个循环啥的,这性能不得爆炸呀!

似乎用HashMap又美观又简洁,还能降低复杂度...太香了...

算了算了,还是改了吧

在选择容器之前,应当充分了解各个属性的对应关系,比如Person与id之间,acquaintance与value之间等,使用HashMap,可以极大的降低查找时的时间复杂度。

如果对应关系不强,或者没有查询操作的属性,就没必要使用HashMap,而采用ArrayList还能够更方便的进行遍历操作。

各个类中使用的容器:

//Person
private HashMap<Integer, Person> peoples = new HashMap<>();
private HashMap<Integer, Integer> roots = new HashMap<>();//并查集
private HashMap<Integer, Message> messages = new HashMap<>();
private HashMap<Integer, Group> groups = new HashMap<>();
private HashMap<Integer, Integer> emojiHeatList = new HashMap<>(); 
//Person
private HashMap<Person, Integer> acquaintance = new HashMap<>();
private ArrayList<Message> messages = new ArrayList<>();
//Group
private HashMap<Integer, Person> peoples = new HashMap<>();

可以看出,由于Person,Group,Message等都有对应的id,因此存放在HashMap中更为方便。

性能分析

第九次作业

  • isCircle()

    功能:查询两个人是否连通

    实现方式:

    • dfs或bfs遍历整个网络,时间复杂度O(n^2),极其容易超时,因此排除。

    • 并查集

      需要增加一个容器,记录每一个结点的根节点,初始时设为自己;

      添加一个递归寻根的方法,递归调用,直到找到一个根指向自己的结点,并把沿途搜到的结点的根都设为该结点;

      每次添加关系时,都要对并查集进行维护,更新每个节点的根节点;

      查询是否相连时,只需要查询这两个结点的根节点是否相同。

  • queryBlockSum()

    功能:查询共有几个连通块

    实现方式:由于采用了并查集,只需要遍历所有节点,每一个根节点是自己的结点就是一块。因此时间复杂度是o(n)

第十次作业

  • queryGroupValueSum()

    功能:查询组内相连人员的value和

    实现方式:给Group增加valueSum的属性,并在addPerson()delPerson()以及addRelation()三个方法中对valueSum的值进行维护,这样查询操作的时间复杂度就是o(1)了。

    我在这里就由于时间复杂度的问题被hack了数刀,本身以为设置脏位的做法已经能够降低很多复杂度了,然而还是抵不过人工构造样例的狂轰滥炸。

第十一次作业

  • sendIndirectMessage()

    功能:通过两结点的最短路径发送信息。

    实现方式:这里需要用到数据结构的有关知识,我选择的是dijkstra算法。但如果采用普通的dijkstra算法,时间复杂度为o(n^2),有了第十次作业的教训,我觉得这肯定是会被hack的,于是进行了改进。

    因此我采用了堆优化的dijkstra算法,利用了priorityQueue,优化了每次取出value最小的结点的操作。优化后没有出现性能问题。

作业架构设计

三次作业我都是实现了官方包,严格按照JML规格进行实现,并没有增添新类。官方已经把架构设计的很清楚了,没有必要再解构了...

下面简单梳理整个的作业架构:

  • Person类 相当于图中的结点,其中的acquaintance属性则储存了与该结点相连的结点与边的权值。
  • Network类 相当于一张图,其中包含了所有的结点(Person),所有的关系
  • Group类 为各个结点的另一种关系形式,相当于一个群聊,里面的人可能是好友也可能不是好友,但可以群发message。

图模型构建:

  • Network就是一张图,包含的Person就是所有结点,边的信息全部存在Person的acquaintance中。

维护策略:

  • addPerson:增加结点
  • addRelation:增加边
posted @ 2021-05-31 20:05  是小鸿不是小红  阅读(83)  评论(0编辑  收藏  举报