BUAA_2022_OO_Unit3_Summary

BUAA_2022_OO_Unit3_Summary

石以砥焉,化钝为利。

第0章 总览


本单元为按照JML规格实现一个带有并查集、最小生成树、最短路径等算法的简单社交网络模拟系统。当然,JML严谨的描述和保姆级别的指导足以让同学写出正确性的代码,但是并不代表“照搬照抄”的代码可以在性能上取得满意的成绩,而且仅仅通过眼花缭乱的JML语言描述,时常会产生误解、也会面对无穷无尽的括号而望洋兴叹,因此,我认为,很好的完成本单元作业,需要做到如下几点:

  • 先理解整个社交系统的各项功能,然后结合JML规格弄清楚自己写的各个类所要维护的属性以及实现的方法;

  • 选择合理的容器管理属性(需要实现快速查询,例如hashMap;需要维护顺序,例如PriorityQueue);

  • 选择合适的算法实现比较复杂的指令,可以适当维护相应的属性和方法,但是需要保证符合规格。

当然,通过了中测也不意味着自己的程序天衣无缝,测试时建议采取下列测试:

  • 编写JUnit测试模块进行功能性测试,但实际上效果比较小,因为符合规格的代码一般不会有功能性问题;

  • 自己编写测试机,依据某几种的指令的组合(包含复杂指令如qbsqgavqlcsim)进行梯度强度测试(随机、0.5w条、1w条、5w条、10w条),以伙伴的标准输出做比较,进行对比,发现错误。

  • 最后我认为最有效的就是,对比JML规格和自己的方法实现,肉眼debug(逃。

上述大约就是第三单元我从反反复复做的事中提取的经验和教训,希望对大家有所帮助。

 

第1章 第九次作业分析


1.1 作业要求

根据给定JML编写符合要求的在限制复杂度内的Java代码。

具体内容为实现GroupNetworkPerson三个类,模拟一个社交网络中的群体、个体及其关系;实现六个抽象异常类,要求具有计数功能。

1.2 图设计与维护

本次作业涉及对图的查询只有query_block_sum,按照JML规格编写,正确性可保证,但是复杂度达到o(n^2),不能满足我们对性能的要求,为了快速查询联通子图的数目,我设计了联通子图类UnionCom,同时在network中维护unioncoms的HashMap,其key为各个联通分支的祖节点,value为对应的联通分支。

UnionCom中维护联通子图中的点集(Person)和无向边集(Relation)。

并查集

祖节点是并查集中的概念,因此我需要简单介绍一下并查集。

  1. 并查集的初始化

    • 并查集设计成hashMap,key为Person的id,具有唯一性,value为该person对应的祖节点,某个人的祖节点就是满足id最小的同时与该人相连的Person。

    • 显然任何一个联通分支的祖节点唯一,因此可做unioncoms的key。

  2. 并查集的维护与更新

    • 当network中add_person<person>后,并查集dsu中新增对<person.getId(), person.getId()>;

    • 当network中add_relation<id1,id2,value>后,并查集查询id1和id2的祖节点是否相同,如果相同,不需要更新dsu;否则更新dsu,使id较大的key变成id较小的key的键对值。

  3. 并查集的查询

    • 查询query_circle<id1,id2>,只需要判断他们的祖节点是否一致。

 

联通分支

有了并查集的概念,我们对于unionComs的维护就变得简单了:

  • 当network中add_person<person>,unionComs中新增对<person.getId(), new UnionCom()>,并把这个人加入联通分枝的点集中;

  • 当network中add_relation<id1,id2,value>,需要注意,我们首先需要判断id1和id2是否处于同一个联通分支(并查集):

    • 如果处于同一分支,对应分支加入该边即可;

    • 如果不处于同一分支,则需要将所处的分支联合:将一个分支中的人和边全部加入另一个分支,然后加入所需要添加的边,同时unionComs中删除无用的分支;

    • 查询query_block_sum,只需要返回unionComs.size()即可。

引入联通分支以后,大大简化了后续迭代的难度。

 

1.3 性能优化

容器选择

本次作业我所有的容器都采用HashMap,有以下原因:

  • 无论是Person还是Group都有唯一的id标示,因此可保证key的唯一性;

  • 查询频繁,HashMap可将查询复杂度控制在常数级别。

由于本次作业查询指令较少,实际上不需要在做优化了

 

1.4 bug分析

自己bug

本次作业在中测、强测中为出现bug,互测中出现query_block_sum的bug,通过上述联通分支的引入解决的bug。

当然在自己做课下测试的过程中,发现了hashMap的一个bug,有一个hashMap以对象作为key,结果引发了错误行为,重写了hashCode()即可,equals()已经写好了,不需要再重新书写,这里附上重写的推荐写法:

  1. 不能包含equals方法中没有的字段,否则会导致相等的对象可能会有不同的哈希值。 (即对类中每一个重要字段,也就是影响对象的值的字段,也就是equals方法里有比较的字段,进行操作)

  2. String对象和Bigdecimal对象已经重写了hashcode方法,这些类型的值可以直接用于重写hashcode方法;

  3. result = 31 result + (dishCode !=null ?dishCode.hashCode() : 0);,这里面为啥用个31来计算,而且很多人都是这么写的。 这是因为31是个神奇的数字,任何数n31都可以被jvm优化为(n<<5)-n,移位和减法的操作效率比乘法的操作效率高很多!

  4. Google首席Java架构师Joshua Bloch在他的著作《Effective Java》中提出了一种简单通用的hashCode算法:

    • 初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;

    • 如果是对象应用(例如有String类型的字段),如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。否则需要为这个域计算一个范式,比如当这个域的值为null的时候(即 String a = null 时),那么hashCode值为0.

 

这里给出评测时可以使用的命令行命令,帮助我们分离时间结果和标准输出:

cd [location] && cat [data] | (time java -jar [.jar] 1> [result]) 2> [time]

具体位置和数据以及结果存放位置可以自己修改。

别人bug

和我一样,存在qbs超时的bug。

hack策略

使用自己编写的python评测机和玩家对拍,筛选出错误输出和超时玩家进行hack,收益甚微。

 

第2章 第十次作业


2.1 作业要求

在第九次作业的基础上,增加首发私聊与群发消息、查询最小关系(查询最小生成树)等功能,并为新增功能实现新的异常类。

 

2.2 图设计与维护

这次作业新增指令query_least_connection<id>,由于我们已经手动维护了联通分支,问题就转换成为了联通图的最小生成树算法。

最小生成树算法

我采用了Kruskal算法,算法不必多说,可以聊一下优化。得到对应联通分支以后,可有以下优化:

  1. 边实现Comparable接口,以对边进行排序:

    public class Edge implements Comparable<Edge> {
    @Override
       public int compareTo(Edge other) {
           if (this.getValue() > other.getValue()) {
               return 1;
          } else if (this.getValue() < other.getValue()) {
               return -1;
          }
           return 0;
      }
    }

    图内调用即可:

    Collections.sort(edges);

事实上我使用过手动维护edges的顺序,采用二分插入和二分查找进行维护查询,但是在后续大数据测试中,却比上述调用接口方法还要慢,应该是维护顺序的成本和query_least_connection的数目限制导致。

  1. 采用并查集优化,判断某条边是否构成环路,逻辑十分简单,只需要判断这条边的起手和终止的祖节点是否相同即可。如果不构成环路,加入这条边即可。

  2. 采用脏位needToChange维护,需要重新计算时当且仅当图结构发生改变,这样可以避免连续的query_least_connection导致反复计算超时(实际上这种手段没有更本解决算法复杂度问题。

 

2.3 性能优化

组内年龄

指令query_group_value_sum<id>,如果按照JML编写,复杂度达到o(n^2),容易超时,因此我们可以考虑每个Group维护sumValue,在add_to_groupadd_relationdel_from_group时维护,虽然维护成本增加,但是查询复杂度课降低至o(1)

指令query_group_age_var<id>,同样的维护sumAgesumAge2,在add_to_groupdel_from_group时维护,根据JML给出的方差算法,可得方差计算公式:

$$
var = \frac{sumAge2 - 2\cdot sumAge\cdot aveAge + n\cdot aveAge^2}{n}\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space
$$

 

2.4 bug分析

自己bug

本次作业在中测、强测未发现bug。互测中出现query_group_value_sum超时,按照上述修改解决

别人bug

同样的,也是query_group_value_sum超时问题。

通过上述bug分析我们可以看到,尽管本单元架构无需设计,算法也较为基础,但是仍然需要计算和控制各操作复杂度。

 

第3章 第十一次作业


3.1 作业要求

在第十次作业的基础上,增加红包首发(单发与群发),首发emoji并对emoji的热门程度排序,完成间接消息发送。

 

3.2 图设计与维护

本次作业设计指令send_indirect_message,需要使用dijkstra最短路径算法。算法内容不在阐述,这里说一下优化:

  1. 使用PriorityQueue手动维护节点顺序,即实现堆优化,压缩复杂度至o((m+n)logn);

  2. 当前选出的节点为目标节点,立即退出;

  3. 遍历当前节点相连且未访问的节点时,使用人的关联数组connections而不是图维护的edges,这样可以节省很多时间遍历无用的节点;

  4. 采用脏位维护最短路径,事实上我原本使用边集数组的方式保存当前的最短路径,原理上可以防止在图结构未发生改变时反复查询带来的消耗,而且dijkstra最短路径算法可计算多条最短路径,但实际上维护成本太大,而且边集数组占用大量内存,得不偿失,遂放弃。

     

3.3 bug分析

自己在中测、强测、互测中均未发现bug,自己测试过程中也未发现bug,且验证了我的优化思路。hack中也未发现bug。但实际上有个同学在send_indirect_message极限数据下时间卡到了8.6s,我认为这已经是超时行为了。

 

第4章 JML测试数据准备


Junit测试

我在第一次作业中,尝试使用Junit进行模块测试,发现junit测试十分有限,而且对指令组合进行测试,并且测试也不强调逻辑性,相较于互测中同学们的智慧,junit并没有发挥什么作用。

自动评测机的构建

为了达到指令组合、JML规格边界、超时检测、异常类正确性分析的效果,我使用python自行构建了U3评测机。

评测机包括:生成数据、终端运行、正确性检测、超时检测四个部分。为了符合题意,我只对生成数据部分做出解释。

get_strong_random_data:数据上限5w条,先加人,加边,加组,加信息,加emojiId、加人到组,然后大量的查询、发送信息、修改组等,测试程序极限下的时间效率。

get_strong_exception_data:在没有添加信息下,多次随机的查询、操作,然后增加信息后,根据异常产生的条件生成大批量符合条件的数据,可分为随机性和组合型。

get_strong_group_checkdata:主要测试query_group_value_sum、query_group_age_var、query_group_people_sum和必要指令的组合,当然先增加必要信息,然后根据这几种指令的JML描述,产生相应的指令,例如query_group_age_var<id>,可以根其JML规格产生相应测试数据:

1)组号不存在的;2)组内无人的;3)组内年龄全为0的;4)组内年龄方差正常计算为小数的;5)多次组内增删后查询的等等;

