BUAA-OO-Unit 3-Summary

BUAA-OO-Unit 3-Summary

一、综述

OO第三单元以完善一个存在个体、群组、消息的社交网络为目的。第一次作业考察基础的社交关系,引入并查集算法寻找个体之间的联系;第二次作业则增设了消息这一元素,并引入最小生成树算法;第三次作业迭代增设了新的消息种类,同时需要使用最短路径算法。通过三次作业的迭代开发,锻炼并考察了阅读JML规格的能力以及对一些涉及到图论的经典数据结构算法的掌握。

二、从JML角度构造测试数据

JML为代码提供了严格的约束,通过仔细阅读JML可以找到许多边界条件以及容易疏忽的细节。比如:

  • 群组人数上限为1111人,不能超出。当超过时,不再往组里加人,但是不会抛出异常。
  • 红包消息的金额计算,要严格按照JML的要求来,而不能想当然的写。

同时,JML便于观测方法的复杂度,从而可以构造极限数据进行测试,方便进行优化。

相比第二单元,随机数据又变得有效了起来,但是这会导致无法找出bug具体是在哪里出现。因此单元测试是十分有效且必要的,从JML约束的规格出发,对所有作业中涉及的指令进行全覆盖测试,从而更精准的定位bug。

三、架构设计

本单元作业中主体架构都在官方包中给出,因此无需进行额外的架构设计。对我个人而言,额外增设了edge类表示边、UnionFind并查集类、迪杰斯特拉算法类以及用于堆优化比较的Priority类与Comparator类。这些新增设的类具有单一功能,且解耦性都很好。

四、容器选择

MyPerson:

 private final HashMap<Person,Integer> acquaintance;//key:person value:value。将熟人与社交值放在同一个容器中。
 private final ArrayList<Message> messages;

MyGroup:

private final HashMap<Integer,Person> people;//key:id value:person

MyNetwork:

 private final HashMap<Integer, Person> people;//key:id value:person
 private final HashMap<Integer, Group> groups;//key:id value:group
 private final HashMap<Integer, Integer> emojiMap;//key:id value:heat

可以看到,为了简化查找的复杂度,基本上都选择了HashMap作为容器,有效优化了性能。

