BUAA-OO-Unit3总结
本单元存在四大类:Network,Group,Person,Message。它们的从属等级是层层递降的。
直接由规格可以得到一个架构:Network管理Group,Group组织Person;由Network负责Message的分发。这种架构与现实中的社交网络相吻合。
测试数据的准备
概论
JML表述符合我们大一下学期学过的离散数学之逻辑部分,有鉴于此,我构造测试数据的过程,就像以工程师的方法(而不是数学家的纯粹推理)解一道离散数学的题目:
阅读JML▶️产生一个形象化(图形)的理解▶️尝试在JML规则之内扩展上一步的图形▶️探索图形边界▶️发现坑点,出成数据
以上是手搓数据的大致流程。另外,这次作业涉及到并查集、MST、最短路等基础图论算法,可以去luogu对应板子题白嫖点测试点然后魔改一下,来测试实现的正确性。
由于舍友写了个数据生成器,自动化测试就全部交给对拍完成了。实际结果表明自动生成的数据还不够强,仍存在拍不出来的bug。🐱
实例
1 /*@ public normal_behavior 2 @ requires contains(id1) && contains(id2); 3 @ ensures \result == (\exists Person[] array; array.length >= 2; 4 @ array[0].equals(getPerson(id1)) && 5 @ array[array.length - 1].equals(getPerson(id2)) && 6 @ (\forall int i; 0 <= i && i < array.length - 1; 7 @ array[i].isLinked(array[i + 1]) == true)); 8 @ also 9 @ public exceptional_behavior 10 @ signals (PersonIdNotFoundException e) !contains(id1); 11 @ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2); 12 @*/ 13 public /*@ pure @*/ boolean isCircle(int id1, int id2) throws PersonIdNotFoundException;
(直观感受)在不满足异常条件的情况下,这个方法要求判断a、b之间是否存在一个路径。(边界条件探索)注意到array.length >= 2要求路径至少包含一条边和两个点。又注意到a.isLinked(a) == true,且异常行为判定条件中没有a != b,所以a和a自己也是"isCircle"的,应当返回true。(出点数据)
架构设计
建模
Person | Relation | Group | Network |
---|---|---|---|
Node | Edge | Subgraph | Combination of Subgraphs |
整个单元都是围绕图来做的。
对于isCircle,要做的是判断两个点是否在同一个强连通分量上,即 a直接或间接认识b && b直接或间接认识a。朴素的算法是dfs或bfs,单次复杂度O(n)。由于数据允许动态加点加边,Tarjan可被卡成dfs。观察发现,节点间的边一定是成对存在的,且权值必相等,可以看作是无向边,整张图可以看作无向图。问题转化为求无向图的连通分量,并查集可做。
对于sendIndirectMessage,阅读JML,发现是最短路的板子。
对于queryLeastConnection,阅读JML,发现是最小生成树的板子。由于上面isCircle我们已经维护了并查集,因此可以用Kruskal来做。实际上,我理解的人际关系网应该以稠密图为主,Prim+堆优化要好一些。
维护
Network内用HashMap维护id到Person等类的映射;Group内用HashSet保存Person;Person内用HashSet保存熟人Id,用HashMap保存熟人id到价值的映射,LinkedList保存收到的Message。同时维护了一些变量用于快速响应询问,比如groupValueSum等。
复杂度
最后一次作业的时间复杂度如下:
Operation | Complexity |
---|---|
add_relation | O(n) |
query_value | O(1) |
query_people_sum | O(1) |
query_circle | O(logn) |
query_block_sum | O(1) |
add_group | O(1) |
add_to_group | O(n) |
del_from_group | O(n) |
query_group_people_sum | O(1) |
query_group_value_sum | O(1) |
query_group_age_var | O(n) |
add_message | O(1) |
send_message | O(n) |
query_social_value | O(1) |
query_received_messages | O(1) |
query_least_connection | O(nlogn) |
add_red_envelope_message | O(1) |
add_notice_message | O(1) |
clean_notices | O(1) |
add_emoji_message | O(1) |
store_emoji_id | O(1) |
query_popularity | O(1) |
delete_cold_emoji | O(n) |
query_money | O(1) |
send_indirect_message | O(nlogn) |
可以看到,单次操作复杂度为O(nlogn),总复杂度为O(n2logn),在时限允许的范围内。
性能问题及修复
第二次作业指令量加大,原来能用O(n2)水过的qgvs现在不行了,于是我和我所在的房间T惨了。
修复方法为复杂度均摊,对每个group添加几个维护量,凡涉及到导致维护量变动的操作,都要同时维护维护量。如此,总复杂度从O(n3)均摊到了O(n2)。
其他问题无。
hack
第一次作业对同房间代码简单跑了几个样例,没有发现bug,遂放弃。
第二次作业做到了“不首先使用核武器”。
第三次作业针对新增的指令肉眼探测bug,发现了一个bug。

