【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中指定的红包消息表示购买请求 */
posted @ 2022-06-06 01:52  Elucidator_xrb  阅读(36)  评论(2编辑  收藏  举报