一、JML简介

Java建模语言(JML)是一种行为接口规范语言,可用于指定Java模块的行为。我们知道,面向对象分析和设计的原则之一就是应当尽可能地把过程设想往后推,把精力集中于整体架构的设计,协调管理各个单元部件的运转,而不拘泥于某些琐碎细节实现当中。

因此,在设计层面,我们对于每一个类以及方法,要淡化过程,只需关注它的结果,所以我们需要精准地描述出它的行为以及影响。(就和计组中,我们使用列表的方法将IFU、ALU、GRF等部件的输入输出功能全部列举出来一样。)但是 Java 语言没有提供显式地将这种信息合并到代码中的方法,JML便应运而生。

二、UML设计图

三、具体算法实现

本次作业的关键是sendIndirectMessage方法,该方法的核心是求解两点间的最短距离,笔者采用的思路是堆优化的 Dijistra,利用了java中的PriorityQueue容器,十分方便。

public int shortest(int start, int end) {
       for (Integer id : people.keySet()) {
           dis.put(id, inf);
           vis.put(id, false);
      }
       dis.put(start, 0);
       queue = new PriorityQueue<>();
       queue.add(new Pair(0, start));
       while (!queue.isEmpty()) {
           Pair temp = queue.poll();
           if (vis.get(temp.getTo())) {
               continue;
          }
           if (temp.getTo() == end) {
               break;
          }
           vis.put(temp.getTo(), true);
           MyPerson myPerson = (MyPerson) getPerson(temp.getTo());
           HashMap<Integer, Person> acquaintance = myPerson.getAcquaintance();
           for (Integer id : acquaintance.keySet()) {
               if (vis.get(id)) {
                   continue;
              }
               int tempDis = temp.getDis() + myPerson.queryValue(acquaintance.get(id));
               if (dis.get(id) > tempDis) {
                   dis.put(id, tempDis);
                   queue.add(new Pair(tempDis, id));
              }
          }
      }
       return dis.get(end);
  }

本次作业还有一个方法可能导致CTLE却被很多同学忽略,那就是第二部分提到的 qgvs,笔者的措施是动态维护Group的valueSum,具体有以下几点:

  • addRelation时,更新所有包含person点的组

    public void updateGroupValueSum(Person person1, Person person2, int value) {
           for (Group group : groups.values()) {
               if (group.hasPerson(person1) && group.hasPerson(person2)) {
                  ((MyGroup) group).addValueSum(value);
              }
          }
    }
  • addPersondelPerson

    public void addPerson(Person person) {
           this.people.put(person.getId(), person);
           for (Person per : people.values()) {
               valueSum += person.queryValue(per);
          }
      }
    public void delPerson(Person person) {
           people.remove(person.getId());
           for (Person per : people.values()) {
               valueSum -= person.queryValue(per);
          }
      }

这样我们将原本 O(n^2)的复杂度分摊成了 O(n)。

 

四、BUG分析

三次作业中,强测得问题不大,但是在互测中总能被人找到设计上的不合理,最终被cpu_tle。对于这类问题,有以下两个解决方法:1.设置更多的属性,如在算group的标准差时,应当将group的均值作为类的属性加入到类当中,这样不用在调用方差函数时每一次都重新计算均值,这样可以让时间复杂度变为o(n);2.降低Dijkstra等算法的时间复杂度,用堆栈的方式,可以将Dijkstra的时间复杂度降低为nlogn。

本次作业没找到heck别人的点。

五、感想

我认为本单元仅仅想要拿到作业分数并不算难,但本单元真正需要思考的却应当是设计与架构方面的问题。对于我的三次作业,前两次作业由于较为简单,因此我的代码中只有继承了三个接口的MyPerson、MyNetwork、MyGroup。第三次作业需要较为复杂的算法,因此我将那些重要的算法都单独分出了一个类,如Dijkstra、Tarjan、UnionFindSet这样三个类。这样将复杂的算法单独分离出来,便于相关算法的维护,也避免了将所有代码堆到MyNetwork中,对于这样的做法我还是比较满意的。不过,对于更深层次的构造问题我没有考虑更多,毕竟实现这些算法就够我花上两三天了。

这个单元我对于规格与代码的区别有了一个较为清晰的认识,也认识到方法的行为是可以从函数中抽象出来单独表示的。虽然之后写JML规格的机会很少,但是在之后写代码的时候至少会在脑海过一遍相关的行为抽象,也会有意识地控制自己写的方法,使其行为的抽象都能较为简单地描述出来。事实上,如果某个方法很难用规格描述,那么就说明这个方法的实现很有可能有较大问题。因此,这一单元的学习对于我之后写代码的影响还是比较大的。