面向对象第三单元总结

第三单元的任务是根据官方给出的JML规格实现代码、完成社交网络模型。由于方法的前置条件、后置条件等信息已由JML给出,本单元的功能实现较之前简单了不少,而算法优化则成为了不可回避的问题,毕竟完全按照JML实现代码只保证结果正确而不保证性能。

一、结构分析

各类的基本结构已由JML确定,类图分析如下:

1. 第九次作业

image

2. 第十次作业

image

3. 第十一次作业

image

二、性能优化

1. qci与qbs

isCircle的核心在于判断两点之间是否存在路径,按照官方给出的规格,可从id1开始搜索有关系的人,若能搜索到id2即返回true,否则即为false。而qbs则要对qci进行O(n2)规模的调用,其时间开销不由令人担忧。由于本单元作业中不涉及边的删除操作,考虑在添加节点与边时维护并查集,具体为:addPerson时生成新Set对象并将其加入,addRelation时将两个Set合并,之后将qci转化为id1所在Set是否包含id2,qbs返回上述Set的数量。互测结果表明,若不对此处算法进行优化,运行极端数据时将会超时。

2. qlc

规格中qlc的描述较为复杂,功能为返回id所在极大连通子图的最小生成树,与qci中维护的Set关系紧密,故新建Connection类,并对每个Set生成对应Connection对象。Connection中按权值大小排序保存Set中的边。此时addRelation分为两类:

  1. 两点不在同一Set中,此时需要合并Set与Connection,之后对各边权值简单求和即为更新的值;
  2. 在同一连通子图中添加边,之后需要删除一条多余的边并更新边权之和。

计算采用Kruskal算法,其优势如下:

  1. 计算过程只需访问Connection中保存的边,无需频繁访问Person获取边的信息;
  2. 计算中舍弃的边无需保留,可直接从Connection中删除,虽然复杂度规模仍为O(eloge),但e的大小在每次计算后减至n-1。

3. sim

sendIndirectMessage需要计算两点间的最短路径,按照之前的思路,可通过Floyd算法进行动态维护,但其缺陷极为明显:最短路径的维护较前两者更为复杂,在时间上带来明显的额外开销,如果测试数据不包括sim指令,将会带来较大的损失。通过比较各算法在最坏情况下的复杂度,最终采用Dijkstra算法。

4. 容器

数据的存储上,为提高查找效率,首先考虑HashMap与HashSet,由于同一类对象的id不会重复,可将hashCode重写为id值。计算最小生成树时,涉及边的插入、删除与归并,考虑LinkedList的底层实现方式,最终选择采用自定义链表存储。其他情况默认采用ArrayList。

三、问题修复

本单元的三次强测中我均取得了满分,而在互测中出现了以下问题:

  1. 第二次作业中qgvs指令完全按照规格描述实现,导致运行超时,修改后的计算方式为对各边均不重复遍历,返回求和结果的两倍,指令的时间开销降至一半;
  2. 执行deleteColdEmoji时,由于重写了equals方法,可通过旧id删除新的Message,修改方式为删除信息前判断目标信息与当前遍历位置是否指向同一对象。

四、构造数据

本单元数据的构造由多人合作完成,我主要负责构造边界超时数据,重点为复杂度较高的指令以及没有条数限制且具有一定复杂度的指令,针对不做优化或优化不完全的情况。

1. qbs

第九次作业的指令条数上限为1000条,针对未优化的算法构造如下数据:首先添加300个人与100个关系,之后每添加一个关系就查询一次blockSum,使脏位无效。

2. qlc

qlc指令上限为20条,剩余4800条内针对不同算法分别构造稀疏图与完全图。

3. qgvs

qgvs指令没有条数限制,考虑组内有n个人,需要一条ag,n条ap与n条atg;剩下指令全部为qgvs,每次计算的复杂度规模为O(n2)。该数据在本地测试中运行较快,但在评测中还是造成了超时现象。

4. sim

最短路径计算的优化思路较多,但效果不够理想,且容易针对。首先随机生成点和边的信息,对于Dijkstra算法,需要将信息发送给距离最远的人。此外,针对堆优化的算法构造稠密图,针对脏位优化不断加边以改变图的结构,可使运行时间较长。