五、图模型构建与维护策略

  • 并查集算法

    代码如下:

    public class UnionFind {
        private final HashMap<Integer,Integer> father;//key:节点id value:对应的父节点id
        private int countFather;
    
        public UnionFind() {
            father = new HashMap<>();
            countFather = 0;
        }
    
        public void add(int id) {
            father.put(id,id); //构造一个新的节点,其初始父节点为自身
            countFather++;
        }
    
        public int findFather(int id) {
            if (id == father.get(id)) {
                return id;
            }
            else {
                int newFather = findFather(father.get(id));
                father.put(id,newFather);
                return newFather;
            }
        }
    
        public void union(int id1,int id2) {
            int father1 = findFather(id1);
            int father2 = findFather(id2);
            if (father1 != father2) {
                father.put(father2,father1);
                countFather--;
            }
        }
    
        public int getCountFather() {
            return countFather;
        }
    }
    

    在一般的并查集上,进行了路径压缩,这样可以提高效率,同时为最小生成树算法提供便利。

  • 最小生成树算法:

    代码如下:

    @Override
        public int queryLeastConnection(int id) throws PersonIdNotFoundException {
            if (contains(id)) {
                UnionFind newUnionFind = new UnionFind();
                TreeSet<Edge> edges = getEdges(id, newUnionFind);
                HashSet<Edge> edgesLeast = new HashSet<>();
                int sum = 0;
                for (Edge edge : edges) {
                    if (newUnionFind.findFather(edge.getId1()) !=
                            newUnionFind.findFather(edge.getId2())) {
                        edgesLeast.add(edge);
                        newUnionFind.union(edge.getId1(), edge.getId2());
                    }
                }
                for (Edge edge : edgesLeast) {
                    sum += edge.getValue();
                }
                return sum;
            } else {
                throw new MyPersonIdNotFoundException(id);
            }
        }
    

    我使用的是kruskal算法。

    首先,对于边集,我采取TreeSet这一容器进行存储,减少排序花费的时间,提升贪心算法的效率。值得注意的是这样的情况,当两条边权值一样但边节点不同时,两条边都要加入set中,因此进行比较时使用的equals方法与自己新增的Comparator需要格外注意。如下:

    @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Edge edge = (Edge) o;
            return ((id1 == edge.id1 && id2 == edge.id2) || (id1 == edge.id2 && id2 == edge.id1))
                    && value == edge.value;
        }
        
    public class MyComparator implements Comparator<Edge> {
        @Override
        public int compare(Edge o1, Edge o2) {
            if (o1.getValue() == o2.getValue() && !o1.equals(o2)) {
                return 1;
            }
            else {
                return o1.getValue() - o2.getValue();
            }
        }
    }
    

    其次,使用并查集进行优化,方便查询两节点是否在同一集合中。

  • 最短路径算法

    我使用的是迪杰斯特拉算法

    代码如下:

    /*people:连通图里所有人
      visitedMap:存储该节点是否被使用,初始均为0
      disMap:存储最短距离的图。在构造方法中初始化,其中,与起始点不直接相连的点,距离设为最大。
      priorities:用于进行堆优化的队列
    */
    public Dijkstra(Person p1,Person p2,HashMap<Integer,Person> peopleCircled) {
            this.people = peopleCircled;
            this.disMap = new HashMap<>();
            this.visitedMap = new HashMap<>();
            this.targetId = p2.getId();
            this.priorities = new PriorityQueue<>(cmp);
            for (Person p : people.values()) {
                if (p.equals(p1)) {
                    disMap.put(p.getId(),0);
                    visitedMap.put(p.getId(),0);
                    Priority priority = new Priority(p.getId(),0);
                    priorities.add(priority);
                }
                else if (p1.isLinked(p)) {
                    disMap.put(p.getId(),p1.queryValue(p));
                    visitedMap.put(p.getId(),0);
                    Priority priority = new Priority(p.getId(),p1.queryValue(p));
                    priorities.add(priority);
                }
                else {
                    disMap.put(p.getId(),100000000);
                    visitedMap.put(p.getId(),0);
                    Priority priority = new Priority(p.getId(),100000000);
                    priorities.add(priority);
                }
            }
        }
    
    public int getMinDis() {
            while (true) {
                int min = 100000000;
                int minId = -1;
                Priority priority = priorities.poll();
                if (visitedMap.get(priority.getId()) == 1) {
                    continue;
                }
                min = priority.getValue();
                minId = priority.getId();
                if (targetId == minId) {
                    return min;
                }
                visitedMap.put(minId,1);
                Person cur = people.get(minId);
                for (Person person : ((MyPerson)cur).getAcquaintance().keySet()) {
                    int id = person.getId();
                    if (visitedMap.get(id) == 0 &&
                            disMap.get(id) > disMap.get(minId) + cur.queryValue(person)) {
                        disMap.put(id,disMap.get(minId) + cur.queryValue(person));
                        Priority priorityNew = new Priority(id,
                                disMap.get(minId) + cur.queryValue(person));
                        priorities.add(priorityNew);
                    }
                }
            }
        }
    

    对于与起始点不直接相连的点,距离设为整型的最大值,算是一个小trick。

    使用优先队列得到最小权值点,可以有效提升效率。

    需要注意的是,更新距离后,要将更新后的值加入优先队列中。同时,通过支点更新距离时,只需要遍历支点的acquaintance即可,否则时间复杂度会高很多。

六、遭遇的性能问题与修复情况

遭遇的问题

  • qgvs

    该指令需要求出指定group中所有关系中value的和,如果完全按照jml来写,会用到二重循环,因此当数据量很大时会超时。

    优化方法是,维护一个属性valueSum,在进行addPersondelPersonaddRelation时对该属性进行实时更新即可。

  • qgav

    qgvs相似,该指令要得到组中年龄的方差,同样会出现二重循环导致复杂度高的问题。

    解决方法也是一样,维护一个属性ageSum实时更新即可。

  • 最短路径算法中出现的问题:

    在迪杰斯特拉的“更新”步骤中,我刚开始的写法遍历了联通图的所有节点,这样大大减少了效率。

    解决方法是,只要遍历当前支点的acquaintance即可。

