BUAA_OO_Unit3_总结

一、JML及数据构造

在本单元的学习中,我们首次接触到了JML语言 (Java Modeling Language),即java建模语言。它以一种数学语言的方式,明确了所需求的规格,消除了自然语言的歧义性。它通过前置条件、后置条件、作用范围等来对每个所需要的方法进行限制和描述,以一种不变的格式和状态约束形式来对每一个类进行了特定的规格化描述,使需求者和程序员之间得到充分而有效的沟通。

在本次的训练中,我们依照提供的JML规格编写代码,需要同时满足在normal_behavior情况下完成题目的要求,以及在exceptional_behavior的情况下抛出对应的异常。一方面,题目的思维难度因为给定的模式而大大降低,甚至逐句翻译JML语言也能在强测中取得不错的成绩(应该?),但另一方面,各种细节层出不穷(异常的先后顺序,群组人数存在上限,id可以为负……),这给测试数据的生成提供了相当大的挑战,从保证正确性的覆盖性测试,到高压力满数据情况下的定点爆破,所有的可能出现的数据都应该得到充分满足,才能充分保证程序的正确性和效率。

在这个单元的学习中,由于身边的同学很多都写了评测机,我就直接搭了顺风车(抱大腿),只是去尽可能挑选一些复杂度相对较高的函数构造了一些针对性的数据(课下确实帮到了部分同学,互测也产生了一定效果),总体而言,这个单元的代码压力相对来说不大,算的上是一个比较轻松愉悦的单元。

二、架构设计

这一部分,主要想谈一谈一些方法的实现方式,以及相关的复杂度分析.

1.atg,dfg与qgvs

(1)大多数人的实现方式是在atg和dfg时将对应的人加入到对应的group中及从对应的group中删除,在qgvs时将对应的组中每一个人的边集进行一次遍历,从中挑选出在组中的边(有很多直接按JML规格写的直接双层循环遍历组中人直接t了)。

这样,每次atg,dfg的复杂度是\(O(1)\)的,而qgvs在最坏情况下会便利整个边集,因此qgvs的复杂度是\(O(n)\)的,事实上,在10s时限,\(10^4\)条数据的情况下总体复杂度\(O(n^2)\)是完全可以接受的。

(2)我自己当时的实现方式则是正好反过来,在atg和dfg时将产生/失去贡献的边的价值加入对应的group,在qgvs时直接静态输出价值结果。在这种情况下,每次atg,dfg在最坏情况下会便利整个边集,复杂度是\(O(n)\)的,而qgvs的复杂度是\(O(1)\)的,总体复杂度和上述的方法相同。

(3)我们可以注意到,第一种方案和第二种方案都是存在一个单次操作复杂度\(O(n)\)的函数和单次操作复杂度\(O(1)\)的函数,那么,我们是否可以构想(口胡)一个总体复杂度更低的算法呢?

以下是一种似乎可行的方案(不完善的地方可以找我继续讨论):

1.把每个人看作点,并按照他所连接的边的多少是否超过\(\sqrt n\) 将没跟人分为大点和小点(这样,大点的总数不超过\(\sqrt n\)而每个小点所连接的人数不超过\(\sqrt n\))。

2.对每个大点而言,我们维护它对每个group贡献的权值和(由于group是常数级别的),当他加入一个group时,直接将对应的权值和加入到对应的group中,当从group中删除时,直接减去对应的权值和。

3.对每个小点而言,小点贡献的价值直接统计在每个组中(后续可以证明这是可行的),当小点加入组中/从组中删除时,由于小点所连的边数是不超过\(\sqrt n\)的,因此单次最高复杂度是\(O(\sqrt n)\)

4.考虑每个加边操作,如果增加边的点

①大点-大点:先互相加入边集,直接更改二者维护的group权值和,由于由于group是常数级别的,因此操作也是常数级别的。

②大点-小点,小点-小点:大点的操作与①相同,对小点而言,如果该操作结束后小点仍然是小点,则直接加入小点的边集中,并计算group贡献(这步的最差复杂度为\(\sqrt n\)*组数);如果在当次操作结束后,小点的边数达到了\(\sqrt n\),小点变为大点,回退他所有边的贡献,并记录到对应的权值和中。

总体复杂度:如果将组数的大小视为常数,那么该组操作的复杂度上限为\(O(n\sqrt n)\);否则若将组数设为m,则通过调节区分大小点块的大小可以把复杂度降为\(O(n\sqrt {nm})\),这都远远小于之前的\(O(n^2)\)复杂度上限。