可以看出作者对removeIf不太熟悉,或对题意理解有误
Network扩展及JML规格
1 /*** 生产产品 ***/ 2 public void addProduct(Product product); // Network 3 /*** 推广产品 ***/ 4 public void addAdvertisementMessage(int advertiserId, int productId, int personId, int cost); // 1对1 5 public void addAdvertisementMessage(int advertiserId, int productId, int gourpId, int cost); // 1对多 6 public void sendAdvertisementMessage(Message message); // Network 7 /*** 购买产品 ***/ 8 public void buyProduct(int customerId, int productId); 9 /*** 查询销售情况 ***/ 10 public int querySales(int id); // 查询某一类商品的销量 11 public int queryAdvertiseCost(int id); // 查询某一类商品的广告费用
生产者生产某一类商品,一类商品可供无限个消费者购买。保证商品种类id唯一。
1 /*** JML ***/ 2 /*** 生产产品。增加一类商品。 ***/ 3 /*@ public normal_behavior 4 @ requires !(\exists int i; 0 <= i && i < products.length; products[i].equals(product)); 5 @ assignable products; 6 @ ensures products.length == \old(products.length) + 1; 7 @ ensures (\forall int i; 0 <= i && i < \old(products.length)); 8 @ (\exists int j; 0 <= j && j < products.length; products[j] == (\old(products[i]))); 9 @ ensures (\exists int i; 0 <= i && i < products.length; products[i] == product); 10 @ also 11 @ public exceptional_behavior 12 @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length; products[i].equals(product)); 13 @*/ 14 public void addProduct(Product product); // Network
1 /*** JML ***/ 2 /*** 购买商品。消费者会购买最近收到的信息内的广告消息对应的商品。如果购买成功,会使生产者加钱,消费者减钱,会改变商品热度。 ***/ 3 /*@ public normal_behavior 4 @ requires contains(customerId); 5 @ requires (\exist int i; 0 <= i && i < \old(queryReceivedMessage(customerId)).length; 6 @ \old(queryReceivedMessage(customerId)[i]) instanceof AdvertisementMessage && containsProduct(\old(queryReceivedMessage(customerId)[i]).getProductId())); 7 @ assignable productHeatList, people.getPerson(customerId).money; 8 @ assignable (\forall int i; 0 <= i && i < \old(queryReceivedMessage(customerId).length) && 9 @ \old(queryReceivedMessage(customerId)[i]) instanceof AdvertisementMessage; 10 @ people.getPerson(\old(queryReceivedMessage(customerId)[i]).getProducer().money); 11 @ assignable people.getPerson(customerId).getMessages(); 12 @ ensures (\forall int i; 0 <= i && i < \old(queryReceivedMessage(customerId)).length; 13 @ (\old(queryReceivedMessage(customerId)[i]) instanceof AdvertisementMessage) ==> 14 @ (people.getPerson(\old(queryReceivedMessage(customerId)[i])).getProducer().money = \old(people.getPerson(\old(queryReceivedMessage(customerId)[i])).getProducer)) + \old(queryReceivedMessage(customerId)[i]).getProduct().getValue()) && 15 @ (people.getPerson(customerId).getMoney() == \old(people.getPerson(customerId).getMoney()) - \old(queryReceivedMessage(customerId)[i]).getProduct().getValue()) && 16 @ (\exist int j; 0 <= j && j < productHeatList.length && productIdList[j] == ((AdvertisementMessage)\old(queryReceivedMessage(customerId)[i]).getProduct().getId()); 17 @ productHeatList[j] == \old(productHeatList[j]) + 1; 18 @ ensures (\forall int i; 0 <= i && i < \old(queryReceivedMessage(customerId)).length; 19 @ !(\old(queryReceivedMessage(customerId)[i]) instanceof AdvertisementMessage) ==> 20 @ \not_assigned(\everything); 21 @ ensures (\forall int i; 0 <= i && i < \old(queryReceivedMessage(customerId).length); 22 @ (\old(queryReceivedMessage(customerId)[i]) instanceof AdvertisementMessage) ==> 23 @ !(\exists int j; 0 <= j && j < people.getPerson(customerId).getMessages().length; people.getPerson(customerId).getMessages()[j].equals(\old(queryReceivedMessage(customerId)[i])))); 24 @ ensures (\forall int i; 0 <= i && i < \old(queryReceivedMessage(customerId).length); 25 @ (!(\old(queryReceivedMessage(customerId)[i]) instanceof AdvertisementMessage)) ==> 26 @ (\exists int j; 0 <= j && j < people.getPerson(customerId).getMessages().length; people.getPerson(customerId).getMessages()[j].equals(\old(queryReceivedMessage(customerId)[i])))); 27 @ ensures people.getPerson(customerId).getMessages().length == (\num_of int i; 0 <= i && i <= \old(queryReceivedMessage(customerId).length); 28 @ (\old(queryReceivedMessage(customerId)[i]) instanceof AdvertisementMessage)); 29 @ also 30 @ public exceptional_behavior 31 @ signals (PersonIdNotFoundException e) !contains(customerId); 32 @*/ 33 public void buyProduct(int customerId);
1 /*** JML ***/ 2 /*** 推广产品。具体而言,在addMessage方法内,生产者掏钱请广告商推广,钱数立即减少;而广告商必须等到sendMessage把广告发送出去后,才能收到报酬;这符合商业契约。这里主要是针对后者,即发送广告这一过程。发送广告会使广告商钱数增加,会使社交网络价值得到提升,会在消费者收到的消息列表中增加该广告。 ***/ 3 /*@ ...... 4 @ public normal_behavior 5 @ requires containsMessage(id) && getMessage(id).getType() == 0 && 6 @ getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) && 7 @ getMessage(id).getPerson1() != getMessage(id).getPerson2(); 8 @ requires getMessage(id).getPerson1() instanceof Advertiser && getMessage(id).getPerson2() instanceof Customer; 9 @ assignable messages, emojiHeatList; 10 @ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money; 11 @ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue, getMessage(id).getPerson2().money; 12 @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 && 13 @ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id; 14 @ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))); 15 @ ensures \old(getMessage(id)).getPerson1().getSocialValue() == 16 @ \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() && 17 @ \old(getMessage(id)).getPerson2().getSocialValue() == 18 @ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue(); 19 @ ...... 20 @ ensures (\old(getMessage(id)) instanceof AdvertisementMessage) ==> 21 @ (\old(getMessage(id)).getPerson1().getMoney() == \old(getMessage(id).getPerson1().getMoeny()) + \old(getMessage(id).getCost)); 22 @ ...... 23 @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id))); 24 @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1; 25 26 @ also 27 @ public normal_behavior 28 @ requires containsMessage(id) && getMessage(id).getType() == 1 && 29 @ getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()); 30 @ requires getMessage(id).getPerson1() instanceof Advertiser; 31 @ assignable people[*].socialValue, messages, getMessage(id).getPerson1().getMoney; 32 @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 && 33 @ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id; 34 @ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))); 35 @ ensures (\forall int i; 0 <= i && i < people.length && people[i] instanceof Customer && \old(getMessage(id)).getGroup().hasPerson(people[i]); 36 @ people[i].getSocialValue() == \old(people[i].getSocialValue()) + \old(getMessage(id)).getSocialValue()); 37 @ ensures (\forall int i; 0 <= i && i < people.length && (!\old(getMessage(id)).getGroup().hasPerson(people[i]) || (\old(getMessage(id)).getGroup().hasPerson(people[i]) && !(people[i] instanceof Customer)); 38 @ \old(people[i].getSocialValue()) == people[i].getSocialValue()); 39 @ ...... 40 @ ensures (\old(getMessage(id)) instanceof AdvertisementMessage) ==> 41 @ (\old(getMessage(id)).getPerson1().getMoney() == \old(getMessage(id).getPerson1().getMoney()) + \old(getMessage(id)).getCost * \old(getMessage(id)).getGroup().getSize()); 42 @ ensures (\forall int i; 0 <= i && i < people.length && 43 @ \old(getMessage(id).getGroup().hasPerson(people[i])) && people[i] instanceof Customer; 44 @ (\forall int j; 0 <= j && j < \old(people[i].getMessages().size()); 45 @ people[i].getMessages().get(j+1) == \old(people[i].getMessages().get(j))) && 46 @ people[i].getMessages().get(0).equals(\old(getMessage(id))) && 47 @ people[i].getMessages().size() == \old(people[i].getMessages().size()) + 1); 48 @ ...... 49 @ also 50 @ public excetpional_behavior 51 @ ...... 52 @ signals (WrongPersonTypeExcetpion e) containsMessage(id) && getMessage(id).getType() == 0 && 53 @ (getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2())) && 54 @ ((getMessage(id).getPerson1() instanceof Advertiser) || (getMessage(id).getPerson2() instanceof Customer); 55 @ signals (WrongPersonTypeExcetpion e) containsMessage(id) && getMessage(id).getType() == 1 && 56 @ (getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1())) && 57 @ ((getMessage(id).getPerson1() instanceof Advertiser); 58 @*/ 59 public void sendMessage(int id);
真难写。
学习体会
JML实现部分
JML撰写部分:真要命。写出一个严谨的JML规格,难度不亚于实现一个JML规格。
开闭原则:尽量不去改动过去已经实现的代码。第三次作业加入了两种新的消息,相应的改了JML中sendMessage部分。根据开闭原则,我们在实现的时候并不一定要去改变上次写好的sendMessage,完全可以另起炉灶。利用java的方法重载特性,适当的改变第二次作业的参数列表,比如增加一个标志位、改变参数类型,然后再重写能支持三种信息的sendMessage方法,让新方法内部调用旧方法,也不失为一种选择。