面向对象第三单元总结

〇.单元总览

本单元的主要目的是建立一个社交通信网络,代码已由课程组给出,我们只需根据给出的jml规格实现接口即可,主要包括人,群组,网络和各种消息以及各种可能会产生的异常,部分方法涉及图论的相关知识。

一.容器选择

在jml规格提供的可选属性中,一般提供的都是使用数组类型存储数据,但为了实现更高的查找效率以及将id和对应数据建立起对应的联系,我最终选择使用HashMap这一类型。

Group

private HashMap<Integer,Person> people;
//建立id-person间的对应关系

Network

private HashMap<Integer,Person> people;
//建立id-person间的对应关系
private HashMap<Integer,Group> groups;
//建立id-group间的对应关系
private HashMap<Integer,Message> messages;
//建立id-message间的对应关系
private HashMap<Integer,Integer> emojis;
//建立id-emoji间的对应关系

Person

private HashMap<Person, Integer> personMap;
//存储当前person和其他person间的权值

Exception

private static HashMap<Integer, Integer> map = new HashMap<>();
//建立触发异常的id-当前id触发次数间的对应关系

二.性能优化

本单元作业不用在架构设计部分花费太多的功夫(毕竟都已经给出了jml规格),但在性能优化部分却是有很多东西可以说道。

qgvs指令

qgvs指令是为了获取一个group内所有person两两间的value总和,若是直接使用二重循环相加,在大规模调用qgvs指令时会导致程序运行时间过长,产生超时错误。

为了解决这个问题,我们可以在Group类中维护一个ValueSum的属性,用来实时更新value值的总和,需要注意的是这个值需要在加人,删人和添加关系时进行更新。

	@Override
    public void addPerson(Person person) {
        for (Person p : people.values()) {
            this.valueSum = this.valueSum + person.queryValue(p) * 2;
        }
        this.people.put(person.getId(), person);
    }
    //加人时更新
	public void updateValueSum(int value) {
        this.valueSum = this.valueSum + value * 2;
    }
	//添加关系时更新
	@Override
    public void delPerson(Person person) {
        people.remove(person.getId());
        for (Person p : people.values()) {
            this.valueSum = this.valueSum - person.queryValue(p) * 2;
        }
    }
	//删人时更新

并查集

在作业中有query_circlequery_block_sum两种指令:query_circle询问两个点是否联通,query_block_sum询问图里的连通块数量。为了提高程序的运行效率,我们可以使用并查集来解决这个问题。

//并查集类
import java.util.HashMap;

public class Ufds {
    private HashMap<Integer,Integer> ufds;

    public Ufds() {
        this.ufds = new HashMap<>();
    }

    public void add(int son, int father) {
        this.ufds.put(son, father);
    }

    public int getFather(int son) {
        return  ufds.get(son);
    }

    public int findFather(int son) {
        if (son == ufds.get(son)) {
            return son;
        } else {
            ufds.put(son, findFather(ufds.get(son)));
            return ufds.get(son);
        }
    }

    public boolean isCircle(int id1, int id2) {
        return findFather(id1) == findFather(id2);
    }

}

最小生成树

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

由于之前已经实现了并查集的算法,因此可以使用Kruskal 算法来生成维护最小生成树。

Network类中使用ArrayList记录下所有的点集合和边集合,命名为pointsedges。在queryLeastConnection方法中首先挑选出所有与传入id连通的点,记录下连通点的数目,循环前使用排序算法调整edges中边的顺序,随后每次在循环中挑选出符合条件的最短边,直到跳出循环。

// Kruskal算法具体实现
@Override
    public int queryLeastConnection(int id) throws PersonIdNotFoundException {
        if (!contains(id)) {
            throw new MyPersonIdNotFoundException(id);
        } else {
            int cnt = 0;
            int sum = 0;
            int i = 0;
            for (int pointsId : points) {
                ufds2.add(pointsId,pointsId);
                if (ufds1.isCircle(id, pointsId)) {
                    cnt++;
                }
            }
            Collections.sort(edges);
            while (cnt != 1) {
                MyEdge edge = edges.get(i++);
                if (!ufds2.isCircle(edge.getId1(),edge.getId2()) &&
                        ufds1.isCircle(id,edge.getId1()) &&
                        ufds1.isCircle(id,edge.getId2())) {
                    ufds2.add(ufds2.findFather(edge.getId2()),
                    ufds2.findFather(edge.getId1()));
                    sum = sum + edge.getValue();
                    cnt--;
                }
            }
            return sum;
        }
    }

最短路径

最短路径直接使用Dijkstra算法即可实现,但由于追求更快的运行效率,我们可以稍作处理,使用堆优化的Dijkstra算法即可,复杂度为O(nlogn)

