BUAA-OO-第三单元总结
规格实现策略
我实现规格时策略如下:
- 总结大家在讨论区和微信群里面聊的最多的函数,先看看大概思路,要添加些什么属性,把大致框架思考好,然后这个最难的自然放最后再写:)
- 从MyPerson、MyGroup等这几个开始写,最后再写MyNetwork。
- 先写那些函数名简单、JML短的,这些一般代码都很短,实现很快。
- 对于JML要求多的,先写
exceptional_behavior
,因为每一种exception一般JML只有一行,简单易懂,实现基本上也很短;然后对于长的JML,其实其中大部分内容是在讲前提和结果,将那些部分先去除,只看真正要写的部分(其实JML就很短了),写完后跟前提和结果对一下,看写的有没有什么问题。 - 另外长JML有的时候是因为有多个
normal_behavior
,这个时候分类写就行。
测试策略
- 主要看自己写的时候错误的地方别人是否注意到,要是没有,就很好构造数据hack。
- 看JML里面前提和结果,自己和别人的保证了没有,一般里面具体实现不太会错。
- 每一次作业都有一个很重要的函数,每次很多人问这个函数,所以基本上大家对这个函数的实现完成度很高,扫一眼没问题就不必在这个函数上纠缠;相反大家对MyPerson、MyGroup等里面的函数大多没仔细思考(至少我是的),里面漏洞挺多,多看看就能找到。
- 还有就是随机生成数据“轰炸”,有的时候能通过这个hack到。
容器选择经验
本单元我使用了如下容器:
ArrayList<T>
:最常见的存储选择。HashMap<K, V>
:Hashmap相较于Arraylist优势在于可以做到key,value对应,方便了许多。如果查找某个存储元素,用containKeys
和containValues
可以实现O(1)查找。我大多选择HashmapLinkedList<T>
:用链表实现的list,主要是使用其能做到头插的功能。PriorityQueue<T>
:优先队列,因为堆优化的Dijkstra算法需要使用堆,因此用java自带的优先队列予以实现
容器有一大坑点:没有初始化就直接用了。因此建议每次在定义的时候直接初始化了,免得自己忘了。
图模型构建和维护策略
1、并查集
并查集第一次使用是在第九次作业,对指令qci
的处理。首先是初始化,并查集初始化应该是在有人的时候,即在addPerson
的时候,进行初始化。然后在addRelation
的时候更改结点的祖宗结点,这样qci的时候直接比较两个结点祖宗结点是否相同即可,大幅降低复杂度。其主要函数为:
public int find(int x) {
if (checkset.get(x) == x) {
return x;
}
else {
int tmp = checkset.get(x);
checkset.replace(x, find(tmp));
return checkset.get(x);
}
}
并查集第二次使用是在第十次作业的queryLeastConnection
函数中,因为要写最小生成树,我采用的Dijkstra
,里面用并查集完成点集合并。
2、最小生成树
最小生成树用于第十次作业的queryLeastConnection
函数中,这里没什么特别之处,全用普通的Dijkstra
,加上并查集和快排优化。具体步骤如下:
每次调用queryLeastConnection
时先将构建相关边,初始化并查集:
for (Integer i : checkset.keySet()) {
if (find(checkset.get(i)) == ances) {
newcheck.put(i, i);
Person p = getPerson(i);
ArrayList<Person> acquaintance = ((MyPerson) p).getAcquaintance();
ArrayList<Integer> values = ((MyPerson) p).getValue();
for (int j = 0; j < acquaintance.size(); j++) {
if (i > acquaintance.get(j).getId()) {
continue;
}
Brim brim = new Brim(i, acquaintance.get(j).getId(), values.get(j));
//flag[k] = false;
valuemap[k++] = brim;
}
}
}
然后用快排将边排序:
public void quickSort(Brim []q, int l, int r) {
//判断边界
if (l >= r) {
return;
}
//取分界点
int i = l - 1;
int j = r + 1;
int x = q[l + r >> 1].getLen();
while (i < j)
{
do {
i++;
} while (q[i].getLen() < x);
do {
j--;
} while (q[j].getLen() > x);
if (i < j) {
Brim tmp = new Brim(q[i].getLeft(), q[i].getRight(), q[i].getLen());
q[i] = q[j];
q[j] = tmp;
}
}
quickSort(q, l, j);
quickSort(q, j + 1, r);
}
最后就是上kruskal
:
for (int m = 0; m < k; m++) {
int left = valuemap[m].getLeft();
int right = valuemap[m].getRight();
int len = valuemap[m].getLen();
int leftances = findNew(newcheck.get(left));
int rightances = findNew(newcheck.get(right));
if (leftances != rightances) {
ans += len;
newcheck.replace(leftances, rightances);
}
}
3、最短路dijkstra
算法
最短路算法是用在第十一次作业sim指令中,我采用的是堆优化的dijkstra
算法。对于堆优化,由于java有内置PriorityQueue
实现了优先队列,所以直接用其存储结点。具体算法如下:
PriorityQueue<Dot> heap = new PriorityQueue<>();
Dot tmp = new Dot(m.getPerson1().getId(), 0);
heap.add(tmp);
HashMap<Integer, Integer> dist = new HashMap<>();
HashMap<Integer, Boolean> flag = new HashMap<>();
// 初始化
for (Person i : people.values()) {
dist.put(i.getId(), 99999999);
flag.put(i.getId(), false);
}
// start
while (heap.size() != 0) {
tmp = heap.peek();
heap.poll();
int pid = tmp.getId();
Person ptmp = getPerson(pid);
int dis = tmp.getDis();
if (!flag.get(pid)) {
flag.replace(pid, true);
for (int i = 0; i < ((MyPerson) ptmp).getAcquaintance().size(); i++) {
Person pac = ((MyPerson) ptmp).getAcquaintance().get(i);
if (dist.get(pac.getId()) > dis + ((MyPerson) ptmp).getValue().get(i)) {
dist.replace(pac.getId(), dis + ((MyPerson) ptmp).getValue().get(i));
Dot tmp1 = new Dot(pac.getId(), dist.get(pac.getId()));
heap.add(tmp1);
}
}
}
}
4.总结
每次作业的算法思路都是:将时间复杂度压进O(n),如果能到O(1)那更好。因为第九次作业我没有使用并查集,导致时间复杂度为O(n^2),强测直接寄了三个点。正是这次的惨痛经历,督促我每次一定要优化,降低时间复杂度。
性能问题和修复
1、第九次作业
偷懒用bfs实现的isCircled,结果性能直接爆炸,所以在bug修复的时候加上了并查集,将复杂度压进O(n)。
2、第十次作业
这一次是qgvs
这个命令会调用两次for循环导致时间复杂度为O(n^2),互测的时候被hack的很惨。因此在MyNetwork
中加入一个Hashmap,对每个Group都维护一个valuesum,在addRelation
、addToGroup
、delFromGroup
里面进行操作,对valuesum进行修改维护,实现O(n)复杂度。
3、第十一次作业
这一次是个小失误,在最短路里面要先初始化一个dist数组,我是直接用的int[],这样就会导致我默认id是正的,因为数组默认从0开始。互测时只有一个人发现了,没那么凄惨。将int[]改成hashmap就修复完成了。
Network扩展
假设出现了几种不同的Person
-
Advertiser
:持续向外发送产品广告 -
Producer
:产品生产商,通过Advertiser来销售产品 -
Customer
:消费者,会关注广告并选择和自己偏好匹配的产品来购买——所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息 -
Person
:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等。讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格。
设计如下:
- 新建
Advertiser
、Producer
、Customer
类,均继承Person
类 Advertiser
类发送的信息定义为Advertise
,继承自Message
,其socialvalue定义为其对人的吸引力。Producer
类有接口直接接收Advertise
,然后定义生产的产品为Product
类,里面包含id、价格等属性。Customer
类新增属性偏好,新增功能购买。- 可以新增方法
buyProduct
、produceProduct
、addAdvertise
、sendAdvertise
、addProduct
等方法
produceProduct
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < productList.length; productList[i] == product);
@ ensures productList[i].num = \old(productList[i].num) + 1
@ also
@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < \old(productList.length); \old(productList[i]) == product);
@ ensures (\exists int i; 0 <= i && i < productList.length; productList[i] == id && productList[i].num = 1);
@*/
public void produceProduct(Product product)
sendAdvertise
/*@ public normal_behavior
@ requires containsM(id)
@ assignable advertise,productInf;
@ ensures !containsM(id) && advertise.length == \old(advertise.length) - 1 &&
@ (\forall int i; 0 <= i && i < \old(advertise.length) && \old(advertise[i].getId()) != id;
@ ensures productInf.length == \old(productInf.length)+1;
@ (\forall int i; 0 <= i && i < \old(productInf.length);
@ (\exists int j; 0 <= j && j < productInf.length; productInf[j].getId() == \old(productInf[i].getId());
@ also
@ public exception_behavior
@ signals (MessageIdNotFoundException e) !containsM(id);
@*/
public void sendAdvertise(int id) throws MessageIdNotFoundException;
bugProduct
/*@ public normal_behavior
@ requires containsM(id) && (getMessage(id) instanceof BuyMessage);
@ requires (getMessage(id).getPerson1() instanceof Customer) && (getMessage(id).getPerson2() instanceof Advertiser);
@ assignable messages, getMessage(id).getPerson1().money;
@ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().money;
@ ensures !containsM(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 (\old(getMessage(id)).getPerson1().getMoney() ==
@ \old(getMessage(id).getPerson1().getMoney()) - ((BuyMessage)\old(getMessage(id))).getMoney() &&
@ \old(getMessage(id)).getPerson2().getMoney() ==
@ \old(getMessage(id).getPerson2().getMoney()) + ((BuyMessage)\old(getMessage(id))).getMoney());
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsM(id);
@*/
public void bugProduct(int id) throws MessageIdNotFoundException;
学习体会
在做完第二单元电梯月后,身心都放松了,因为学长曾说后两个月的oo很简单,因此第九次作业我放松了警惕,虽然很多人说要用并查集,但当时的我觉得不会这么离谱吧,第三单元第一次就搞这么难?结果还真被卡了,从此再也不轻视第三单元。
这个单元学习的JML十分友好,把程序整体框架都告诉你,你只需填空和优化,因此写代码时间骤减。但读代码的时间花费挺久,因为JML的括号极其多,加上每一行必有一个@
,整体看上去就很晕,需要将括号对齐,才能看明白。
虽然这个给写代码带来了极大的遍历,但是要我去写它却是极大的麻烦,研讨课写这个的时候是真的头疼...,前提和后果要先列出来,看想全没(很容易漏了条件),然后才能写对。
总的来说是个较为轻松的单元,这次就不说希望下个单元简单了,因为刚做完第四单元第一次,被血虐了(求放过/(ㄒoㄒ)/~~