其余指令同理,可采取组合和分别测试的方式,我推荐后者,方便定位。

get_strong_circle_check_data:主要测试query_block_sum、query_least_connection和必要指令的组合,这几条指令JML描述十分复杂,但是我们以可以根据每个语句块{P,P,P}进行测试,按照规格分别涉及符合规则的、不符合规则的、混合的进行组装,此时我们需要建立常量池去维护既有的信息,方便测试数据的生成,例如我们对于query_least_connection的测试,根据上述原则:

/*@ public normal_behavior
     @ requires contains(id);
     @ ensures \result ==
     @         (\min Person[] subgroup; subgroup.length % 2 == 0 &&
     @           (\forall int i; 0 <= i && i < subgroup.length / 2; subgroup[i * 2].isLinked(subgroup[i * 2 + 1])) &&
     @           (\forall int i; 0 <= i && i < people.length; isCircle(id, people[i].getId()) <==>
     @             (\exists int j; 0 <= j && j < subgroup.length; subgroup[j].equals(people[i]))) &&
     @           (\forall int i; 0 <= i && i < people.length; isCircle(id, people[i].getId()) <==>
     @             (\exists Person[] connection;
     @               (\forall int j; 0 <= j && j < connection.length - 1;
     @                 (\exists int k; 0 <= k && k < subgroup.length / 2; subgroup[k * 2].equals(connection[j]) &&
     @                   subgroup[k * 2 + 1].equals(connection[j + 1])));
     @               connection[0].equals(getPerson(id)) && connection[connection.length - 1].equals(people[i])));
     @           (\sum int i; 0 <= i && i < subgroup.length / 2; subgroup[i * 2].queryValue(subgroup[i * 2 + 1])));
     @ also
     @ public exceptional_behavior
     @ signals (PersonIdNotFoundException e) !contains(id);
     @*/

