BUAA-OO-UNIT3-JML

JML规格及测试:

JML规格:

JML规格提供了一种对于数据的约束,通过更加形式化的语言来对于前置条件、后置条件进行约束,从而保证程序在正确执行后能得到正确的结果。

总体来说,JML规格相较于平时使用的自然语言来说更加复杂,这在作业中也有很明显的体现,例如最短路、最小生成树的描述,让人看着感到不适。

但这样也有效的避免了二义性的出现,同时也使得实现的过程中的逻辑更加清晰。

测试:

首先是介绍了一种新的测试手段JUnit。它需要你实现一个testcase子类,通过断言的方式来进行验证。

其次还是通过自己手写评测机的方式,进行对拍。我在测试的过程中主要还是通过对拍的方式进行。

JML规格提供了对于数据的约束,通过这些约束,可以进行数据的构造,也可以进行评测机的编写。

数据主要还是根据规格进行make,通过随机数据进行全面覆盖的测试,再通过构造corner case的方式进行边界测试,最后对于时间复杂度进行测试以及优化。

架构设计:

这三次的作业是实现一个人际网络关系,主要有Network,Group,Person这几个主要的类需要实现以及增量开发。

NetWork作为最顶层的类,内部包含了Group和Person。Group可以理解为群聊,消息的发出可以是P2P也可以是P2G,通过不同消息的类来实现。

除了JML规格需要实现正确性,还需要引入一些图论算法或者是预处理对于询问进行加速。

例如并查集、kruskal实现最小生成树、堆优化的dijkstra算法等。

在增量开发的过程中,会遇到一个类的长度大于500行的问题,这就需要我们进行解耦,专门抽离出一部分内容单独实现,我是通过新开了一个algorithm类,将所有需要使用的算法放入其中。

具体实现如下:

最小生成树:

public int mst(int id) {
        int rt = 0;
        int totalNode = 0;
        ArrayList<Edge> edges = new ArrayList<>();
        ArrayList<Person> people1 = new ArrayList<>();
        MyPerson fa = find(id);
        for (Person person : people) {
            if (!find(person.getId()).equals(fa)) {
                continue;
            }
            totalNode++;
            people1.add(person);
            ArrayList<Person> node = ((MyPerson) (person)).getAcquaintance();
            for (Person p : node) {
                if (find(p.getId()).equals(fa)) {
                    edges.add(new Edge(person.getId(), p.getId(), person.queryValue(p)));
                }
            }
        }
        for (Person person : people1) {
            ((MyPerson) (person)).setFa(person.getId());
        }
        edges.sort(Comparator.comparingInt(Edge::getValue));
        int totalEdge = 0;
        for (Edge edge : edges) {
            MyPerson fa1 = find(edge.getNode1());
            MyPerson fa2 = find(edge.getNode2());
            if (!fa1.equals(fa2)) {
                rt += edge.getValue();
                fa1.setFa(fa2.getId());
                totalEdge++;
            }
            if (totalEdge == totalNode - 1) {
                break;
            }
        }
        return rt;
    }

并查集:

    private MyPerson find(int id) {
        MyPerson temp = ((MyPerson)getPerson(id));
        if (id == temp.getFa()) {
            return temp;
        }
        int fa = temp.getFa();
        MyPerson person = find(fa);
        temp.setFa(person.getId());
        return person;
    }

dijkstra:

HashMap<Integer, Integer> ans = new HashMap<>();
                PriorityQueue<Node> pq = new PriorityQueue<>();
                pq.add(new Node(getMessage(id).getPerson1(), 0));
                ans.put(getMessage(id).getPerson1().getId(), 0);
                Person end = getMessage(id).getPerson2();
                while (!pq.isEmpty()) {
                    Node temp = pq.poll();
                    if (temp.getDis() != ans.get(temp.getPerson().getId())) {
                        continue;
                    }
                    if (temp.getPerson().equals(end)) {
                        break;
                    }
                    ArrayList<Person> person = ((MyPerson)temp.getPerson()).getAcquaintance();
                    ArrayList<Integer> val = ((MyPerson)temp.getPerson()).getValue();
                    int size = person.size();
                    for (int i = 0; i < size; i++) {
                        if (!ans.containsKey(person.get(i).getId()) ||
                                temp.getDis() + val.get(i) < ans.get(person.get(i).getId())) {
                            ans.put(person.get(i).getId(), temp.getDis() + val.get(i));
                            pq.add(new Node(person.get(i), temp.getDis() + val.get(i)));
                        }
                    }
                }

通过这样的形式可以很大程度上使得network的实现更加清晰,阅读代码和debug也相对更加轻松。