// 堆优化的Dijkstra算法
public int dijkstra(int id1, int id2) {
        HashMap<Integer,Integer> visited = new HashMap<>();
        PriorityQueue<Ele> pq = new PriorityQueue<>
            (Comparator.comparingInt(Ele::getLength));
        pq.offer(new Ele(id1, 0));
        for (int id : people.keySet()) {
            if (ufds1.isCircle(id1,id)) {
                visited.put(id,maxValue);
            }
        }
        visited.put(id1,0);
        while (!pq.isEmpty()) {
            Ele ele = pq.poll();
            int minId = ele.getId();
            int minValue = ele.getLength();
            if (!visited.containsKey(minId)) {
                continue;
            }
            if (minId == id2) {
                return minValue;
            }
            visited.remove(minId);
            for (Person p : ((MyPerson)getPerson(minId)).getPersonMap().keySet()) {
                int newValue = minValue + getLength(minId,p.getId());
                if (visited.containsKey(p.getId())) {
                    if (newValue < visited.get(p.getId())) {
                        visited.put(p.getId(), newValue);
                        pq.offer(new Ele(p.getId(), newValue));
                    }
                }
            }
        }
        return 0;
    }

三.Bug分析

三次作业中存在两处bug:第一处为qgvs指令超时错误;第二处为添加新关系时若2人同时在多个群组里时只会找到第一个群组,其他群组会被忽略。

发现其他同学四处bug:第一处为qgvs指令超时错误;第二处为分发红包时发红包人扣除money数错误;第三处为deleteColdEmoji方法中可能会删除多个message但只删除了一个message的错误;第四处为群组里加人的时候上限为 1111,未进行该判断而产生的错误。

由于本单元作业是基于jml规格进行的设计,因此主要的debug策略是将代码与规格进行比对,其次便是依靠评测机进行大规模测试。

四.Network 扩展

假设出现了几种不同的Person

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

AdvertiserProducerCustomer均可设计为Person的子类;AdvertiseMessageBuyMessage可以设计为 Message 的子类。

发送广告:

/*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) &&
      @			   (message instanceof AdvertiseMessage) &&
      @            (message.getType() == 0) ==> (message.getPerson1() != message.getPerson2()) ;
      @ assignable messages;
      @ ensures messages.length == \old(messages.length) + 1;
      @ 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));
      @ also
      @ public exceptional_behavior
      @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length;
      @                                     messages[i].equals(message));
      @*/
    public void addAdvertiseMessage(Message message) throws EqualMessageIdException;

查询销售额:

/*@ public normal_behavior
   @ requires (\exists int i; 0 <= i && i < products.length; products[i].getId() == id)
   @ ensures \result==getProduct(id).getSaleNum();
   @ also
   @ public exceptional_behavior
   @ signals (ProductIdNotFoundException)
   @     !(\exists int i; 0 <= i && i < products.length; products[i].getId() == id)
   @*/
 public int querySaleNum(int id) throws ProductIdNotFoundException;

生产商生产产品:

/*@ public normal_behavior
	@ requires contains(producerId) && (getPerson(producerId) instanceof Producer) && 
	@ 			getPerson(producerId).containProduct(productId);
	@ assignable getPerson(producerId).productNum;
	@ ensures getPerson(producerId).getProductNum(productId) ==
	@           \old(getPerson(producerId).getProductNum(productId)) + 1;
	@ also
	@ public normal_behavior
	@ requires contains(producerId) && (getPerson(producerId) instanceof Producer) && 
	@			!getPerson(producerId).containProduct(productId);
	@ assignable getPerson(producerId).productCount, getPerson(producerId).productNum;
	@ ensures getPerson(producerId).getProductCount() ==
	@           \old(getPerson(producerId).getProductCount()) + 1;
	@ ensures getPerson(producerId).containProduct(productId) && 
	@			getPerson(producerId).getProductNum(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;

五.心得体会

本单元整体难度相较于前两单元难度有所下降,主要考察对jml的阅读理解,为此我们需要熟悉jml的相关规模格式。

虽说简单,但也不能够掉以轻心,因为总有一些地方会存在某些坑点,我们需要仔细阅读jml以确保程序的正确性。当然jml也仅仅只是一个规范的描述,并不是一门真正的编程语言,我们编写代码只需要符合它的根本要求即可,不需要完全根据它的描述来编写程序,例如本次作业中的最小生成树部分,如果直接按照jml来编写程序,绝对会导致最后运行超时,我们需要对其实现进行一定的改变。

最后,请让我吐槽一句:jml好长啊!!!