BUAA_OO第三单元总结

A u t h o r : g p f \mathcal{Author:gpf} Author:gpf
博客阅读体验更佳 OO第三单元总结 - solor-wind’s blog

题目概述与架构

维护一个简单的社交网络,对其中的用户和彼此之间的关系、标签、消息进行管理

  • 第9周:维护一个图,修改操作包括:添加节点、添加/删除/修改边,主要查询操作包括:查询两节点是否联通、查询极大联通子图的数量、查询三元组的数量
  • 第10周:增加tag(子图),新增修改操作:向某个节点添加标签、向标签内添加/删除节点,新增查询操作:子图内所有边权之和、两节点之间的最短路径(边权视为1)、互为最好朋友的节点对个数(即彼此边权最大)
  • 第11周:增加message,新增修改操作:添加/删除/发送消息,新增查询操作:查询消息相关

在这里插入图片描述

算法与时间复杂度

我宣布,这是BFS的胜利,是摆烂人的胜利

课程组限制:10000条指令,CPU最大运行时间10s,最多使用760MB空间

显然,程序的整体时间复杂度不能超过 O ( n 2 log ⁡ n ) O(n^2\log n) O(n2logn) ,也意味着每个方法的时间复杂度不能超过 O ( n log ⁡ n ) O(n\log n) O(nlogn)

第9周

主要难点在于 qci(query_circle)qbs(query_block_sum)qts(query_triple_sum) 这三条指令

  • qci:查询两点之间是否存在路径。两点、路径,最先想到的是dijstra算法,但事实上这一询问只要求返回是否存在,因此可以考虑其他实现更加简单、时间复杂度更优的算法。往届大都采用并查集,但本次第一次作业就加入了删除操作,使得并查集的动态维护变得复杂,因此被我放弃(主要是因为懒)。最终我采用了BFS,单次查询时间复杂度 O ( V + E ) O(V+E) O(V+E) ,即 O ( n ) O(n) O(n) ,可以通过。(dfs可能爆栈)
  • qbs:查询图中极大联通子图的数量(相当于有几个连通块)。同样采用BFS解决
  • qts:查询图中三元组(triple,三个节点互相连接)的数量。这个没办法BFS了,只能通过动态维护实现。当新增边/删除边时,遍历这条边的两个节点的公共节点,增加/删除 triple 的数量。维护的时间复杂度 O ( n ) O(n) O(n) ,查询时间复杂度为 O ( 1 ) O(1) O(1)

第10周

主要难点在于 qtvs(query_tag_value_sum)qbs(query_best_acquaintance)qcs(query_couple_sum)qsp(query_shortest_path) 四条指令

  • qtvs:查询子图中的边权之和。采用类似BFS的方法实现,遍历tag(子图)中的每一个节点,遍历与节点直接相连的每一条边,若这条边的另一节点也在子图中,则加上对应权重。看似两层循环,时间复杂度为 O ( n 2 ) O(n^2) O(n2) ,但实际上最多对每条边遍历2次,因此类似BFS,仍然为 O ( n ) O(n) O(n)
for (int i : persons.keySet()) {//注意第二层不能改为遍历子图节点,否则真就变成O(n^2)了
    for (int j : ((MyPerson) persons.get(i)).getAcquaintance().keySet()) {
        if (persons.containsKey(j)) {
            valueSum += persons.get(i).queryValue(((MyPerson) persons.get(i)).getAcquaintance().get(j));
        }
    }
}
  • qba:查询某人的最好朋友是谁(节点连接的所有其他节点中,边权最大且序号最小的)。动态维护有序的数据,可以采用TreeMap。具体采用 TreeMap<Integer, TreeSet<Integer>> 的嵌套方式。第一层以边权为key,Set为value;第二层存储id。这样就可以用 map.get(map.lastKey()).first() 的方式得到边权最大的set中的id最小的结果。动态维护时间复杂度为 O ( log ⁡ 2 n ) O(\log^2 n) O(log2n) ,单次查询时间复杂度为 O ( 1 ) O(1) O(1)
  • qcs:查询couple(彼此的关系(边权)最好且序号最小)的数量。解决了qba之后,qcs就十分简单——直接遍历当前所有节点,对符合“某人的最好朋友的最好朋友是自己”的节点进行计数即可,最后除以二。时间复杂度 O ( n ) O(n) O(n)
  • qsp:查询一个节点到另一个节点之间最少需要经过几个节点。我一看——这不是单源最短路么!——堆优化dij秒了。后来发现由于边权视为1,直接用BFS就行,但是已经写完dij了,又懒得改了。堆优化dij时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn) ,BFS依然 O ( n ) O(n) O(n)

第11周

架构上,network中用HashMap存储Message,而Person中用LinkedList存储Message。对于emojiMessage,则采用 HashMap<Integer,Integer> 的方式存储emojiId和heat。同时,MyEmojiMessage等类还要继承MyMessage类,避免大量重复代码。

性能方面基本不会有太多问题,唯一需要注意的是emojiMessage相关的操作,不仅要区分messageId和emojiId,还要知道什么时候删除、从哪些容器中删除

本单元的测试过程

黑箱测试与白箱测试

