BUAA OO 第三单元总结与反思
BUAA OO 第三单元总结与反思
写在前面
本单元主要考察了对JML规格的理解与运用,目的是实现一个小型的社交系统,支持建立小组、私发/群发消息、发送表情包、发送红包等功能,并具有相对完备的异常处理能力。
由于JML的规格已经由助教门给出,因此绝大部分内容只需要照猫画虎即可完成,实现的难度相较于前两个单元的确是小了不少。但同时要想取得满分,尤其是要避免被卡TLE,就要求我们充分理解特定方法的规格,而后进行无向图模型的建模,找到合适的图算法,并在能力范围内进行最大程度的优化。
自测与数据
在本单元的学习中,我尝试学习了课程组推荐的Junit
测试工具,了解了具体如何使用Junit
进行测试,但由于任然需要自己编写测试数据进行测试,Junit
相当于为我们提供了一个框架,因此我最终并没有大量使用它来进行测试,而是以自动生成测试数据+与同学进行对拍的方式进行测试。
对于数据整体覆盖的全面性,采用C++代码自动生成的方式生成大量测试数据:
int personId = 0, groupId = 0, messageId = 0;
int maxag = 20, maxap = 2500, maxqci = 100, maxqlc = 20;
int cntag = 0, cntap = 0, cntqci = 0, cntqlc = 0;
int type = 0; // 0 normal, 1 message, 2 group, 3 graph, 4 exception
如图是为了符合课程要求的一些限制条件(来自G同学的代码)。
同时对于特定的高复杂度指令,我也使python代码构造高强度数据进行测试防止出现TLE。
from numpy import random
addperson = ""
addrelation1 = ""
addrelation2 = ""
addtogroup = ""
qlc = ""
for i in range(1500):
addperson = addperson + 'ap ' + str(i) + " name"+str(i) + ' 10\n'
for i in range(1500):
addrelation1 = addrelation1 + 'ar ' + str(i) + " " + str(i+1) + ' 20\n'
for i in range(1980):
addrelation1 = addrelation1 + 'ar ' + str(random.randint(0, 1500)) + " " + str(random.randint(0, 1500)) + ' ' + str(random.randint(1, 50)) +'\n'
for i in range(20):
qlc = qlc + 'qlc ' + str(i) + ' \n'
fo = open("case3.txt", "w")
fo.write(addperson+addrelation1+qlc)
如图是针对 qlc
指令的高强度测试数据。
整体架构设计
模型构建
本单元的作业背景是社交网络模型,可以比较完美的抽象成无向图的模型。Person可以看作图中的点,Relation可以看作图中的边,Group可以看作图中特定点的集集合,Message可以看作 点/点集
与 点/点集
之间的交流。
容器选择
JML并没有限制我们使用哪种容器,因此需要我们自己根据该容器在JML中的使用特性来做出选择。对于大部分数据我都采用了Arraylist容器,但事实上归过头来看这并不是一个很好的选择,对大部分元素来说有唯一Id,因此选择使用Hashmap或者HastSet这样的哈希容器时更好的选择,因为它们拥有O(1)的查询复杂度,而Arraylist在查询元素时自带了O(n)的时间复杂度,因此对后续算法要求提高了很多,这可能也是导致我出现tle的原因之一。
图论算法
在本次作业中,部分指令用到了一些图论算法,这也是本单元作业的难点和性能优化的重点,例如qbs和qci使用到了并查集、qlc使用到了最小生成树算法、sim使用到了最短路算法。
路径压缩的并查集
public void addper(int id) {
pre[id] = id;
}
public boolean unite(int x, int y) {
int rootx = findpre(x);
int rooty = findpre(y);
if (rootx != rooty) {
pre[rooty] = rootx;
return true;
}
return false;
}
public int findpre(int x) {
if (pre[x] == x) {
return x;
}
return pre[x] = findpre(pre[x]);
}
kruskal
算法求最小生成树
int ans = 0;
int flag = 0;
while (flag < related.size() - 1) {
Edge edge = priorityQueue.poll();
if (search1.findpre(edge.getId1()) != search1.findpre(edge.getId2())) {
search1.unite(edge.getId1(), edge.getId2());
flag++;
ans += edge.getValue();
}
}
return ans;
Dijsktra
算法求(单源)最短路
//dij
ans[idroot] = 0;
while (true) {
int flag = 0;
Point point = null;
while (!priorityQueue.isEmpty()) {
point = priorityQueue.poll();
if (ans[point.getIdto()] > point.getValue()) {
ans[point.getIdto()] = point.getValue();
dirt[point.getIdto()] = 1;
flag = 1;
break;
}
}
if (dirt[mapping.get(tmp.getPerson2().getId())] == 1 || flag == 0) {
break;
}
MyPerson person = (MyPerson) people.get(point.getIdto() - 1);
for (int i = 0; i < person.getAcquaintance().size(); i++) {
if (dirt[mapping.get(person.getAcquaintance().get(i).getId())] == 0) {
priorityQueue.add(new Point(mapping.get(person.getId()),
mapping.get(person.getAcquaintance().get(i).getId()),
ans[point.getIdto()] + person.getValue().get(i)));
}
}
}
messages.remove(tmp);
return ans[mapping.get(tmp.getPerson2().getId())];
数据缓存
这是本次作业的一个重要思想,对于需要频繁查询的数据进行缓存处理,将时间复杂度分摊到其他的指令,从而避免了tle的出现。
以qgvs为例:
首先在MyGroup类中增加 valuesum
属性。
private int valuesum;
在addPerson方法中维护`valuesum
@Override
public void addPerson(Person person) {
people.add(person);
for (Person person1 : ((MyPerson) person).getAcquaintance()) {
if (hasPerson(person1)) {
valuesum += person.queryValue(person1) * 2;
}
}
}
在delPerson方法中维护valuesum
@Override
public void delPerson(Person person) {
int i;
for (i = 0; i < people.size(); i++) {
if (people.get(i).equals(person)) {
break;
}
}
people.remove(i);
for (Person person1 : ((MyPerson) person).getAcquaintance()) {
if (hasPerson(person1)) {
valuesum -= person.queryValue(person1) * 2;
}
}
}
在addRelation方法中维护valuesum
@Override
public void addRelation(int id1, int id2, int value) throws
PersonIdNotFoundException, EqualRelationException {
if (!contains(id1)) {
throw new MyPersonIdNotFoundException(id1);
} else if (!contains(id2)) {
throw new MyPersonIdNotFoundException(id2);
} else if (getPerson(id1).isLinked(getPerson(id2))) {
throw new MyEqualRelationException(id1, id2);
}
MyPerson p1 = (MyPerson) getPerson(id1);
MyPerson p2 = (MyPerson) getPerson(id2);
p1.addfriend(getPerson(id2), value);
p2.addfriend(getPerson(id1), value);
if (search.unite(mapping.get(id1), mapping.get(id2))) {
qbs--;
}
for (Group group1 : group) {
if (group1.hasPerson(getPerson(id1)) && group1.hasPerson(getPerson(id2))) {
((MyGroup) group1).setValuesum(((MyGroup) group1).getValuesum() + value * 2);
}
}
}
最后在qgvs指令中只需找到对应group返回其 valuesum
即可
@Override
public int queryGroupValueSum(int id) throws GroupIdNotFoundException {
for (Group value : group) {
if (value.getId() == id) {
return value.getValueSum();
}
}
throw new MyGroupIdNotFoundException(id);
}
问题与修复
第九次作业
在第九次作业中,我由于未读清 EqualRelationException 异常的描述,误以为id1与id2相等之后就仅需输出一次;同时也未缓存qbs的数据,而是完全按照JML的规格完成,未完全理解qbs所求的是Network中联通分支数量这一抽象概念。从而导致了强测中挂掉了4个点。
第十次作业
在第十次作业中,我在提交截止前加强的测试,生成了大量数据与同学进行对拍,确保了万无一失,最终做的还算不错,强侧与互测都未出现问题。
第十一次作业
在第十一次作业中,我虽然采用了Dijsktra算法并实现了堆优化+并查集优化,将时间复杂度由O(m + n^2)
降为 O(n \* logn + m)
(其中n为点数目,m为边的数目)但由于在执行算法的过程中写了一条十分愚蠢的代码,从而提升了复杂度最终使得强侧因为sim指令而导致了tle,互测中也因此被hank了两个点。在之后的bug修复环节,我一度以为自己时的dij写的不够好,最终花了大量时间(几乎不亚于完成这次作业的时间),最终才在这个最不可能的地方找出了这个小小的bug,这让我我十分懊恼。
Network扩展
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买(所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息)
- Person:吃瓜群众,不发广告,不买东西,不卖东西
① 新增Good类表示可交易产品,包括售价、折扣等等属性。
② Advertiser、Producer和Customer可以继承Person类;Producer需要增加一个属性来表示产的产品的id集合,Advertiser需要增加一个属性来表示其向外发送广告的产品的id集合,Customer需要增加两个属性,一个表示其偏好的产品的id集合,另一个来表示对偏好id集合中每个id的偏好值。
③ 新增广告信息 AdvertisementMessage 和 PurchaseMessage 两个类,它们继承Message类。
④ 当Customer对某物品的偏好值大于等于某个阈值时,通Advertiser过向该产品的Producer发送PurchaseMessage。
设置消费者偏好
/*@
@ public normal_behavior
@ requires contains(personId);
@ requires getPerson(personId) instance of Customer;
@ ensures (\forall int i; 0 <= i && i < \old(favorGoods.length);
@ (\exists int j; 0 <= j && j < favorGoods.length;favorGoods[j] ==
@ (\old(favorGoods[i]) && favorValues[j] == (\old(favorValues[i]))));
@ ensures (\exists int i; 0 <= i && i < favorGoods.length; favorGoods[i] ==
@ favorGoodId && favorValues[i] == (\old(favorValues[i])) + value);
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(personId);
@ signals (PersonWrongTypeException e) !(getPerson(personId)
@ instance of Customer);
@*/
public /*@ pure @*/void addFavor(int personId, int favorGoodId, int value) throws PersonIdNotFoundException,
PersonWrongTypeException;
消费者购买物品
/*@
@ public normal_behavior
@ requires contains(personId);
@ requires getPerson(personId) instance of Customer;
@ assignable getPerson(personId).favorGoods;
@ ensures (\forall int i; 0 <= i && i < getPerson(personId).favorGoods.length);
@ favorValues.get(i) < limit);
@ ensures (\forall int i; 0 <= i && i < getPerson(personId).\old(favorGoods).length
@ && \old(favorValues).get(i) >= limit;
@ sendPurchaseMessage(getPerson(personId).favorGoods.get(i))
@ );
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(personId);
@ signals (PersonTypeWrongException e) !(getPerson(personId) instance of Customer);
@*/
public void buyGoods(int personId ,int limit) throws PersonIdNotFoundException, PersonTypeWrongException;
查询生产者销售额
/*@
@ public normal_behavior
@ requires contains(personId);
@ requires getPerson(personId) instance of Producer;
@ assignable nothing;
@ ensures \results == (\sum int i; 0 <= i && i <((Producer)
@ getPerson(personId)).\old(saleGoods).length;
@ saleGoods.get(i).getPrice() * salenumbers.get(i));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(personId);
@ signals (PersonTypeWrongException e) !(getPerson(personId) instance of Customer);
@*/
public /*@ pure @*/ int querySaleValue(int personId) throws PersonIdNotFoundException, personTypeWrongException;
心得体会
本单元的难度总体来说相比于前两个单元并不能算大。主要的任务就是阅读 JML 约束并遵照它进行代码的书写,只要仔细阅读并遵照它的规则指引,几乎就不会犯什么错误(除非像我一样阅读指导书出了错)。与之相比,本单元更大的工作量都体现在还是在性能优化和测试上。尤其是对图论相关知识的复习和再次学习,甚至都超过了曾经的数据结构课程。同时在写的过程中我亦感受到Java这门高度封装的语言相比C语言的优势——更简洁的代码书写,一个用C语言需要近百行的图论算法用java在50行之类一定能轻松解决,且拥有更好的可读性。
本单元作业是OO课程的最后的互测机会,相比于前两个单元,我由于更多被hank,因此也被迫激发了自己hank的动力,结合自动化生成数据和针对性构造样例,成功hack到许多的bug。每周的hankday结束之后,都能感觉到莫名的轻松自如。
总的来讲,本单元的OO作业完成的并不好,出现了好一些不该出现的低级错误,但是在这样的打击中也让我的心态得到了一些历练,在今后面对意外情况的时候希望自己能处理的更加完美。OO还剩最后一个单元的内容在等着我,希望自己能够抓住机会,细心对待,不要再留下更多的遗憾。