#BUAA-面向对象设计与构造 ——第三单元总结#

BUAA-面向对象设计与构造

——第三单元总结(JML)

单元主题

完成简单社交网络关系的模拟,并实现一些信息的查询,待完成的函数都已经通过JML规格给出,完全按照规格中的写法进行函数编写可以完全保证正确性,但于时间效率方面会出现问题。

架构设计

这个单元的架构关键在于建立图模型并实现动态图的维护,涉及了许多图论部分的知识,在其中比较重要的概念是——极大连通分量,整个网络可以看作是极大联通分量的集合,同时许多操作的查询和实现都基于此,如最短路径,最小生成树的查询。

本单元的图的构建具有动态性,整个社交网络是从空开始的,通过指令不断向其中增加结点,再在节点间增加关系,最后逐步形成网络,而且没有从网络中删除结点的操作。所以对整个图的维护只基于下面两个操作:

  • 添加节点:addPerson

  • 添加关系:addRelation

对于极大连通分量的处理,我没有采用并查集的策略,而是新建了一个类graph,用以指代一个极大联通分量,同时graph还成为了一个工具类,上文提到的最短路径和最小生成树的查询操作都封装在了这个类中,提高了扩展性,同时做到了解耦合。

  • 当向图中仅仅增加一个结点(addPerson操作)时,就会创造一个新的联通分量,此时只需要新建一个graph对象即可。所以判断两个person是否在同一联通分量中就转化为判断他们是否在同一graph对象中,免去了照搬规格中二重循环的写法,减少了时间复杂度。

  • 另一方面,当为结点添加关系时,首先判断两个结点是否已经在同一个graph对象中了,如果成立,那么只需为两个结点添加关系即可;反之,采用基于重量的方式将两个节点的graph对象合并(将包含节点数目少的graph对象添加到多的对象中。

有关代码如下:

 public void addPerson(Person person) throws EqualPersonIdException {
        if (contains(person.getId())) {
            throw new MyEqualPersonIdException(person.getId());
        } else {
            (this.peopleHash).put(person.getId(), (MyPerson) person);
            Graph graph = new Graph(person.getId(), this.graphCounter);
            this.graphs.add(graph);
            this.graphCounter++;
            this.qbs++;
        }
    }
public void addRelation(int id1, int id2, int value) throws
            PersonIdNotFoundException, EqualRelationException {
        if (contains(id1) && contains(id2) &&
                !(getPerson(id1).isLinked(getPerson(id2)))) {      
            Graph graph1 = queryGraph(id1);
            Graph graph2 = queryGraph(id2);
            Graph graphDes = graph1;
            if (!graph1.equals(graph2)) {
                this.qbs--;
                /* 不是一个联通分量的关系才需要更新 */
                int size1 = graph1.getSize();
                int size2 = graph2.getSize();
                /* 基于重量的合并 */
                if (size1 < size2) {
                    graph2.unionGraph(graph1);
                    graphDes = graph2;
                    this.graphs.remove(graph1);
                } else {
                    graph1.unionGraph(graph2);
                    this.graphs.remove(graph2);
                }
            }
          .......

 

性能问题和修复情况

粗略计算了一下cpu的时间限制,如果能够保证在作业中没有出现O(N^2)的代码,应该就不会出现TLE问题

复杂度较高的几个操作是:

  • 查询联通分量的数目query_block_sum

    若照搬规格,复杂度为O(N^2)

    /*@ ensures \result ==
          @         (\sum int i; 0 <= i && i < people.length &&
          @         (\forall int j; 0 <= j && j < i; !isCircle(people[i].getId(), people[j].getId()));
          @         1);
          @*/
    • 如果采用上述方法,维护一个qbs变量,那么复杂度变为O(1)

  • 查询两个person是否在同一个联通分量中isCircle

    • 采用graph对象的比较,复杂度将为O(1)

  • 查询某个群组的价值总和queryGroupValueSum

    • 得益于图的逐步建构,因此在每个group中维护一个valueSum值即可,当增加Person和删除Person时,对其进行相应改变,查询操作变为O(1)

      public void addPerson(Person person) {
              for (MyPerson myPerson : this.people) {
                  if (myPerson.isLinked(person)) {
                      this.valueSum += 2 * myPerson.queryValue(person);
                  }
              }
      .....
      }
    • 同时,关系具有自反性和对称性,因此算一半的就好。

  • 最小生成树算法

    堆可以用优先级队列PriorityQueue进行实现,容器中存储的类需要实现Comparable接口。

    提供了poll()offer()方法。

    但需要注意的是:PriorityQueue用于寻找最小的元素很方便,却不能直接用于排序

    会出现即使容器内的元素相同,但调用toString()方法输出的列表却存在差异的情况

    • Kruskal算法

      核心是对边进行操作

      • 按照边的权重建立最小堆

      • 取出最小堆堆顶数据,并判断两端节点是否在同一集合

      • 如不在,则将这两个节点添加到同一集合,接着将边加入生成边,如在,则不进行操作,为无效边

      • 重复上面的操作,直到所有的边都检查完

    • Prim算法

      核心是对结点进行操作

      • 选定起始结点,并将与该节点相连的所有边建立成堆

      • 从堆中取最小的边,然后判断to节点是否被访问过,如果没有,将这个边加入生成树,并标记该节点访问。

      • 然后将to节点所相连的边添加到最小堆中。

      • 循环上面的操作,直到所有的节点遍历完。

      public int solveQlc(int fromId) {
          // notChange用于标识该联通分量自上一次查询后是否添加过元素或者新的关系
              if (!notChange) {
                  weightSum = 0;
                  // 权重和,也就是qlc的值
                  // 核心数据结构,存储边的优先级队列
                  // 也就是到还没有连接上最小生成树上的结点的边
                  PriorityQueue<Edge> pq = new PriorityQueue<>();
                  // 用于标志某个结点是否已经加入了最小生成树中
                  HashMap<Integer, Boolean> in = new HashMap<>();
                  for (Integer integer : graph.keySet()) {
                      in.put(integer, false);
                  }
                  in.put(fromId, true);
                  nodeIn(fromId, in, pq);
                  while (!pq.isEmpty()) {
                      Edge edge = pq.poll();
                      int toId = edge.getToId();
                      if (in.get(toId)) {
                          // 节点 to 已经在最小生成树中,跳过
                          // 否则这条边会产生环
                          continue;
                      }
                      // 将边 edge 加入最小生成树
                      weightSum += edge.getValue();
                      in.put(toId, true);
                      nodeIn(toId, in, pq);
                  }
                  notChange = true;
              }
              return weightSum;
          }
      ​
          public void nodeIn(int nodeId, HashMap<Integer, Boolean> in, PriorityQueue<Edge> pq) {
              ArrayList<Edge> oneNode = graph.get(nodeId);
              // 加入与这个结点相连的所有边
              for (Edge edge : oneNode) {
                  if (in.get(edge.getToId())) {
                      continue;
                  }
                  pq.offer(edge);
              }
          }
  • 最短路径算法:

    主要包含两个集合:已加入路径的节点集合S未加入路径的节点集合U

    • 确认起始结点s,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为”起点s到该顶点的距离

    • 从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时,从U中移除顶点k。

    • 更新U中各个顶点到起点s的距离。

    在最后一次作业的最短路径算法中出现了二重循环,导致复杂度变成了O(V*E),如果采用优先级队列进行优化,可将复杂度降为O(V*logE)

    public int solveSim(int fromId, int toId) {
            HashMap<Integer, Integer> dis = new HashMap<>(); // 到连通分量每个结点的距离:结果
            PriorityQueue<Edge> heap = new PriorityQueue<>();
            for (Integer integer : this.graph.keySet()) {
                dis.put(integer, Integer.MAX_VALUE);
            }
            dis.put(fromId, 0); // 初始设置自己到自己是0
            heap.add(new Edge(fromId, 0));
            ArrayList<Integer> in = new ArrayList<>(); // 标志结点是否添加进了结点集
            while (!heap.isEmpty()) {
                Edge edge = heap.poll();
                assert edge != null;
                int now = edge.getToId();
                int baseValue = edge.getValue();
                if (in.contains(now)) {
                    continue;
                }
                in.add(now);
                if (in.contains(toId)) {
                    break;
                }
                ArrayList<Edge> oneNode = graph.get(now);
                for (Edge edge1 : oneNode) {
                    int to = edge1.getToId();
                    int newValue = edge1.getValue() + baseValue;
                    if (dis.get(to) > newValue) {
                        dis.put(to, newValue);
                        heap.add(new Edge(to, newValue));
                    }
                }
            }
            return dis.get(toId);
        }

 

测试

依然采用了手搓数据的方式,测试当满足JML规格的前置条件时,是否能出现相应的后置条件。此处应该要保证覆盖所有的前置条件。

对于后置条件的正确性判定则相对比较困难:

  • 如果是抛出异常行为的,看是否有相应异常抛出即可

    • 需要注意一些细节,比如id按照大小进行排序是否实现等等

  • 如果是给出了明确的可直接实现的for循环或其他后置条件,但又和自身函数实现有差异的情况,可以运用Junit插件(有点类似计组的Testbench,方便测试,可以不用考虑输入接口进行特定对象的构造),在插件里直接翻译JML规格,然后与实际函数运行结果进行比较,正确性可以得到保证。

    • Junit

      • 会针对相应的代码文件生成与内部函数相对应的测试模块@Test

      • 起始和结尾会有@Before以及@After,在这两部分填写的内容会在每次调用一个测试模块的前后运行

    • 举个栗子

       /*@ public normal_behavior
            @ requires contains(id);
            @ ensures (\exists int i; 0 <= i && i < people.length; people[i].getId() == id &&
            @         \result == people[i]);
            @ also
            @ public normal_behavior
            @ requires !contains(id);
            @ ensures \result == null;
            @*/
      @Test
      public void testGetPerson() throws Exception { 
      //TODO: Test goes here...
          MyNetwork myNetwork = new MyNetwork();
          MyPerson myPerson = new MyPerson(3,"kll",99);
          myNetwork.addPerson(myPerson);
          MyPerson result = (MyPerson) myNetwork.getPerson(3);
          Assert.assertEquals(result,myPerson);
          result = (MyPerson) myNetwork.getPerson(88);
          Assert.assertNull(result);
      } 

      输出结果为

      Before Method
      After Method
      ​
      进程已结束,退出代码0

      说明测试成功

  • 对于比较复杂的规格,一定要进行层层拆分,可以以分号为单位进行剥离,就可以得到如下图的划分好层次的规格,这样理解错问题的概率就会大大降低

  • 但是相应的,越复杂的规格的目的性更好从函数名中猜出,所具备的特征也就越明显,这样的情况找同学对拍是比较理想的测试方法

  • 同时针对复杂度较高的函数构造了在所给数据限制内的测试数据,

对NetWork的扩展

假设出现了几种不同的Person

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

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

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

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

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


类扩展

  • 新增Product

    public Product{
        private int proId;//对于所有产品独一无二的id
        private String type;//用于识别商品种类
        private double price;//该商品的价格
        private double sellSum; // 该商品的销售额
        private Producer producer;
        private ArrayList<Integer> advs;
        private ArrayList<Integer> cus;
        /* 用于记录销售路径
        每售卖一次,在ArrayList中存放对应的广告商和生产者*/
    }
  • 新增三个类AdvertiserProducerCustomer继承自Myperson,这三个类中均有对应的String type,用以标记发送广告,生产,消费的商品种类

  • 新增AdvertiseMessage类以及PurchaseMessage

  • 新增相应的异常类

  • 在NetWork类里需要新增下列内容

    // @ public instance model non_null Product[] productList;
    // @ public instance model non_null AdvertiseMessage[] ads;
    // @ public instance model non_null Customer[] customers;
    ​
    //@ ensures \result == (\exists int i; 0 <= i && i <proIdList.length; proIdList[i].getId() == proId);
    public boolean containsProduct(proId);
    ​

核心方法

  • Advertiser:向所有的消费者发送产品广告

  • Customer:向其接受到的信息中的指定广告商购买商品

  • Product:查询某个商品的销售额

public interface 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 (\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
      @ ensures (\exists int i; 0 <= i && i < products.length; products[i].getId() == id &&
      @         \result == products[i]);
      @ also
      @ public normal_behavior
      @ requires (\forall int i; 0 <= i && i < products.length; products[i].getId() != id);
      @ ensures \result == null;
      @*/
    public /*@ pure @*/ Product getProduct(int id);
    
    /*@ public normal_behavior
      @ requires contains(personId) && containsMessage(id) && getMessage(id) instance of AdvertiseMessage 
      @ assignable messages,customers[*].messages,getMessage(id).getProduct().ads
       @ ensures (\forall int i;0<=i&&i<customers.length;
       @ (\forall int j; 0 <= j && j < \old(customers[i].getMessages().size());
       @          \old(customers[i].getMessages().getMessages().get(j+1) == \old(customers[i].getMessages().getMessages().get(j))));
       @ ensures (\forall int i;0<=i&&i<customers.length;
       @ \old(customers[i].getMessages().get(0).equals(\old(getMessage(id)));
       @ ensures (\forall int i; 0<=i&&i<customers.length; \old(customers[i].getMessages().size() == \oldcustomers[i].getMessages().size()) + 1;
      @ 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 < \old(getMessage(id).getProduct().ads.size()));
      @         (\exist int j;0 <= j < getMessage(id).getProduct().ads.size();
      @         \old(getMessage(id).getProduct().ads.get(i))
      @         ==getMessage(id).getProduct().ads.get(j)
      @ ensures (\exist int i;0 <= i < getMessage(id).getProduct().ads.size();
      @ getMessage(id).getProduct().ads.get(j)==personId);
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(personId);
      @ signals (MessageIdNotFoundException e) contains(personId) && !containsMessage(msgId)
      @ signals (AdvertiseMessageNotFoundException e) contains(personId) && containsMessage(id) && !(getMessage(id) instance of AdvertiseMessage) 
      @*/
      public void sendAds(int personId,int id) throw PersonIdNotFoundException,MessageIdNotFoundException,AdvertiseMessageNotFoundException;
    
    
     /*@ public normal_behavior
       @ requires contains(personId) && containsMessage(id) && getMessage(id) instance of PurchaseMessage 
       @ assignable messages,getMessage(id).getPerson2().messages,
       @            getMessage(id).getProduct().customers,getMessage(id).getProduct().sellSum
       @            getMessage(id).getPerson1().money
      @ 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 !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 < \old(getMessage(id).getProduct().customers.size()));
      @         (\exist int j;0 <= j < getMessage(id).getProduct().customers.size();
      @         \old(getMessage(id).getProduct().customers.get(i))
      @         ==getMessage(id).getProduct().customers.get(j)
      @ ensures (\exist int i;0 <= i < getMessage(id).getProduct().customers.size();
      @ getMessage(id).getProduct().customers.get(j)==personId);
      
      @ ensures (\old(getMessage(id)).getPerson1().getMoney() ==
      @         \old(getMessage(id).getPerson1().getMoney()) - \old(getMessage(id)).getProduct()).getPrice(); 
      @ ensures (\old(getMessage(id)).getProduct().getsellNum() ==
      @         \old(getMessage(id).getProduct().getsellNum()) + \old(getMessage(id)).getProduct()).getPrice(); 
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(personId);
      @ signals (MessageIdNotFoundException e) contains(personId) && !containsMessage(msgId)
      @ signals (PurchaseMessageNotFoundException e) contains(personId) && containsMessage(id) && !(getMessage(id) instance of PurchaseMessage) 
      @*/
    public void sendPurMsg(int id,int personId) throws PersonIdNotFoundException,MessageIdNotFoundException,PurchaseMessageNotFoundException;
}
​
/*@ public normal_behavior
      @ requires containsProduct(proId);
      @ ensures \result == getProduct(proId).getsellNum();
      @ also
      @ public exceptional_behavior
      @ signals (ProductIdNotFoundException e) !containsProduct(proId);
      @*/
public double querySellNum(int proId) throws ProductIdNotFoundException;

学习体会

这个单元的难度相较前两个单元是有所降低的,感觉比较难的部分其实集中在图论算法的方面,也算是顺带复习数据结构的知识了(捂脸)

但是有很多细节的地方需要注意,不然可能不小心就会产生bug,导致强测的结果不尽如人意,而且由于作业是迭代开发的,也需要小心某些方法的JML规格发生了变化。

这个单元大致就是酱紫啦,UML单元加油鸭!

 
posted @ 2022-06-06 10:43  Tian_Kuang  阅读(12)  评论(0编辑  收藏  举报