黑箱测试顾名思义,把测试对象看作一个黑箱,完全不考虑程序内部的逻辑结构和内部特性而检查程序的功能是否符合它的功能说明。在本次作业中就对应不看程序代码而只测试正确性,此时需要结合JML,构造覆盖率较高、较为全面的测试点进行测试。

白箱测试则相反,程序内部的逻辑结构和内部特性完全可见,此时不仅要通过测试点对齐进行测试,还要分析其内部代码结构是否符合功能规范。本次作业中对应自己和互测过程中的静态代码检查,通过阅读代码逻辑来找到和JML不一致的地方,进行有针对的测试。

各种测试

单元测试

单元测试,是指对软件中的最小可测试单元进行检查和验证。在这次作业中,对应于对每个方法的测试,要结合JML的约束来测试,比如 pure 约束要求不能有任何副作用。

功能测试

功能测试就是对产品的各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求的功能。在这次作业中就要求对社交网络的各个功能/指令去进行测试,看是否正常运行。

集成测试

在单元测试的基础上,将所有模块按照设计要求组装成为子系统或系统,进行集成测试。在本单元的作业中,相当于各个方法之间互相调用、各个类之间相互合作的测试,即测试过程中对多个方法的联合调用进行测试。

压力测试

压力测试,一般为长时间或超大负荷地运行测试软件来测试被测系统的性能、可靠性、稳定性等。在本次作业中就明显的表现为对时间复杂度过高的方法集中调用以测试是否超时。

回归测试

回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。具体表现为每次迭代开发的过程中,不仅要对新引入的指令、方法进行测试,还应对之前的方法进行测试以防错误修改出现bug

评测机与手动数据构造

评测机

本次依然与zx合作完成评测机的搭建,主要负责数据生成部分。由于能够卡时间、测试性能的样例基本不可能随机构造出来,因此本次数据生成主要针对正确性部分,而性能测试点手动构造完成。具体思路如下:

首先将随机生成指令数量,第三次作业默认将40%的指令用于建图,15%用于添加tag,35%用于添加消息,10%用于查询,比例可调。

具体代码已经开源https://github.com/solor-wind/BUAA_OO_TEST

建图

首先不能纯随机建图,否则生成的图过于稀疏,强度过低,覆盖不到所有情况。因此,采用的洛谷的数据生成器,首先生成一个较为稠密的图,然后随机添加散点,再随机添加散点到稠密图之间的边。这样可以保证稠密图、线状图、散点三者皆有,覆盖尽可能多的情况。

添加节点

tag相关指令要求子图内的节点数量不能过少,因此采用了类似建图的方法。先添加一些比较大的子图,再随机生成相关tag

添加消息

由于消息必须要两个节点之间有边或者tag存在才有效,因此上述两个过程需要保留已经建立的边和tag相关数据,方便生成有效的消息。

查询

随机查询,其中查询已有节点、tag等指令占查询的80%,其余为随机查询。

手动数据构造

大多数情况下,评测机可以覆盖较多的正确性检验情况,手动数据构造在本单元一般用于压力测试。

下面以第一次作业中的 qci(query_circle) 指令为例,构造压力测试

针对BFS,构造如下数据:使用ln构造300个点的完全图,而后再用6000条数据构造一条长链,共计3300个点的连接图,而后剩余指令查询两端是否连通。

针对DFS,构造如下数据:使用ln构造300个点的完全图,而后再用9990条数据构造一条长链,共计5295个点的连接图,而后剩余指令查询两端是否连通。

针对并查集和延迟维护/延迟查询,构造如下数据:使用ln构造300个点的完全图,而后再用5000条数据构造一条长链,共计2800个点的连接图,而后剩余指令以下面形式为一组进行删边、加边、查询。

mr 300 301 -1
qci 1 2800
ar 300 301 1

性能bug、规格与实现、Junit

很幸运本单元没有在强测和互测中发现bug,性能bug也没有出现。

对于JML,仅仅是对方法的前提、副作用与结果进行了约束,并未对具体实现有所约束。因此,要采取合适的数据结构进行存储(如用HashMap存储Person),采用合适的算法进行计算(如动态维护triple而非三重循环)

对于Junit,一定要利用规格信息来更好的设计实现Junit测试。本单元的Junit测试提交了多次,大部分原因都是忽略了方法中的 pure 等副作用限制或检查不足。对于副作用的检查,不仅要采用类似影子电梯式的构造方法防止浅拷贝,还要对每一属性进行检查,同时对数据生成也要保证有所覆盖。

心得体会

这个单元的压力相比前两个单元小了很多,主要是因为课程组为我们搭好了总体架构,而且有JML对方法功能进行描述,自己只需严格按JML来并保证时间复杂度不太高即可。

关于JML,这种契约式编程思想主要应用于航空航天等高精尖领域,对代码正确性要求极为严格。实现是一定要注意方法的前提与副作用,不能只专注于功能。

槽点:

  1. 关于代码风格限制。500行的行数限制,然而Runner超过700行,且使用了import*。
  2. 关于时间限制。过于严苛的时间限制,又不公布评测机硬件环境。从公测结束到强测开始,长达好几个小时的时间是否可以均匀测试,而非短时间内高强度并发?
  3. 频繁修改指导书
  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值