BUAA-OO第三单元总结

总述

本单元的任务是实现简单的社交网络关系的模拟和查询, 包括人与人互动、消息收发等操作。学习目标是理解JML规格在面向对象设计与构造中的重要意义,并掌握利用JML规格提高代码质量的能力。官方包已经通过JML给定了整个社交网络的基本功能规格,如何设计层次之间的交互方法甚至额外层次是本单元作业的关键。

一、基于JML规格准备测试数据

JML为测试提供了划分依据和判定依据,代码需要实现的功能规格都已给出,可以针对每个具体方法构造单独构造测试数据。依据前置条件分别构造满足不同前置条件的数据类型,通过后置条件和不变式检查结果,以及异常行为是否正确。而用Junit可以实现测试执行的自动化。

仍然需要注意构造边界数据,比如 group 中人数上限是1111。针对age相关计算、不同类型消息的收发操作构造特殊数据。

由于部分操作需要遍历,如果没有没有采用合适的数据结构或算法,所有的查询和分析都要遍历整个大图,性能会极低,可以针对此类操作如qgav、 qbs、qlc等构造大量查询的数据。

二、架构设计与图模型构建和维护策略

2.1 第一次作业

  • query_circle query_block_sum

    构架一个类 UnionFindSet 来维护并查集,采用按秩合并路径压缩优化。person 的 id 为图中的节点,用 HashMap 维护父节点图和节点秩图,通过判断两点的顶级父节点是否相同来判断两点是否联系,同时在在增加节点和合并节点维护 blocknum。实现如下:

    private final HashMap<Integer, Integer> parent; //key id - value parentId
        private final HashMap<Integer, Integer> rank; //key id - value rank
        private int blockNum;
    // add person
    public void insert(int id) {
            parent.put(id, id);
            rank.put(id, 1);
            blockNum = blockNum + 1;
    }
    // add relation
    public void union(int id1, int id2) {
            int root1 = find(id1);
            int root2 = find(id2);
            if (root1 == root2) {
                return;
            }
            if (rank.get(root1) < rank.get(root2)) {
                parent.put(root1, root2);
            } else if (rank.get(root1) > rank.get(root2)) {
                parent.put(root2, root1);
            } else {
                parent.put(root2, root1);
                int oldRank = rank.get(root1);
                rank.put(root1, oldRank + 1);
            }
            blockNum = blockNum - 1;
    }
    // isCircle
    public int find(int id) {
            if (parent.get(id) != id) {
                int newParent = find(parent.get(id));
                parent.put(id, newParent);
            }
            return parent.get(id);
    }
    

2.2 第二次作业

  • query least connection

    采用并查集优化 Kruskal 算法,在 NetWork 中的 unionFindSet 中重载 union 方法,用 HashMap<Integer, ArrayList> 维护每个 block 中的边集合和边数,求最小生成树时获取当前查询节点的顶级父节点,进一步获取对应的 block 信息,然后传给 Kruskal 方法。在 Kruskal 算法中增加边时也用并查集来判断两节点的联通性。实现如下:

    更新的 UnionFindSet:

    private final HashMap<Integer, ArrayList<Edge>> edgeBlocks; // parentId - edgeBlock
    private final HashMap<Integer, HashSet<Integer>> personBlocks; // parentId - personBlock
    
    public void union(int id1, int id2, int value) {
            int root1 = find(id1);
            int root2 = find(id2);
            Edge edge = new Edge(id1, id2, value);
            ArrayList<Edge> edgeList1;
            ArrayList<Edge> edgeList2;
            if ((edgeList1 = edgeBlocks.get(root1)) == null) {
                edgeList1 = new ArrayList<>();
            }
            if ((edgeList2 = edgeBlocks.get(root2)) == null) {
                edgeList2 = new ArrayList<>();
            }
            HashSet<Integer> personSet1 = personBlocks.get(root1);
            HashSet<Integer> personSet2 = personBlocks.get(root2);
            if (root1 == root2) {
                edgeList1.add(edge);
                edgeBlocks.put(root1, edgeList1);
                personSet1.add(id2);
                personBlocks.put(root1, personSet1);
                return;
            }
            if (rank.get(root1) < rank.get(root2)) {
                parent.put(root1, root2); // root1'parent becomes root2
                edgeList2.add(edge);
                edgeList2.addAll(edgeList1);
                edgeBlocks.put(root2, edgeList2);
                edgeBlocks.remove(root1);
                personSet2.addAll(personSet1);
                personBlocks.put(root2, personSet2);
                personBlocks.remove(root1);
            } else {
                parent.put(root2, root1); // root2'parent becomes root1
                if (rank.get(root1).intValue() == rank.get(root2).intValue()) {
                    int oldRank = rank.get(root1);
                    rank.put(root1, oldRank + 1);
                }
                edgeList1.add(edge);
                edgeList1.addAll(edgeList2);
                edgeBlocks.put(root1, edgeList1);
                edgeBlocks.remove(root2);
                personSet1.addAll(personSet2);
                personBlocks.put(root1, personSet1);
                personBlocks.remove(root2);
    
            }
            blockNum = blockNum - 1;
    }
    

    Kruskal:

    public int kruskal(Network network, ArrayList<Edge> edges, int total) {
            int sum = 0;
            int num = 0;
            UnionFindSet ufs = new UnionFindSet();
            edges.sort(Comparator.comparingInt(Edge::getWeight));
            for (Edge edge : edges) {
                if (num >= (total - 1)) {
                    return sum;
                }
                int id1 = edge.getId1();
                int id2 = edge.getId2();
                if (!ufs.contains(id1)) {
                    ufs.insert(id1);
                }
                if (!ufs.contains(id2)) {
                    ufs.insert(id2);
                }
                if (!ufs.isCircle(id1, id2)) {
                    ufs.union(id1, id2);
                    sum = sum + network.getPerson(id1).queryValue(network.getPerson(id2));
                    num = num + 1;
                }
            }
            return sum;
    }
    