bug

第二次互测中由于qgvsqgva的问题被hack了两次。

强测均没有出现问题。

七、Network扩展

题目要求

假设出现了几种不同的Person

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

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

新增接口与方法

  • Advertiser:继承Person,有productId的属性。
  • Producer:继承Person,有productId的属性。
  • Customer:继承Person,有preference的属性。
  • Product:新开设的类,代表产品,有id以及money属性。
  • AdvertisementMessage:继承Message,有advertiserId、producerId、productId的属性。
  • PurchaseMessage:继承Message,有advertiserId、producerId、productId的属性。
  • Network:需新增方法addAdvertiser、addProducer、addProduct、containsAdvertisementMessage、addAdvertisementMessage、sendAdvertisementMessage、containsPurchaseMessage、addPurchaseMessage、sendPurchaseMessage方法。

JML规格:

  • containsProduct

    /*@ ensures \result == (\exists int i; 0 <= i && i < productList.length; 		  	  /*@				productList[i].getProductId() == productId);
        public /*@ pure @*/ boolean containsProduct(int productId);
    
  • addProduct

    /*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < products.length;                             @			products[i].getProductId() == product.getProductId();
      @ assignable products;
      @ ensures (\forall int i; 0 <= i && i < \old(products.length);
      @ ensures (\exists int i; 0 <= i && i < products.length; products[i] == product);
      @(\exists int j; 0 <= j && j < products.length; products[j] == (\old(products[i]))));
      @ ensures products.length == \old(products.length) + 1;
      @ also
      @ public exceptional_behavior
      @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length;
      @      	 products[i].equals(product));
      @*/
    public void addProduct(Product product) throws EqualProductIdException;
    
  • sendAdvertisementMessage

     /*@ public normal_behavior
       @ requires containsMessage(id) 
       @          && getMessage(id).getType() == 1 
       @          && getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1())     
       @          && getMessage(id).getPerson1() instanceof Advertiser
       @          && ProductList.contain(product); 
       @ assaignable product;
       @ assaignable people[*]
       @ 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 Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==\old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue());
       @ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);\old(people[i].getSocialValue()) 	  @	== people[i].getSocialValue());
       @ ensures  money = \old(money) + money
       @ also
       @ public exceptional_behavior
       @ signals (MessageIdNotFoundException e) !containsMessage(id);
       @ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
       @          !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
       @*/
     public void sendAdvertisementMessage(Product product, int money, int id) throws MessageIdNotFoundException;
    

八、学习体会与建议

学习体会

本单元OO学习让我第一次接触到规格化编程。良好的JML为程序提供了严谨的约束,作为实现者,必须严格执行JML的规格要求,即进行契约式编程。相比前两个单元,本单元借助JML的帮助无需过多考虑架构的问题,所需要完成的需求也一目了然,任务更加清晰,完成起来属实是轻松愉快不少。

但是,在严格执行规格要求的同时,不能被JML拘束。通过三次作业的迭代开发,我深刻认识到JML告诉你需要达成什么样的目的,而非告诉你怎么完成目的。规格是不可逾越的达摩克斯之剑,在符合规格的前提下,更要灵活设计,擅用算法与容器进行优化维护,切忌仗着有JML就按部就班,不过脑子。

本单元的学习,第一感觉就是JML太棒了!但是在日后的开发过程中,不可能有现成的JML供自己参考;因此,本单元通过给定的JML规格编写代码的过程中,那些对面向对象更深入的理解,以及对于规格化编程裨益的认识,我想才是这单元最珍贵的收获吧。

建议

首先,和上单元一样,中测偏弱。尤其是前两次,甚至都存在一些指令没考察到的情况。因此还是建议增加一些强度,至少做到指令全覆盖

其次,我个人认为这个单元以JML为主,但三次作业主要的难点还是在图论算法上,稍微有些懵逼。也许可以减少对算法的考察,而将根据JML进行单元测试的部分内容作为考察内容。

posted @ 2022-06-01 23:08  yeger118  阅读(36)  评论(1编辑  收藏  举报