BUAA_OO_2022_第三单元总结

面向对象 第三单元总结

基于规格的测试策略

本单元的测试内容可以大致分成两个部分:

  • 首先是代码的正确性测试。由于本单元的主题是契约式编程,所以我们不需要担心在代码架构上的问题;但是很可能出现对于jml理解上的错误,进而导致代码中的bug。这一部分的测试我起初采取了课程组推荐的 JUnit 来单独测试每一个函数,但是发现实在是过于复杂,需要对每一个函数手动构造数据。因此,在后面的作业中我都采取了自动生成数据与同学对拍来发现问题。这一步一般只需要几个数据点就能解决掉绝大部分的bug。
  • 其次是代码的复杂度测试。本单元强测中有很多对于实现方法的复杂度的测试数据点,这一部分的测试通过算法手动构造数据点即可,比如说对并查集和最短路的压力测试。

架构设计

虽然说本单元已经给出了每个类的结构以及方法规格,但是留给我们自行设计的架构部分也有很多且十分关键,比如说容器的选择与图模型的构建与维护。

容器选择

在容器选择方面,我们可以用 ArrayListHashMapHashSetLinkedList 等等多种容器。具体该怎么选择,应当对于问题与容器特点进行分别分析后再进行选择。比如本单元中我大量使用的容器是 HashMap 而不是 ArrayList。这是因为我们常常需要通过id来索引某一个元素,若使用 ArrayList 则每次查找都需要遍历得到,时间复杂度为 O(n)。但是如果使用 HashMap 就可以在 O(1) 的复杂度内查找到目标元素。而在图相关的算法中,我们常常会用到队列,这时候就不能再使用 HashMap,而应当使用 Queue 等容器了。

图模型的构建与维护

  • 首先是第九次作业中的 qci 指令,查询图中两点是否连通,我使用了路径压缩的并查集来降低复杂度。具体来说,在普通并查集的基础上加入了路径压缩,将同一集合中所有结点的父节点都直接设置成根节点,这样就避免了图退化成一条链,进而导致超时的情况。
public int find(int curId) {
        int rootId = curId;
        while (roots.get(rootId) != rootId) {
            rootId = roots.get(rootId);
        }
        int fatherId;
        int tmpId = curId;
        while (tmpId != rootId) {
            fatherId = roots.get(tmpId);
            roots.replace(tmpId, rootId);
            tmpId = fatherId;
        }
        return rootId;
    }
  • 然后是第十次作业中的 qlc 指令,实际上是查询包含某结点的最小生成树。通常最小生成树有prim和kruskal两种算法,但是两种方法的原版复杂度都会导致超时的情况。因此,基于上一次作业实现的并查集,并考虑到本题目中稀疏图出现的概率是远大于稠密图的,我最终选择了并查集和快排优化的kruskal算法。
	/* 构建并查集 */
	for (int i: dsu.getRoots().keySet()) {
            if (dsu.find(dsu.getRoots().get(i)) == father) {
                dsuNew.getRoots().put(i, i);
                HashMap<Integer, Person> acq = ((MyPerson) getPerson(i)).getAcquaintance();
                for (int j: acq.keySet()) {
                    if (i > acq.get(j).getId()) {
                        continue;
                    }
                    left[cnt] = i;
                    right[cnt] = j;
                    length[cnt] = (Integer) ((MyPerson) getPerson(i)).getValue().get(j);
                    cnt++;
                }
            }
        }
       /* 快排 */
        Util.qSort(left, right, length, 0, cnt - 1);
        /* kruskal */
        for (int i = 0; i < cnt; i++) {
            int l = left[i];
            int r = right[i];
            int len = length[i];
            int leftFather = dsuNew.find(dsuNew.getRoots().get(l));
            int rightFather = dsuNew.find(dsuNew.getRoots().get(r));
            if (leftFather != rightFather) {
                res += len;
                dsuNew.getRoots().replace(leftFather, rightFather);
            }
        }
  • 最后是第十一次作业中的 smi 指令,实际上是查询两个点之间的最短路径。我选择了堆优化的Dijkstra算法,并且直接利用了java内置的 PriorityQueue 来存储结点。由于最短路算法本身比较简单就不放在博客上了。

代码性能及修复

由于有关容器和图的时间复杂度问题为已在上一部分进行分析,故本部分我只列出三次作业中强测及互测中出现的问题。

  • 第九次作业因为偷懒所以直接使用了bfs,虽然过了强测,却在互测被hack烂了,所以不得再bug修复时进行大重构整体换成了并查集。事实证明偷懒不会有好果汁吃。
  • 第十次作业本以为用了并查集优化的kruskal算法就不会出什么问题,却没想到在 qgvs 这个指令上挂掉了。我使用了朴素的两重循环导致时间复杂度是 O(n^2),同样在互测时被hack惨了。这个对应的修复也比较复杂,需要动态维护一个 HashMap<Integer, Integer> groupValue ,需要在 addRelationdelRelation 等多个方法中进行对应的修改。

Network扩展

对于Network的扩展,我选择 sendAdvertiseaddProductquerySale 三个方法来详细展开:

/*@ public normal_behavior
  @ requires containsMessage(id);
  @ assignable messages;
  @ assignable people[*].messages;
  @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
  @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
  @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
  @ ensures (\forall int i; 0 <= i && i < people.length; person[i].likeType(\old(((Advertisement)getMessage(id))).getType) ==> 
  @         ((\forall int j; 0 <= j && j < \old(person[i].getMessages().size());
  @          person[i].getMessages().get(j+1) == \old(person[i].getMessages().get(j))) &&
  @         (person[i].getMessages().get(0).equals(\old(getMessage(id)))) &&
  @         (person[i].getMessages().size() == \old(person[i].getMessages().size()) + 1)));
  @ also
  @ public exceptional_behavior
  @ signals (MessageIdNotFoundException e) !containsMessage(id);
  @*/
public /*@ pure @*/ void sendAdvertise(int id) throws MessageIdNotFoundException;
/*
			@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < products.length; products[i].equals(product));
      @ assignable products;
      @ 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));
      @ also
      @ public exceptional_behavior
      @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length;
      @                                     products[i].equals(product));
*/
public /*@ pure @*/ void addProduct(ProductInfo product) throws EqualProductIdException
/*@ public normal_behavior
  @ requires containsProduct(id);
  @ ensures \result == getProduct(id).getNum();
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !containsProduct(id);
  @*/
public /*@ pure @*/ int querySale(int id) throws ProductIdNotFoundException;

学习体会

本单元是我第一次正式接触契约式编程的思想,在学习之后不得不感叹JML语言的确可以严谨的给出函数和类的功能和执行要求。同时让我想到了这学期的os实验其实也与JML描述十分相似,我们可以通过函数上方的描述清楚地了解每个函数的作用是什么以及在实现时应当注意什么。但是我认为JML也有其弊端,其中最显著的一个就是为了追求严谨的表达有时会过于抽象和冗杂。在完成作业时看到有些方法上面是数十行的JML时实在是令人头痛不知该从何下笔。最后,虽然JML给出了我们代码的整体框架,却没有规定代码内部的实现逻辑。因此,我们必须认真考虑代码的时间复杂度甚至空间复杂度等属性,以免在运行时出现不可预料的错误。

posted @ 2022-06-01 21:31  JackyZhuo  阅读(56)  评论(1编辑  收藏  举报