【Unit3】社交系统模拟(JML规格化设计)-作业总结
第三单元作业难度在OO课程中当属最低。原因在于最复杂多变(贻害无穷)的设计环节被作业接口和JML规格描述限定,我们不再需要考虑整体的构架(抽象出那些类,设置哪些方法等),唯一的能动性仅在具体实现和复杂度控制上。思考的维度降低了,自然会觉得轻松许多。长期的思维懈怠也导致本人开始写第四单元作业时受到惊吓
于是本单元作业的重点是JML理解、复杂度和规格检验。
1 第一次作业
1.1 题目概述
- 依据接口和JML实现MyPerson,MyGroup,MyNetwork
- 实现6个具有计数功能的异常类
最终支持命令:
add_person id(int) name(String) age(int) //ap
add_relation id(int) id(int) value(int) //ar
query_value id(int) id(int) //qvi
query_people_sum //qps
query_circle id(int) id(int) //qci
query_block_sum //qbs
add_group id(int) //ag
add_to_group id(int) id(int) //atg
del_from_group id(int) id(int) //dfg
1.2 关键实现
首次实现时,憨憨得按JML一板一眼地写,十分顺畅。除了MyPerson中明显的键值对关系,其余全用了ArrayList;各类查找方法均为for循环。所幸指令的方法调用叠加产生的复杂度没有超过O(n2),没出大问题。
倘若书写代码时出现容器选择权衡的困难,或对全局任务构架不够清晰,不妨直球选择最熟悉的ArrayList以保证思路流畅,心情愉悦。待到构架搭好纵观全局时,便可针对性优化,神清气爽利于bug难产。
1.2.1 难点命令--qci、qbs(isCircle()、queryBlockSum())
根据JML可知,本质需求:判断社交关系图上任意给定两个结点的连通性。
若根据JML实现,queryBlockSum()
表面复杂度O(n2),调用isCircle()
;isCircle()
采用如dfs,不妨记复杂度O(n2)。qbs总复杂度可达O(n4)。超过题干限制,所以应选择算法降低复杂度。
命令上限1000,时间限制2s,构造命令序列类似{999ap,qbs}:9994数量级为1012,若计算机1秒运行1010数量级指令,则超时
本质需求十分适合使用并查集,在ap、ar命令中维护,理论上可将isCircle()
复杂度降为O(1),queryBlockSum()
也可不调用isCircle()
,自身复杂度改为O(n)。
/* 增设并查集 HashMap<pid, father_pid> */
private final HashMap<Integer, Integer> ufSet;
/* 查 + 路径压缩 */
private int find(int id) {
int father = ufSet.get(id);
if (id != father) { // also (ufSet.get(father) != father)
ufSet.put(id, find(father));
}
return ufSet.get(id);
}
/* 并 */
private void union(int id1, int id2) {
ufSet.put(find(id1), find(id2));
}
如此,isCircle主干仅需一句find(id1) == find(id2)
。日后若想知道两个人是否认识(是否连通),可代替有异常处理的isCircle()
,轻便简洁。queryBlockSum()
即为数ufSet中有几个不同的值。
1.2.2 计数异常类实现
一般在异常中加上static的cnt便可。不过当时脑子里全是上单元流水线模式中使用的单例计数器,便将所有的数据统一到一个单例模式的ExceptionCounter中:
private HashMap<String, HashMap<Integer, Integer>> e2Record;
/* <exc, <id, cnt>> */
public void initial() {
e2Record = new HashMap<>();
e2Record.put("pinf", new HashMap<>());
e2Record.put("epi", new HashMap<>());
e2Record.put("rnf", new HashMap<>());
e2Record.put("er", new HashMap<>());
e2Record.put("ginf", new HashMap<>());
e2Record.put("egi", new HashMap<>());
...
}
不知道有没有多余的开销
1.3 bug记录与分析
本单元作业强测均100,互测均未被hack。
同房bug多为性能问题,hack时针对特定复杂度可能出问题的指令进行满负荷轰炸。JML细节方面有一个group上限1111的设定,不过认真看JML就不会有问题。
由于此次模型并非静态图,会随命令而实时变化。所以缓存/延迟更新等操作未必带来优化。
后续便不设置此模块了。
2 第二次作业
2.1 题目概述
- 增加MyMessage类,MyNetwork、MyPerson中方法
- 增加2个计数异常类
增设命令:
query_group_people_sum id(int) //qgps
query_group_value_sum id(int) //qgvs
query_group_age_var id(int) //qgav
add_message id(int) socialValue(int) type(int)//am
person_id1(int) person_id2(int)|group_id(int)
send_message id(int) //sm
query_social_value id(int) //qsv
query_received_messages id(int) //qrm
query_least_connection id(int) //qlc
2.2 关键实现
加入了消息类:对群消息,对人消息。
对拍时用时几乎是zwh的一倍,开始优化。然而优化完,测试时间几乎没怎么变化还是比zwh慢,有些怪,可能是构建的开销和查询有所抵消。
2.2.1 复杂度优化
一看吓一跳,上次作业流毒不尽:
- 基石方法(如
getPerson(id)
,contains(id)
,queryValue(person)
等)都是照抄JML,复杂度均为O(n),那随便在O(n2)的方法中用就爆了嘛; - MyGroup中计算平均年龄O(n),计算年龄方差调用了平均年龄,总的复杂度O(n2)*O(n)
毛咕咕这次作业O(n3)的方法就可能超时
于是进行了优化:
- MyGroup中存储valueSum和ageSum并维护,使相应计算降到O(1)
- MyGroup中改用容器HashSet存people,使相应查找理论上降到O(1)
- MyNetwork中改用容器HashMap存people、groups、message,键值对均为为<id, refType>,配上refType相应类中的getId()方法,无论从id->refType还是refType->id均为O(1)
- HashMap的引入将所有基石方法将为O(1)
若将指令分为构建命令和查询命令,那我所谓的优化不过是将查询命令的复杂度散入构建命令中。倘若全是构建命令,则优化失效。然而查询命令的耦合度一般较高,几个复杂度的堆叠便会产生高复杂度导致超时,所以此般“散财”还是有性能可图的。同时这也在用空间换时间。
2.2.2 难点指令--qlc(queryLeastConnection())
理解JML后可知,本质需求:求社交关系图中某结点的最小生成树路径和
阅读了Kruskal、Prim、Boruvka算法,感觉从已知点出发的Prim算法最好实现。找最小边没有采取堆优化,复杂度就是O(n2),不过限制下超不了时就好
其实之前的并查集更适合Kruskal的实现。
但我当时觉得遍历people获取连通图所有边的操作中people人太多(然而其实一样),而且排序算法复杂度不好说(然而我们应该相信Java官方的排序),就没用Kruskal。
int num = 0; // 连通图结点数量,用作终止条件:要将所有连通图的点加入verSet中
for (Integer pid : ufSet.keySet()) {
if (find(id) == find(pid)) {
num += 1;
}
}
HashSet<Person> verSet = new HashSet<>(); // 选入最小生成树的点集
HashMap<Person, Integer> edgSet = new HashMap<>(); // key:当前verSet下可直达的点 value:到这个点的最小边
int total = 0; // 所求值:最小生成树路径和
Person person = getPerson(id);
edgSet.put(person, 0);
do {
person = getVertexOfMinEdge(edgSet); // 该方法选择edgSet中value最小的key。遍历O(n)算法没有优化,而且因为要更新键值对,好像不方便用ProirQueue优化。这么看来应该用Kruskal算法的
verSet.add(person); // 将这个点加入verSet
total += edgSet.get(person); // 更新路径和
edgSet.remove(person); // 将这个点移出edgSet
for (Person p : ((MyPerson) person).getAcquaintances()) { // 根据新加入varSet的点更新edgSet
if (verSet.contains(p) || (edgSet.containsKey(p) && edgSet.get(p) < person.queryValue(p))) {
continue;
}
edgSet.put(p, person.queryValue(p));
}
} while (verSet.size() < num); // 判断是否结束
return total;
不过虽然这里用Prim不是最优,但和下次作业的算法实现倒几乎一模一样,提供了方便。主要烦恼就是好像很难做堆优化(优先队列?)
3 第三次作业
3.1 题目概述
- 增加MyMessage子类MyMessageEmoji、MyMessageNotice、MyMessageRedEnvelope,MyNetwork中新方法
- 增加2个计数异常类
增设命令:
add_red_envelope_message id(int) money(int) type(int) //arem
person_id1(int) person_id2(int)|group_id(int)
add_notice_message id(int) string(String) type(int) //anm
person_id1(int) person_id2(int)|group_id(int)
clear_notices id(int) //cn
add_emoji_message id(int) emoji_id(int) type(int) //aem
person_id1(int) person_id2(int)|group_id(int)
store_emoji_id id(int) //smi
query_popularity id(int) //qp
delete_cold_emoji limit(int) //dce
query_money id(int) //qm
send_indirect_message id(int) //sim
3.2 关键实现
加入了三种特殊消息,引入了JML的层次(继承)规格描述。奇怪的是这次对拍就比zwh快了,明明没有较优的操作。
{群发消息,单发消息}\(\times\){普通消息,通知消息,表情消息,红包消息}
在写sendMessage()处理时稍微犹疑了一下该怎么方便嵌套这种乘法原理的分类
注意点:群发红包消息的分法需注意JML(所谓群发,都会包括自己);消息的迭代删除如果懒得自己设置迭代器可以用removeif
3.2.1 关于类型层次规格
子类在实体上包含父类;父类在语义上包含子类。
LSP原则:在任何父类出现的地方,都可以替换为子类
- 子类的规格可以对父类进行扩充,但需要保持父类的规格仍然成立。
- 子类重写方法规格可以减弱父类方法规格的requires,或者加强父类方法规格的ensures
3.2.2 难点指令--sim(sendIndirectMessage())
据JML可知,该方法将消息传给可以通过熟人链相连的陌生人。本质需求:求社交关系图中两个结点的最短路。若找到则发送,找不到则不操作(这点容易灯下黑误实现,连通性的判断应该在发消息前,之前对拍的3个人同时错了这里)。
查阅了Floyd、Bellman-Ford(SPFA,没读懂直接不管了)、Dijkstra算法。果然还是经典的Dijkstra老爷子好。而且和之前Prim实现相似,虽然O(n2)不过挺难卡的。
if (find(p1.getId()) != find(p2.getId())) { //需先判断这两人是否连通
return -1;
}
HashMap<Person, Integer> finSet = new HashMap<>(); // 已求得最短路的点集。 key:点 value:到该点的最短路径 (初始为 {起始点:0})
HashMap<Person, Integer> tarSet = new HashMap<>(); // 在当前已求得最短路的点集基础上,可直达的点集。 key:点 value:在当前已求得最短路的点集的基础上,到达该点的最短路径
Person person;
int value2p;
int value2person;
tarSet.put(p1, 0);
while (true) {
person = getVertexOfMinDistance(tarSet); // 还是遍历算法,修改了名字分别为Prim和Dijkstra使用
value2person = tarSet.get(person);
if (person.equals(p2)) { // 如果获取的人就是目标,则可以结束循环
return value2person;
}
finSet.put(person, value2person);
tarSet.remove(person);
for (Person p : ((MyPerson) person).getAcquaintances()) { //更新tarSet,即松弛操作
if (finSet.containsKey(p)) {
continue;
}
value2p = value2person + person.queryValue(p);
if (!tarSet.containsKey(p) || tarSet.get(p) > value2p) {
tarSet.put(p, value2p);
}
}
}
4 规格检验方式
本单元推荐的测试方式为JUnit4,基于JML书写测试代码。然而我除了在第一次作业中建了个JUnit4模板外,便没再用过,感觉是一大缺失。还是使用了全局的Python数据生成+对拍姬。其实主干都是pxt写的,我拿来改了改,优化了一些代码构架而已(学到了按字典值权重随机产出命令的方式)
inst = random.choices(list(weight_set.keys()), weights=list(weight_set.values()), k=1)[0]
debug几乎都是对拍姬和同伴们jar包的功劳,除了第三次作业那儿一个共同错误以外(着实太巧了,以及发现这个错误的是pxt)
对测评的倦怠像是天生的,虽然经历了第二单元的教训,还是能别人代劳就不自己动手,改代码倒是很兴奋,喜欢拾人牙慧。JUnit4的Training就稍微尝试了下就懒待做了。
倒还像是能力问题,自己做完那点基本任务就想摸鱼不想干活了;而且除去基本摸鱼外,一周的时间和精力差不多就被各种任务占满了。似乎并不能一直以工作写代码为乐(只是时不时在有这种心境,鉴定为成就感驱使),这种人在计算机行业能活下去嘛(
5 心得体会
- 掌握了JML的基本阅读能力
- 对图的一些基本算法进行了回顾复习(毕竟DS图学得不多,大部分也没自己用C实现过)
- 在迭代中体验了用缓存优化性能的方法;体验了容器选择困难症
- 稍微了解了下PriortyQueue还有Map.Entry
以小见大,似乎能窥见代码工业化体系的恐怖。彼时彼刻似乎还听了关于航天嵌入式系统开发和测试的讲座。只能说,大多数人脑是弱小的(巨佬除外),而系统、软件是强大的,这之间的桥梁便是方法学、规格、测试...工业产品的宏伟瑰丽之下是无数人在机械体系下枯燥劳动。
我喜欢计算机,不过是被这能从无到有创造一切的神奇所吸引。而随着我潜入计算机从数字电路到上层应用的学习过程,并深受个中繁杂事项侵扰时,便开始怀疑:我是否最终能驾驭这些代码,掌控这个机器?我所谓的喜欢是否会变质?
这两年来除了课内那点东西,技术知识水平几乎没有,闲暇时看番打游戏仍然是第一诱惑。但还是会对神奇技术表示极大的热情,或许大约还有救(
↑刚脑海里冒出几个想做的项目,血又沸腾起来。大概就是喜新厌旧。。
6 思考题
6.1 题干:Network拓展
新增三个Person的子类
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格
6.2 设计
新增AdvertisementMessage类继承Message类,Network新增
/*@ public instance model non_null Advertiser[] billBoard; *@/
👇太繁琐了,承认下面的JML书写仅大概表个意
setCustomerFocus()——消费者订阅广告商
/*@ public normal_behavior
@ requires (contains(ctId) && (getPerson(ctId) instanceof Customer)) &&
@ (contains(adId) && (getPerson(adId) instanceof Advertiser)) &&
@ !getPerson(adId).getFollower().contains(getPerson());
@ assignable getPerson(adId).getFollowers();
@ ensures \old(getPerson(adId).getFollowers().size()) == getPerson(adId).getFollowers().size() - 1;
@ ensures (\forall int i; 0 <= i && i < \old(getPerson(adId).getFollowers().size());
@ (\exists int j; 0 <= j && j < getPerson(adId).getFollowers().size();
@ getPerson(adId).getFollowers().get(j) == \old(getPerson(adId).getFollowers()).get(i)));
@ ensures (\exists int i; 0 <= i && i < getPerson(adId).getFollowers().size();
@ getPerson(adId).getFollowers().get(i) == getPerson(ctId));
@ also
@ public exceptional_behavior
@ signals (CustomerIdNotFoundException e) !contains(ctId) || !(getPerson(ctId) instanceof Customer);
@ signals (AdvertiserIdNotFoundException e) (contains(ctId) && (getPerson(ctId) instanceof Customer)) &&
@ (!contains(adId) || !(getPerson(adId) instanceof Advertiser));
@ signals (CustomerFocusExistException e) (contains(ctId) && (getPerson(ctId) instanceof Customer)) &&
@ (contains(adId) && (getPerson(adId) instanceof Advertiser)) &&
@ getPerson(adId).getFollowers().contains(getPerson());
@*/
public void setCustomerFocus(int ctId, int adId) throws CustomerIdNotFoundException, AdvertiserIdNotFoundException, CustomerFocusExistException;
/* 将Customer添加到Advertiser的订阅列表Follower中 */
sendAdvertiseMessage()——广告商发布广告
/*@ public normal_behavior
@ requires (containsMessage(admIdId) && (getMessage(admId) instanceof AdvertiseMessage)) &&
@ (contains(adId) && (getPerson(adId) instanceof Advertiser));
@ assignable messages[], billBoard[], people[*].messages;
@ ensures \old(messages.length) == messages.length + 1;
@ ensures (\exist int i; 0 <= i && i < \old(messages.length);
@ (\forall int j; 0 <= j && j < messages.length; !messages[j].equals(\old(messages[i]))));
@ ensures \old(billBoard.length) == billBorad.length - 1;
@ ensures (\forall int i; 0 <= i && i < \old(billBoard.length);
@ (\exists int j; 0 <= j && j < billBoard.length; billBoard[j].equals(\old(billBoard[i]))));
@ ensures (\exists int i; 0 <= i && i < billBoard.length; billBoard[i] == getMessage(admId));
@ ensures (\forall int i; 0 <= i && i < getPerson(adId).getFollowers().size();
@ getPerson(adId).getFollowers().get(i).getMessages().containsMessage(getMessage(admId))) &&
@ \old(getPerson(adId).getFollowers().get(i).getMessages().size()) == getPerson(adId).getFollowers().get(i).getMessages().size() - 1 &&
@ (\forall int j; 0 <= j && j <= \old(getPerson(adId).getFollowers().get(i).getMessages().size());
@ (\exists int k; 0 <= k && k <= getPerson(adId).getFollowers().get(i).getMessages().size();
@ getPerson(adId).getFollowers().get(i).getMessages().get(k) == \old(getPerson(adId).getFollowers().get(i).getMessages()).get(j))));
@ also
@ public exceptional_behavior
@ signals (AdvertiseMessageIdNotFoundException e) !containsMessage(admIdId) || !(getMessage(admId) instanceof AdvertiseMessage);
@ signals (AdvertiserIdNotFoundException e) (containsMessage(admIdId) && (getMessage(admId) instanceof AdvertiseMessage)) &&
@ (!contains(adId) || !(getPerson(adId) instanceof Advertiser));
@ */
public void sendAdvertiseMessage(int admId, int adId) throws AdvertiseMessageIdNotFoundException, AdvertiserIdNotFoundException;
/* 发送广告,向所有follower私发一份广告,同时向公共billBoard添加一份广告 */
sendPurchaseMessage()——消费者委托广告商向生产商发送购买请求
/*@ public normal_behavior
@ requires (contains(ctId) && (getPerson(ctId) instanceof Customer)) &&
@ (contains(adId) && (getPerson(adId) instanceof Advertiser)) &&
@ (contains(pdId) && (getPerson(pdId) instanceof Producer)) &&
@ requires getMessage(mid) instanceof RedEnvelopeMessage;
@ requires (getPerson(adId).getFollowers().contains(ctId) && getPerson(adId).getRegisters().contains(pdId));
@ assignable messages[], getPerson(pdId).messages[];
@ assignable getPerson(pdId).money, getPerson(ctId).money;
@ ensures \old(messages.length) == messages.length + 1;
@ ensures (\exist int i; 0 <= i && i < \old(messages.length);
@ (\forall int j; 0 <= j && j < messages.length; !messages[j].equals(\old(messages[i]))));
@ ensures \old(getPerson(pd).getMessages().size()) == getPerson(pd).getMessages().size() - 1;
@ ensures (\forall int i; 0 <= i && i < \old(getPerson(pd).getMessages().size());
@ (\exists int j; 0 <= j && j < getPerson(pd).getMessages().size();
@ getPerson(pdId).getMessages().get(j).equals(\old(getPerson(pdId).getMessages()).get(i))));
@ ensures (\exists int i; 0 <= i && i < getPerson(pd).getMessages().size();
@ (\exist int i; 0 <= i && i < \old(messages.length);
@ getPerson(pdId).getMessages().get(i).equals(messages[i])));
@ also
@ public exceptional_behavior
@ signals (CustomerIdNotFoundException e) !contains(ctId) || !(getPerson(ctId) instanceof Customer);
@ signals (AdvertiserIdNotFoundException e) (contains(ctId) && (getPerson(ctId) instanceof Customer)) &&
@ (!contains(adId) || !(getPerson(adId) instanceof Advertiser));
@ signals (ProducerIdNotFoundException e) (contains(ctId) && (getPerson(ctId) instanceof Customer)) &&
@ (contains(adId) && (getPerson(adId) instanceof Advertiser)) &&
@ (!contains(pdId) || !(getPerson(pdId) instanceof Producer));
@ signals (PurchaseRelationLinkNotFound e) (contains(ctId) && (getPerson(ctId) instanceof Customer)) &&
@ (contains(adId) && (getPerson(adId) instanceof Advertiser)) &&
@ (contains(pdId) && (getPerson(pdId) instanceof Producer)) &&
@ (!getPerson(adId).getFollowers().contains(ctId) || !getPerson(adId).getRegisters().contains(pdId));
@ */
public void sendPurchaseMessage(int ctId, int adId, int pdId, int mid) throws CustomerIdNotFoundException, AdvertiserIdNotFoundException, ProducerIdNotFoundException, PurchaseRelationLinkNotFound;
/* Customer指示Advertiser向Producer发送一个messages中指定的红包消息表示购买请求 */