1)人不存在的;2)人所处分支仅有自己的;3)人所处分支为稠密图,存在多个生成树的,4)存在多个不同的最小生成树的;5)边权值均为理论最大的;6)图本身就是最小生成树的。

当然根据JML一一列举成本较大,可以有选的选取关键信息进行生成。

get_strong_add_and_send_message_data:主要测试add_message、send_message、query_received_messages、add_red_envelope_message、add_notice_message 、clear_notices、add_emoji_message、store_emoji_id、query_popularity、delete_cold_emoji以及必要的指令组合,思想同上,根据JML语句块编写测试数据,进行测试,注意异常和正常行为可以分开测试,提升测试效率。

get_strong_send_indirect_message:主要测试send_indirect_message和必要指令的组合,可分为效率测试和JML规格测试,效率测试:

def get_send_indirect_message(j, maxLength):
   # 98阶完全无向图
   saved_stdout = sys.stdout
   with open("tests/test_point" + str(j) + ".txt", "w+") as file:
       sys.stdout = file
       for i in range(maxLength):
           print("ap", str(i), ''.join(random.sample(string_pool, 10)), random.randint(150, 200), sep=" ")
       for i in range(0, 4000 - maxLength):
           print("ar", random.randint(0, int(maxLength / 2)), random.randint(int(maxLength / 2 + 1), maxLength - 1),
                 random.randint(0, 1000), sep=" ")
       for i in range(500):
           print("arem", str(i), 100, 0, random.randint(0, maxLength - 1), random.randint(0, maxLength - 1), sep=" ")
           print("sim", str(i), sep=" ")
   sys.stdout = saved_stdout  # 恢复标准输出流
   return "tests/test_point" + str(j) + ".txt

规格测试中,考虑不存在信息、发送信息的种类、群组的人数、存在多条最短路径的等等情况着手组建测试数据。

 

第5章 架构分析


本单元作业架构已经固定了,我只是增加了UnionCom、Edge、Node类来维护图结构,其余不变。

image

 

图模型构建和维护策略已在三次作业中仔细分析过了,故不在赘述。

 

第6章 Network扩展

拓展假设

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告

  • Producer:产品生产商,通过Advertiser来销售产品

  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息

  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格

 

任务分析

  • Advertiser, Producer, Customer均继承自Person类。Advertiser可发送广告,Producer可依生产产品,Customer通过自己的喜好和广告购买商品。

  • 关于广告,增加Advertisement类继承Message,其中有商品id、吸引力attraction,其socialValue等于attraction,具体行为类似于noticeMessage

  • 关于商品,新增Product类用于买卖,内置属性编号id(唯一)、价格Price、种类type

  • 人新增拥有商品列表owendProducts[]和喜爱偏好likedProducer新增生产商品列表products[]

  • 可以新增方法addProductsendAdvertisementbuyProductsetLikedType等方法。

 

sendAdvertisement

/*@ public normal_behavior
     @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
     @         getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
     @         getMessage(id).getPerson1() != getMessage(id).getPerson2();
     @ assignable messages, emojiHeatList;
     @ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
     @ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue, getMessage(id).getPerson2().money;
     @ ensures !containsMessage(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)).getPerson1().getSocialValue() ==
     @         \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
     @         \old(getMessage(id)).getPerson2().getSocialValue() ==
     @         \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
     @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
     @         (\old(getMessage(id)).getPerson1().getMoney() ==
     @         \old(getMessage(id).getPerson1().getMoney()) - ((RedEnvelopeMessage)\old(getMessage(id))).getMoney() &&
     @         \old(getMessage(id)).getPerson2().getMoney() ==
     @         \old(getMessage(id).getPerson2().getMoney()) + ((RedEnvelopeMessage)\old(getMessage(id))).getMoney());
     @ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(people[*].money));
     @ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
     @         (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
     @         emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
     @ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);
     @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
     @         \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
     @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
     @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
     @ also
     @ public normal_behavior
     @ requires containsMessage(id) && getMessage(id).getType() == 1 &&
     @           getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1());
     @ assignable people[*].socialValue, people[*].money, messages, emojiHeatList;
     @ ensures !containsMessage(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 (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==
     @         \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue());
     @ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
     @         \old(people[i].getSocialValue()) == people[i].getSocialValue());
     @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
     @         (\exists int i; i == ((RedEnvelopeMessage)\old(getMessage(id))).getMoney()/\old(getMessage(id)).getGroup().getSize();
     @           \old(getMessage(id)).getPerson1().getMoney() ==
     @           \old(getMessage(id).getPerson1().getMoney()) - i*(\old(getMessage(id)).getGroup().getSize() - 1) &&
     @           (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p) && p != \old(getMessage(id)).getPerson1();
     @           p.getMoney() == \old(p.getMoney()) + i));
     @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
     @         (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
     @           \old(people[i].getMoney()) == people[i].getMoney());
     @ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(people[*].money));
     @ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
     @         (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
     @         emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
     @ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);
     @ also
     @ public exceptional_behavior
     @ signals (MessageIdNotFoundException e) !containsMessage(id);
     @ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
     @         !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
     @ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
     @         !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
     @*/
   public void sendAdvertisement(int id) throws
           RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;

 

addProduct

/*@ public normal_behavior
 @ requires !(\exists int i; 0 <= i && i < products.length; products[i].equals(product));
 @ assignable products;
 @ ensures products.length == \old(products.length) + 1;
 @ ensures (\forall int i; 0 <= i && i < \old(products.length);
 @         (\exists int j; 0 <= j && j < products.length; products[j] == (\old(products[i]))));
 @ ensures (\exists int i; 0 <= i && i < products.length; products[i] == product);
 @ also
 @ public exceptional_behavior
 @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length;
 @                                     products[i].equals(product));
 @*/
public void addPerson(Product product) throws EqualProductIdException;

 

setLikedType:

/*@ public normal_behavior
 @ requires (\exists int i; 0 <= i && i < products.length; products[i].type == type);
 @ assignable liked;
 @ ensures liked == type;
 @ also
 @ public exceptional_behavior
 @ signals (TypeNotFoundException e) !(\exists int i; 0 <= i && i < products.length; @ products[i].type == type);
 @*/
public void addPerson(int type) throws TypeNotFoundException;

 

第7章 体会感想

第三单元以社交网络为载体,考察JML阅读和图相关算法,并学习Junit进行单元测试

通过大量JML规格的阅读,让我发现JML是严格的,因为他限制、规定了的方法的作用区域和具体的行为,同时也是灵活的,因为他不限制程序员编写实现的具体细节和逻辑。通过三次作业,我大大熟悉了JML规格,掌握了从JML规格中读取关键信息并翻译成代码的能力,同时也增强了优化程序、使用算法解决问题的能力,也算是收获颇丰吧。

但实际上,本单元难度大打折扣,也失去oo设计架构所带来的快乐,于是我将重心放在评测程序的书写,对于测试也有了更深的理解。

不过无论如何,oo还没有结束,我仍不能掉以轻心。(加油!!!)

posted @ 2022-06-01 00:58  `Demon  阅读(118)  评论(0编辑  收藏  举报