2.3 第三次作业

  • 注意区分什么时候用 message 的 id,什么时候用 emojiId。

  • send indirect message

    采用堆优化的 Dijkstra 算法求最短路径,用优先队列 存放每个节点,需要注意 PriorityQueue 只有在增加或删除元素时才会重新排序,仅修改其中元素的值并不会触发排序。因为没有考虑到这一点强测 WA 了5个点。

    public int dijkstra(Person src, Person dst) {
            PriorityQueue<MyPerson> pq =
                    new PriorityQueue<>(Comparator.comparingInt(MyPerson::getPath));
            ArrayList<Integer> over = new ArrayList<>();
            HashSet<Integer> visit = new HashSet<>();
            ((MyPerson) src).setPath(0);
            pq.add((MyPerson) src);
            visit.add(src.getId());
            while (!pq.isEmpty()) {
                MyPerson p = pq.poll(); // 取出 删除 调整堆结构
                over.add(p.getId()); // 已找到最短路径
                if (p.getId() == dst.getId()) {
                    return p.getPath();
                }
                for (Person nowAcq : p.getAcquaintance().values()) {
                    if (!over.contains(nowAcq.getId())) {
                        MyPerson acq = (MyPerson) nowAcq;
                        if (visit.contains(acq.getId())) {
                            acq.setPath(Math.min(p.getPath() + p.queryValue(acq), acq.getPath()));
                        } else {
                            acq.setPath(p.getPath() + p.queryValue(acq));
                            visit.add(acq.getId());
                        }
                        pq.remove(acq);//注意pq只修改元素但是不add 它不会重新排序!!!! 
                        pq.add(acq); // 加入堆尾 调整堆
                        //也可以直接add 在for之前用over加判断条件
                    }
                }
      
            }
            return -1;
    }
    
  • 简便的删除操作

    deleteColdEmoji

    emojiIdHeat.values().removeIf(heat -> (heat < limit));
    messages.values().removeIf(message -> ((message instanceof EmojiMessage) && (!containsEmojiId(((EmojiMessage)message).getEmojiId()))));
    

    clearNotices

    getPerson(personId).getMessages().removeIf(message -> (message instanceof NoticeMessage));
    

三、性能问题和修复情况

  • query_group_age_var

    如果每次查询时都计算可能会超时。在group 中 add person、del person 时 维护 ageSum、ageSqrSum,那么在每次查询时就无序遍历。

    ageMean = ageSum / people.size()

    ageVar = (ageSqrSum - 2 * ageMean * ageSum + people.size() * ageMean * ageMean) / people.size()

    需要注意如果直接由数学公式推导,ageSum 可以替换成 people.size() * ageMean,但这样会因为精度出现bug。

  • query_group_value_sum query_group_people_sum

    与 age 操作类似,但除了在group 中 add person、del person 时只需要一层循环遍历维护 valueSum 外,add relation 时也需判断是否需要更新 group 的 valueSum。

  • 算法的性能,采用路径压缩并查集和堆优化的Dijkstra。

四、NetWork 的扩展

4.1 要求

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告
  • Producer:产品生产商,通过Advertiser来销售产品
  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)。

4.2 规定

为了扩展NetWork,做出如下规定:

  • 新增三个 Person 的子类为Advertiser、Producer、Customer,新增 Product 类,新增 ProductNotFoundException、EqualProductIdException、NotProducerException 异常类。
  • Product 有 price 属性,有唯一的 id 属性。Advertiser 有一个 sales 属性,表示总销售额,有 experience 属性,表示经验值。
  • 每个 Product 有一个唯一的id,Producer 可以生产 Product,之后可以向 Advertiser 发送ProductMessage,之后 Advertiser 可以向 Customer 推销产品,推销成功后,Advertiser 的经验值会加1。Customer 收到推销之后,只有 money 大于 Product 的 price 才会购买该 Product。

为满足新规定,Network 新增:

/*@ public instance model non_null Product[] products;
  @*/

//@ ensures \result == (\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
public /*@ pure @*/ boolean containsProduct(int id);

/*@ public normal_behavior
  @ requires containsProduct(id);
  @ ensures (\exists int i; 0 <= i && i < products.length; products[i].getId() == id &&
  @         \result == products[i]);
  @ also
  @ public normal_behavior
  @ requires !containsProduct(id);
  @ ensures \result == null;
  @*/
public /*@ pure @*/ Product getProduct(int id);

4.3 实现

  • 功能1:生产商品

    /*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < people.length; products[i].equals(product));
      @ assignable products;
      @ assignable ((Producer)getPerson(product.getProducerId())).getProducts();
      @ 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);
      @ ensures ((Producer)getPerson(product.getProducerId)).getProductsNum() = \old(((Producer)getPerson(product.getProducerId())).getProductsNum()) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(((Producer)getPerson(product.getProducerId())).getProductsNum()); 
      \exists int j; 0 <= j && j < ((Producer)getPerson(product.getProducerId())).getProductsNum(); ((Producer)getPerson(product.getProducerId())).getProducts().get(j).equals(\old(((Producer)getPerson(product.getProducerId())).getProducts().get(i))));
      @ ensures (\exists int i; 0 <= i && i < ((Producer)getPerson(product.getProducerId())).getProductsNum(); ((Producer)getPerson(product.getProducerId())).getProducts().get(i).equals(product));
      @ also
      @ public exceptional_behavior
      @ signals (EqualProductIdException e) containsProduct(product.getId());
      @ signals (PersonIdNotFoundException e) (!containsProduct(product.getId()) && !contains(product.getProducerId()));
      @*/
    public void produce(Product product) throws EqualProductIdException, PersonIdNotFoundException;
    
  • 功能2:向顾客推销产品

    /*@ public normal_behavior
      @ requires containsProduct(productId) && contains(adId) && contains(customerId);
      @ assignable getPerson(customerId).money, ((Producer)getPerson(getProduct(productId).getProducerId())).sales, ((Advertiser)getPerson(adId)).exp;
      @ ensures (getPerson(customerId).getMoney() >= getProduct(productId).getPrice()) ==> ((getPerson(customerId).getMoney() == \old(getPerson(customerId).getMoney()) - getProduct(productId).getPrice()) && (((Producer)getPerson(getProduct(productId).getProducerId())).getSales() = \old(((Producer)getPerson(getProduct(productId).getProducerId())).getSales()) + getProduct(productId).getPrice()) && (((Advertiser)getPerson(adId)).getExp() == \old(((Advertiser)getPerson(adId)).getExp()) + 1));
      @ also
      @ public exceptional_behavior
      @ signals (ProductNotFoundException e) !containsProduct(productId);
      @ signals (PersonIdNotFoundException e) containsProduct(productId) && (!contains(adId) || !contains(customerId));
      @*/
    public void promote(int productId, int adId, int customerId) throws ProductNotFoundException, PersonIdNotFoundException;
    
  • 功能3:查询某个 id 的 Producer 的销售额

    /*@ public normal_behavior
      @ requires contains(id) && (getPerson(id) instanceof Producer);
      @ ensures /result == getPerson(id).getSales();
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id);
      @ signals (NotProducerException e) (contains(id) && (!(getPerson(id) instanceof Producer)));
      @*/
    public /*@ pure @*/ int querySales(int id) throws PersonIdNotFoundException, NotProducerException;       
    

五、心得体会

总体来说,本单元作业没有前两个单元难度大。所有方法的具体操作都由JML明确规定,但同时需要我们严格遵守规格。jml 作为一种接口规格语言,可以准确定义和表示方法行为的正确性,提供了一种通过逻辑来检查代码的方式。

通过本单元的练习,我学会了jml的方法规格表示和基于规格的测试方法,以及类型层次下的规格分析设计,体会到了jml的优势。但在阅读一些比较复杂的jml描述时需要花很长时间理解,比如对qlc、qbs等方法的描述,面对复杂的jml时,可以通过画图、转换自然语言等加快理解。

此外,在作业中巩固了许多图论算法及其优化。

posted @ 2022-06-05 18:52  wwllll  阅读(16)  评论(0编辑  收藏  举报