意义:如果对Unit3第一次作业进行具体分析,我们不难发现,复杂度能够达到单次\(O(n)\)的仅有这对操作中的一个,其他操作化简到单次\(O(1)\)并不困难,也就是说,我们把该次作业的复杂度上限从\(O(n^2)\)降低到了\(O(n\sqrt n)\),意义不能说不大。事实上,如果在第二次作业中,如果考虑动态最小生成树的单次操作复杂度仅有\(O(log{n})\),即使不限定最小生成树操作的次数,第二次作业作业的复杂度上限也为\(O(n\sqrt n)\)。(所以快去把n的范围调成\(10^5\)(bushi)

2.qlc

很经典的最小生成树,考虑到写完prim后可以稍改改就是kruskal可以直接用(还不用全局维护一个边集),写起来代码也并不长,所以就用prim了。算法原理大概就是每次选出已选点集合(初始只有起点)和为选点集合之间的最短边,因此需要上一个堆优化(PriorityQueue),最终复杂度\(O(nlogn)\)

具体代码如下:

public int queryLeastConnection(int id) throws PersonIdNotFoundException {
        if (!contains(id)) {
            throw new MyPersonIdNotFoundException(id);
        } else {
            int ans = 0;
            PriorityQueue<Relation> q = new PriorityQueue<>();
            HashMap<Integer, Integer> vis = new HashMap<>();
            vis.put(id, 0);
            HashMap<Integer, Integer> edges = ((MyPerson) getPerson(id)).getEdges();
            for (int x : edges.keySet()) {
                if (!vis.containsKey(x)) {
                    q.add(new Relation(x, edges.get(x)));
                }
            }
            while (!q.isEmpty()) {
                while (!q.isEmpty() && vis.containsKey(q.peek().getPoint())) {
                    q.poll();
                }
                if (q.isEmpty()) {
                    return ans;
                }
                Relation now = q.poll();
                ans += now.getValue();
                vis.put(now.getPoint(), 0);
                edges = ((MyPerson) getPerson(now.getPoint())).getEdges();
                for (int x : edges.keySet()) {
                    if (!vis.containsKey(x)) {
                        q.add(new Relation(x, edges.get(x)));
                    }
                }
            }
            if (q.isEmpty()) {
                return ans;
            }
        }
        return 0;
    }

3.qci

判断两个点是否在同一个连通块内,爆搜的复杂度是\(O(n)\),可以过,但是用并查集优化一波可以优化到很低的复杂度,而且还可以顺便把qbs预处理掉,这东西原理感觉没什么可说的,复杂度上限\(O(logn)\)但其实远远达不到。

具体实现如下:

public int find(int x) {
        ArrayList<Integer> a = new ArrayList<>();
        int xx = x;
        while (fa.get(xx) != xx) {
            //System.out.println(xx);
            a.add(xx);
            xx = fa.get(xx);
        }
        for (int y : a) {
            fa.put(y, xx);
        }
        return xx;
    }
    public boolean isCircle(int id1, int id2) throws PersonIdNotFoundException {
        if (!contains(id1)) {
            throw new MyPersonIdNotFoundException(id1);
        } else if (!contains(id2)) {
            throw new MyPersonIdNotFoundException(id2);
        } else {
            return find(id1) == find(id2);
        }
    }

由于不知道java是怎样处理栈的,怕爆栈并查集就写成数组版的了。

4.sim

JML好几页,翻译过来就是求一个最短路。。。

kruskal上一个堆优化,基本和上面的prim差不多,原理就是利用没有负边我拿从起点到该点距离最小的点去更新其他点肯定不会出问题,这样一直更新到终点到了堆顶就求出最短路了,不知道有没有人写SPFA被卡T(感觉没人写也没构造数据去卡)最终复杂度\(O(nlogn)\)

最终实现的代码如下:

public int sendIndirectMessage(int id) throws MessageIdNotFoundException {
        if (!containsMessage(id) || (containsMessage(id) && getMessage(id).getType() == 1)) {
            throw new MyMessageIdNotFoundException(id);
        }
        int id1 = getMessage(id).getPerson1().getId();
        int id2 = getMessage(id).getPerson2().getId();
        try {
            if (!isCircle(id1, id2)) {
                return -1;
            }
        } catch (PersonIdNotFoundException e) {
            e.printStackTrace();
        }
        sendMessage1(id);
        PriorityQueue<Relation> q = new PriorityQueue<>();
        HashMap<Integer, Integer> vis = new HashMap<>();
        HashMap<Integer, Integer> dis = new HashMap<>();
        q.add(new Relation(id1, 0));
        dis.put(id1, 0);
        while (true) {
            while (!q.isEmpty() && vis.containsKey(q.peek().getPoint())) {
                q.poll();
            }
            Relation now = q.poll();
            int x = now.getPoint();
            int y = now.getValue();
            if (x == id2) {
                return y;
            }
            if (vis.containsKey(x)) {
                continue;
            }
            vis.put(x, 0);
            HashMap<Integer, Integer> edges = ((MyPerson) getPerson(x)).getEdges();
            for (int to : edges.keySet()) {
                if (!dis.containsKey(to)) {
                    dis.put(to, 1000000000);
                }
                if (dis.get(to) > dis.get(x) + edges.get(to)) {
                    dis.put(to, dis.get(x) + edges.get(to));
                    q.add(new Relation(to, dis.get(to)));
                }
            }
        }
    }

注意别写最短路写嗨了忘把sendMessege加上

5.其他

其他能做优化的地方也还相当多,这里就不展开细说了,还相对有一点印象的有:

qgav可以通过预处理\(\sum{val}\),及\(\sum {val}^2\)实现单次的\(O(1)\)查询;

dce可以通过一个TreeSet实现单次\(O(logn)\)复杂度的操作。

三、bug及其修复

第九次作业

自己强测和互测均未出现bug;

在互测中卡掉了qgvs按JML规格写的直接双层循环遍历组中人的代码;

第十次作业

强测未出现bug,互测中并查集被卡掉了(具体原因是没有看到id可以为负数初始的fatherId都设成了0,然后要是某个人的id是0就大寄特寄了)。

互测未找到其他人的bug。

第十一次作业

强测和互测未出现bug。

互测未找到bug。

四、Network拓展

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告

  • Producer:产品生产商,通过Advertiser来销售产品

  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息

  • Person:吃瓜群众,不发广告,不买东西,不卖东西

    购买商品

    /*@ public normal_behavior
      @ assignable getPerson(personId1).money,  getTrader(personId2).getProduct(productId)
      @ requires contains(personId1) && contains(personId2);
      @ requires containsProduct(productId);
      @ requires containsTrader(personId2);
      @ ensures getPerson(personId1).money = \old(getPerson(personId1).money) - getProduct(productId).getValue;
      @ ensures getTrader(personId2).getProduct(productId) = \old(getTrader(personId2).getProduct(productId)) - 1;
      @ also
      @ public exceptional_behavior
      @ signals (PeronIdNotFoundException) !contains(personId1);
      @ also
      @ public exceptional_behavior
      @ signals (PeronIdNotFoundException) !contains(personId2);
      @ also
      @ public exceptional_behavior
      @ signals (TraderIdNotFoundException) !containsTrader(personId2);
      @ also
      @ public exceptional_behavior
      @ signals (ProductIdNotFoundException) !containsProduct(productId);
      @*/
    public void purchaseProduct(int personId1, int productId, int personId2);
    

    增加商品

    /*  @ public normal_behavior
        @ requires (\exists int i; 0 <= i && i < list.length; list[i] == product)
        @ assignable nothing
        @ ensures \result = (\sum int i; 0 <= i && i < people.length;
    	@ (\sum int j; 0 <= j && j < people[i].poccess && people[i].poccess == product; 1));
        @ also
        @ public exceptional_behavior
        @ signals (ProductNotFoundException e) !(\exists int i; 0 <= i && i < list.length; list[i] == product)
        @*/
        public int tradeNumber(/*@ non_null @*/ Product product) throws ProductNotFoundException
    

    发送广告

    /*@ public normal_behavior
          @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) &&
          @ message.getType() == ADVERTISE && (message.getPerson1() instanceof Advertiser);
          @ assignable messages;
          @ ensures (\forall int i; 0 <= i && i < \old(messages.length);
          @          (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
          @ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message));
          @ ensures messages.length == \old(messages.length) + 1;
    	  @ also
          @ public exceptional_behavior
          @ signals (AdvertiserNotFoundException e) 
          @ !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) 
          @ && !(messages[i].getPerson1() instanceof Advertiser);
          @*/
    public void addAdvertisement(/*@ non_null @*/Message message) throws AdvertiserNotFoundException;
    

五、体会和感想

在本单元的JML学习中,从一开始的很难看懂,难以理解到最后能够明白他所讲述的规格,所依存的逻辑。在看来,对于JML语言的应用应该辩证地看待:一方面,在一些重要的工作场合用JML语言可以有效避免自然语言的二义性,以避免带来工程上的巨大损失;但另一方面,我们也不难发现JML语言的繁琐,以本次作业中的最短路为例,最短路的JML描述比最短路的代码还要长,那为什么不用同样没有二义性的代码呢?因此,我们也能看到一些JML表现出的不足,但总体而言,这是一种我们学习java的强有力的工具。

在本单元的评测和互测中,尽管和同学进行了大量的对拍,但是在第二次作业中还是出现了一定的小bug,这说明我自己的细心程度仍然有待提高,同时,也说明了自己手造的评测数据不够强,在今后的测试中要重点加以改进。

总体而言,这个单元是一个相对轻松的单元,码量和思维难度与前两个单元相比都有所下降,但对细心程度的要求更高,对数据构造能力的要求更高,在这个单元的学习中,我充分认识到了自己的不足,也渐渐理解了面向对象编程的重要意义。

posted @ 2022-06-03 01:58  locnxe  阅读(66)  评论(3编辑  收藏  举报
站长工具: