面向对象设计与构造第三单元总结

作业分析

题目简述

根据给出的 JML 规格实现并维护一个社交网络的模型,包括人、群组、网络和各种消息,还有各种异常。

涉及图论模型,并查集、最短路和最小生成树算法。

架构设计

按要求实现几个类,每个类里面都使用 HashMap 建立编号到对象的映射,方便查找。

大致如下:

//MyNetwork
    private final HashMap<Integer, Person> people;
    private final HashMap<Integer, Group> groups;
    private final HashMap<Integer, Message> messages;
    private final HashMap<Integer, Integer> emojiHeatMap;

//MyGroup
    private final HashMap<Integer, Person> people;

//MyPerson
    private final HashMap<Integer, Person> acquaintances;
    private final HashMap<Integer, Integer> values;

//My____Exception
    private static HashMap<Integer, Integer> idCnt;

另外实现了并查集、最小生成树、最短路三个工具类,以及两个辅助类:存储边的类和最短路时用于在堆里实现比较的 Node 类。

图模型构建和维护策略

这次的社交网络很明显是一个图的模型,是有边权的无向图。每个人可以看做结点,人和人的关系可以看做边,群组就是整个网络的子图。消息则是在网络里传递信息,和图关系不大。

边显然是稀疏的,因此我采用邻接表(用 ArrayList 实现)来存储边。加入一条边的时候,就存到两个点的邻接表中。

对于群组,只需要记录所有点即可,没有必要记录边。

对于消息,加入的时候存在 Network 的 HashMap 中,发送的时候维护 SocialValue,转移到对应的人的 ArrayList 里。

对图的维护基本按照 JML 来实现即可。特殊实现在下面的“性能问题”中提到。

性能问题及优化

作业中的数据范围不算大,但也不算很小,是 \(10^3-10^4\) 级别的。考虑到 Java 的运行速度和时间限制,能支持的最大复杂度为 \(O(n^2)\)。(不过测试中有些指令的次数有限制,可以适当放宽)

因此并不能每个操作都按 JML 暴力遍历或搜索来解决,有几个地方需要用数据结构和算法来优化。

1、并查集

在第九次作业中,出现了 query_circle 和 query_block_sum 两种指令。query_circle 就是询问两个点是否联通。query_block_sum 是询问图里的连通块数量。

考虑到题目中没有删边只有加边,这是很经典的并查集模型,只需要在加边的同时维护一下并查集即可。题目也没有特殊要求,因此没有必要按秩合并,可以直接路径压缩。总复杂度为 \(O(n \alpha (n))\)

query_circle 就是看两个点是否在同一并查集中。

在维护并查集的过程中还可以顺便维护 block_sum,加点的时候 +1,合并的时候 -1。这样可以 \(O(1)\) 回答 query_block_sum。

2、最小生成树

在第十次作业中,出现了 query_least_connection 指令。这个指令的含义是询问图中包含某个点的最小生成树的边权和。

在前面实现了并查集的基础上,可以直接实现 Kruskal 算法来维护最小生成树。由于加边是不断进行的,边集发生了变化,因此每次询问必须重新做一遍最小生成树。

Kruskal 需要对边排序。每次都做一遍快排不如加边的时候直接插入排序。

过程中需要维护每个连通块当前的边权和,这也可以在并查集中维护,把每个连通块的边权和存在并查集顶部的点即可,合并的时候就把两边加起来,再加上新加的边,存到新的顶部的点上。

排序总复杂度 \(O(n^2)\),qlc 单次复杂度 \(O(n\alpha(n))\)

3、最短路

第十一次作业中出现了 send_indirect_message 指令,需要求消息的双方的最短路。

直接上堆优化 Dijkstra 即可。单次复杂度 \(O(n \log n)\)

4、query_group_value_sum

query_group_value_sum 这个指令一开始没有什么特别的,但是在第十次作业数据范围增大之后就需要优化了。含义是询问群组代表的子图的边权和。

现在不能暴力枚举两个点看是否有边来计算。一种方法是询问时枚举所有边,看是否两端都在子图里。另一种方法是加边的时候枚举所有群组,以及往群组里加点的时候枚举它的所有边,看是否会产生贡献,并对于每个群组实时维护 valueSum。

两种方法总复杂度都是 \(O(n^2)\)

Bug分析

三次作业均顺利通过强测和互测。找到同学的 bug 也都集中在上面提到的性能问题上。

有的同学觉得打标记延迟更新可以变快,实际上只要修改和查询交替出现还是会很慢。

有个唯一的坑点,是往群组里加人的时候上限为 1111,但这个 bug 没有出现在我互测房间中的任何一份代码中。

测试数据

手动测试

需要仔细阅读 JML 规格,找到一些可能注意不到导致出错的地方。比如群组人数 1111 的上限,getReceivedMessages 是返回最近四条等。

还要对 JML 中涉及的每个方法的每个异常进行充分的测试,保证都能够正确触发。

此外就是一些性能方面的问题,针对这几个容易出现性能问题的指令来有针对性的构造数据。比如造链,完全图,菊花图……

自动测试

大力随机即可。可以有针对性地控制每个指令的数量,全面测试。

测试异常可以让询问的随机范围略大于编号范围。

Network 扩展

题目描述

假设出现了几种不同的Person

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

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

Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买

  • 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息

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

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等

请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

扩展

Advertiser、Producer 和 Customer 可以设计为 Person 的子类,Advertisement 和 BuyMessage 可以设计为 Message 的子类。

发送广告:

    /*@ public normal_behavior
      @ requires containsMessage(id) && (getMessage(id) instanceof Advertisement);
      @ 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 && !getMessage(id).getPerson1().isLinked(people[i]);
      @           people[i].getMessages().equals(\old(people[i].getMessages()));
      @ ensures (\forall int i; 0 <= i && i < people.length && getMessage(id).getPerson1().isLinked(people[i]);
      @           (\forall int j; 0 <= j && j < \old(people[i].getMessages().size());
      @             people[i].getMessages().get(j+1) == \old(people[i].getMessages().get(j))) &&
      @           people[i].getMessages().get(0).equals(\old(getMessage(id))) &&
      @           people[i].getMessages().size() == \old(people[i].getMessages().size()) + 1);
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id);
      @ signals (NotAdvertisementException e) !(getMessage(id) instanceof Advertisement);
      @*/
    public void sendAdvertisement(int id) throws
            MessageIdNotFoundException, NotAdvertisementException;

生产商生产产品:

    /*@ public normal_behavior
      @ requires contains(producerId) && (getPerson(producerId) instanceof Producer);
      @ assignable getProducer(producerId).productCount;
      @ ensures getProducer(producerId).getProductCount(productId) ==
      @           \old(getProducer(producerId).getProductCount(productId)) + 1;
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(producerId);
      @ signals (NotProducerException e) !(getPerson(producerId) instanceof Producer);
      @*/
    public void produceProduct(int producerId, int productId) throws
            PersonIdNotFoundException, NotProducerException;

发送购买消息(由于要经过 Advertiser,所以分为发送和接收两部分,分别代表 Customer-Advertiser/Advertiser-Producer,先付钱再发货):

    /*@ public normal_behavior
      @ requires containsMessage(id) && (getMessage(id) instanceof BuyMessage);
      @ requires (getMessage(id).getPerson1() instanceof Customer) && (getMessage(id).getPerson2() instanceof Advertiser);
      @ assignable messages;
      @ assignable getMessage(id).getPerson1().money;
      @ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().money;
      @ 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 < \old(getMessage(id).getPerson2().getMessages().size());
      @          \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
      @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
      @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
      @ ensures (\old(getMessage(id)).getPerson1().getMoney() ==
      @         \old(getMessage(id).getPerson1().getMoney()) - ((BuyMessage)\old(getMessage(id))).getMoney() &&
      @         \old(getMessage(id)).getPerson2().getMoney() ==
      @         \old(getMessage(id).getPerson2().getMoney()) + ((BuyMessage)\old(getMessage(id))).getMoney());
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id);
      @ signals (NotBuyMessageException e) !(getMessage(id) instanceof BuyMessage);
      @ signals (NotCustomerException e) !(getMessage(id).getPerson1() instanceof Customer);
      @ signals (NotAdvertiserException e) !(getMessage(id).getPerson2() instanceof Advertiser);
      @*/
    public void sendBuyMessage(int id) throws
            MessageIdNotFoundException, NotBuyMessageException, NotCustomerException, NotAdvertiserException;

学习体会

本单元的难度并不大,主要是让我们了解规格化设计,熟悉基于 JML 的规格模式。

规格化设计是一种致力于保证程序正确性的方法,十分的严谨。我觉得我们在自己设计程序的时候也可以学着去使用规格化设计,能够提高思维的严谨性、降低出错的概率。

当然,代码并不能完全等价于规格,代码的核心在于架构设计,数据、容器等也需要自主选择。不过有了规格之后,能够对代码形成一定约束,提醒自己需要注意哪些地方。

这几次作业也带我们回顾了大一程设和数据结构里学到的一些知识,涉及了时间复杂度的计算。这让我更加体会到,在给定的规格下,内部的实现可以是多种多样的。

posted @ 2022-05-31 20:38  Oshwiciqwq  阅读(84)  评论(0编辑  收藏  举报