问题以及修复:

在作业中主要是遇到的性能问题,一部分原因是由于做本地测试的时候评测环境比较好,速度较快,从而忽略了一些写法上的优化。

第一次作业因为QBS(Query Block Sum)操作的复杂度过高导致TLE,后面改成了在加入或者删除一个人或者一条边的时候,考虑对答案的改变。

第二次作业和第三次作业主要问题都是JML阅读不仔细,第二次是QVS(Query Value Sum)想当然的认为一条边的贡献只算一次,导致答案只有正确答案的一半。

第三次作业是对于红包消息的发送,是发送到除自己以外的所有人,我发送到了group内部包括自己的所有人,这导致QM的时候会出现错误。

但由于评测机是根据自己错误理解写的,跟别人进行对拍的时候主要也是集中于需要算法实现的几个方法,因此导致最后错误了几个点。

第三次作业由于dijkstra算法写的不是很好,虽然在本地能够很好的完成,但在评测的时候出现了TLE的情况。

最后发现在最开始的时候初始化HashMap,就将HashMap填入了1000+个元素,导致取用的时候复杂度高。

后来改成了在取用的时候先判断是否存在,不存在就应该为INF,然后修改之后再加入HashMap,这样减少了HashMap取值的复杂度,在最终评测的时候能优化1倍以上的时间。

NetWork拓展:

Advertiser、Producer、Customer可以在Person的基础上继承。

Person吃瓜群众不接受任何购买信息,可以直接忽略。

Producer在生产了物品之后,将信息发送给Advertiser,Advertiser再将这些信息发送到需要的Customer。

对于Producer或者通过Advertiser的销售额统计:

/*@public normal_behavior
    @requires contains(id) && getPerson(id) instanceof Producer;
    @ensures \result == getPerson(id).getTotalSales();
    @also
    @public exceptional_behavior
    @signals (PersonIdNotFoundException e) !contains(id);
    */
public int querySales(int id);

对于Advertiser发送的广告进行统计:

/*
 @ public normal_behavior
 @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Advertiser && 
            (\forall int j; 0 <= j && j < people.length; people[j].getId() == id && people[j] instanceof Customer && 
            (\forall int k; 0 <= k && k < people[j].ads.length; people[j].ads[k].getId() != people[i].getAdId()))) ;
 
 @ assignable people[*];
 @ ensures  (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Advertiser && 
            (\forall int j; 0 <= j && j < people.length; people[j].getId() == id && people[j] instanceof Customer && 
            (\exists int k; 0 <= k && k < people[j].ads.length; people[j].ads[k].getId() == people[i].getAdId()))) ;
 @ also
 @ public exceptional_behavior
 @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer);
 @ also
 @ public exceptional_behavior
 @ signals (AdIdNotFoundException e) (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Advertiser && 
                                     (\forall int j; 0 <= j && j < people.length; people[j].getId() == id && people[j] instanceof Customer && 
                                     (\exists int k; 0 <= k && k < people[j].ads.length; people[j].ads[k].getId() == people[i].getAdId()))) ;
 */
 public sendAd(int id) throws PersonIdNotFoundException, AdIdNotFoundException;

Producer生产一个新产品:

    /*@ public normal_behavior
      @ requires contains(id1) && getPerson(id1) instanceof Producer && containsMessage(id2) 
    @&& getMessage(id2) instanceof ProduceMessage && getPerson(id1) == getMessage(id2).getPerson1();
      @assignable messages;
      @assignable getMessage(id2).getPerson2().getProduct();
      @ensures !containsMessage(id2) && 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 < \old(getMessage(id2).getPerson2().getProducts().size());
      @          \old(getMessage(id2)).getPerson2().getProducts().get(i+1) == \old(getMessage(id2).getPerson2().getProducts().get(i)));
      @ ensures \old(getMessage(id2)).getPerson2().getProducts().get(0) == \old(getMessage(id2));                         
      @ ensures \old(getMessage(id2)).getPerson2().getProducts().size() == \old(getMessage(id2).getPerson2().getProducts().size()) + 1;
      @also
      @public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id2);
      @signals (PersonIdNotFoundException e) !contains(id1);
      @*/
public void produce(int id1, int id2) throws MessageIdNotFoundException, PersonIdNotFoundException

心得体会:

总体来说这个单元的作业难度不高,根据JML规格就可以写出对应的实现。

出现的最多的问题就是JML太多太杂,在阅读中不仔细就会出现问题,导致强测、互测中失分。

还需要更加适应形式化语言的复杂与严谨

posted @ 2022-06-06 12:27  Satom1shihara  阅读(8)  评论(0编辑  收藏  举报