除此之外,部分规格存在多层括号嵌套,容易导致理解错误,也是构造数据的重点。

五、扩展

扩展功能的本质为Message的添加与发送,Advertiser向外发送广告类型的Message,Producer通过Advertiser销售产品分析为添加相应广告信息,Customer通过Advertiser给Producer发送购买信息分析为添加由Advertiser定向发送给Producer的购买信息并发送。规格如下:

/*@ public normal_behavior
  @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && (message.getPerson1 instanceof Advertiser);
  @ 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));
  @ signals (WrongPerson1Exception e) !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && !(message.getPerson1() instanceof Advertiser);
  @ signals (EqualPersonIdException e) !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && (message.getPerson1() instanceof Advertiser) && message.getType() == 0 && message.getPerson1() == message.getPerson2();
  @*/
public void addAdvertisementMessage(AdvMessage message) throws EqualMessageIdException, WrongPerson1Exception, EqualPersonIdException;


/*@ public normal_behavior
  @ requires containsMessage(id) && getMessage(id).getType() == 0 && getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2());
  @ assignable messages;
  @ assignable getMessage(id).getPerson2().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 \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
  @ 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)));
  @ also
  @ public normal_behavior
  @ requires containsMessage(id) && getMessage(id).getType() == 1;
  @ assignable messages, 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 Person p; \old(getMessages(id)).getGroup().hasPerson(p); p.getMessages().size() == \old(p.getMessages().size()) + 1 && p.getMessages().get(0).equals(\old(getMessage(id))) && (\forall int i; o <= i && i < \old(p.getMessages().size()); p.getMessages().get(i+1) == \old(p.getMessages().get(i)));
  @ also
  @ public exceptional_behavior
  @ signals (MessageIdNotFoundException e) !containsMessage(id);
  @ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 && !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
  @ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 && !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
  @*/
public void sendAdvertisementMessage(int id) throws RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;


/*@ public normal_behavior
  @ requires !(exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && message.getType == 0 && message.getPerson1() instanceof Advertiser && message.getPerson2() instanceof Producer;
  @ 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));
  @ signals (WrongMessageTypeException e) !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && message.getType == 1;
  @ signals (WrongPerson1Exception e) !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && message.getType == 0 && !(message.getPerson1() instanceof Advertiser);
  @ signals (WrongPerson2Exception e) !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) && message.getType == 0 && message.getPerson1() instanceof Advertiser && !(message.getPerson2() instanceof Producer);
public void addPurchaseMessage(PurchaseMessage message) throws EqualMessageIdException, WrongMessageTypeException, WrongPerson1Exception, WrongPerson2Exception;


/*@ public normal_behavior
  @ requires containsMessage(id);
  @ assignable messages, getMessage(id).getPerson2().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 \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
  @ 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)));
  @ also
  @ public exceptional_behavior
  @ signals (MessageIdNotFoundException e) !contains(id);
  @*/
public void sendProduceMessage(int id) throws MessageIdNotFoundException;

六、心得体会

本单元的契约式编程较前两个单元轻松了不少,不必考虑从输入到解析再到输出结果的整个过程,只需要完成相应接口函数即可,各部分代码目的、分工明确,免去了瞻前顾后担心出错的时间浪费。相比前两个单元作业的完成历程,我深感契约式编程的优势所在。

而JML的缺点也比较明显:对人不友好。譬如qlc指令中最小生成树计算相关描述的理解让人着实费了一番功夫,而在大家都已经学习过离散数学2课程的背景下,自然语言只需一句话便能让大家明白指令的需求;在括号嵌套严重的地方,括号的配对也耗费了不少眼力。此外,以qbs与qci指令为例,JML规格的描述借助了一定的实现算法,编写代码时首先需要通过给出的算法理解需求,再依据需求重新设计算法(因为所给基础算法会超时),个人认为该过程带来了一定的浪费;而若是将需要实现的算法直接给出,又必然造成多个方法的相互影响、牵连。就目前接触的JML规格而言,我认为比较好的模式是以JML描述为标准,同时将自然语言描述作为辅助以便理解。

总之,本单元的训练确实给我带来了一些之前从未设想过的内容,让我确有所获。