BUAAOO-第三单元总结-社交网络

基于JML规格建立社交网络

0 题目概述与博客说明

第三单元作业的目的是基于课程组所给出的JML规格构建一个支持消息传达的社交网络。图相关的问题,其特点之一就是复杂,结点数目超过100的网络就已经可以被称为复杂网络了。相较于前几单元,本单元对于算法的考察,是隐含在JML之下的。也因此,我将图的构建与维护策略放在了开篇之首,以供读者参考。

1 图的构建与维护策略

图论是优美的,也是复杂的。如何构建、优化图算法,具有很强的应用价值。通常情况下,对于结点数超过100的图,就已经可以被称为复杂网络。对于本单元的作业,其强测数据普遍达到了3000个结点,基于此图所构建的关系数量级已经达到了恐怖的107~108之间,也因此,运行时间与性能是一定会纳入考量之中的。

策略1 连通块合并

本次作业所采取的连通块合并算法本质上是对传统并查集的一个优化。与普通并查集不同,我维护了一个包含连通块内部所有结点的Map,连通块内的每一个结点均含有该Map的引用,这样就避免了传统并查集对于祖先结点维护的困难。
判断两个结点A、B是否处于同一个连通块,只需要判断A的Map中是否含有B。增加关系事实上等价于连通块的合并,可以先判断两个结点是否位于同一个连通块中(即调用isCircle方法),如果不在的话,就修改数目较少的连通块中结点的引用。合并连通块的逻辑如下:

if (!isCircle(id1, id2)) {
    Map<Integer, Integer> linkedPerson = new HashMap<>();
    linkedPerson.putAll(p1.getLinkedPerson());
    linkedPerson.putAll(p2.getLinkedPerson());
    for (Integer id : linkedPerson.keySet()) {
        MyPerson p = (MyPerson) getPerson(id);
        p.setLinkedPerson(linkedPerson);
    }
}

对于本题,如果所有的Map均采用HashMap实现,isCircle方法的复杂度为O(1),基于此方法实现的连通块数目计算(qbs)复杂度可降为O(n)。在课上分享该算法时,有同学质疑此方法会大大增加addRelation的时间复杂度和空间复杂度。事实上,对于一个复杂网络,将其所有结点合并为一个连通块所需要的addRelation与可能使用的addRelation之比为V/[V!*(V-1)!],其中V为该复杂网络结点数目,可以看出,在均摊情况下,addRelation的时间复杂度趋于O(1)。与传统并查集相比,本算法维护了一个Map,空间复杂度是传统并查集的V倍。

时间复杂度上,只有进行了路径压缩的并查集,在时间效率上才可能会与本算法相同,但本算法所带来的开发效率是要远优于传统并查集的,可以看到,用于维护的Map代码只有四行,节省了笔者大部分的开发时间。

策略2 连通块数目计算

在策略1中,计算连通块数目的方法是查询时计算,事实上,连通块数目在建立图时就已经确定了。对于确定的图而言,每加入一个孤立结点,连通块数目+1;在结点A、B之间建立关系时,若A、B不属于同一连通块,则连通块数目-1。因此,我维护了一个blockSum变量,记录当前图的连通块数目,维护成本为O(1),查询成本也为O(1),较之前的连通块数目计算又优化了一个数量级。

策略3 组权重优化

Group中存在对valueSum的查询,其本质是计算一个组中所有边权重的2倍,若每次查询时都进行遍历,时间复杂度为O(n2),容易造成超时。
因此,可以维护一个valueSum的变量,记录group里所有边的权重,在addToGroup和delFromGroup时更新组内权重,此外,在addRelation和sendMessage时,均需要对valueSum进行更新。查询时,返回valueSum*2即可。
同时,可以顺便维护totalAge和totalAgesqrt两个变量,分别记录组内结点的年龄和与年龄平方和。计算年龄平均值,返回totalAge/size即可;计算方差需要用到DX=EX2-(EX)2。这样维护和查询的复杂度均为O(1)。

策略4 Dijkstra搜索缓存

寻找最短路径时,我用了小顶堆优化的Dijkstra,时间复杂度为O(|E|log|V|)。此外,由于Dijkstra算法每次得到的是源点到所有顶点的最短距离,而我们只需要源点到目标点的最短距离,如果不进行记录,这部分数据就丢失了,之后如果想要得到源点到其他顶点的距离,再调用Dijkstra算法,实际上做的是无用功。因此,我额外设置了路径缓存,用于记录每次查询后得到的结果,当缓存中存在最短路径时直接返回最短路径。

策略5 HashMap底层优化

第10次作业结束后的研讨课上,助教和老师指出了一些逻辑上是O(1),但事实上仍然是O(n)的操作。对于HashMap,containsKey(containsValue)等方法本质上等价于get方法,每次需要遍历整个Map,如果多次调用contains方法,再调用get方法;或是在循环中判断contains方法是否为真,从底层实现上会造成方法复杂度几何级增加,因此,可以直接调用get方法来替代contains方法。

2 性能分析

图的构建与维护策略本单元容易出现的性能问题已经在图的构建与维护策略中进行阐明,本节不再赘述。唯一值得注意的一点是,相较于传统并查集算法,我采用的连通块合并算法的时间复杂度略优,而空间复杂度上会稍有劣势,由于每次申请空间需要花费时间,同时JVM的垃圾回收也会花费一定的时间,因此,该算法实际运行时间应与传统并查集几乎一致。
对于社交网络,其构成呈现明显的小世界模型,因此,采用每个连通块之内共享所有节点而言,是具有较强的应用价值的。

3 规格实现策略

JML提供了方法的逻辑关系,相较自然语言而言,更为严谨,但丧失了形而上的总体描述。这种感觉用教员的话来说,就是“或者叫做只看见局部,不看见全体,只看见树木,不看见森林。”
JML将方法之间的逻辑和调用关系全部隐藏起来,只留下了单独每个方法的实现原理,因此实现起来需要自底而上的过程。对于每个单独的方法,体现在先实现exceptional_behavior,再实现normal_behavior;对于不同的类,体现在先构建Person和Message类,再构建Group和Network类。此外,由于JML将方法的逻辑全部以数组的方式体现,因此容器的选择需要在最开始就确定。

4 测试策略

对于JML测试而言,首要任务就是仔细阅读规格,在源头上避免方法实现错误。
通常的测试可以采用Junit,其测试方法类似于上学期计组的testbranch,进行assert操作,可以初步验证方法的正确性。
本单元我的测试主要是利用python中的networX库和随机数模块构造了自动评测机,构造策略如下:

自动评测机的搭建思路

本次自动评测机得益于@王卓浩同学的大力支持,数据生成方面与整体测试框架的绝大部分出自他手。本次数据生成采用了概率矩阵的方法,通过修改指令的概率,可以用于生成某种数据的加强测试;此外,对于每组数据,调用random模块,保证可以存在一定的概率生成异常类数据。逻辑如下(以qci为例):

    sig = np.random.choice(choice_list, p=p)
    instr = ""
    if sig == 0:
        instr = gen_qci()

sig为0时,调用qci的生成函数:

def gen_qci():
    id1 = random_person_id()
    id2 = random_person_id()
    return "qci " + str(id1) + " " + str(id2)

此外,由于绝大部分数据生成都需要随机生成id,为了实现代码复用,我们将其提炼为random_person_id()、random_group_id()、random_message_id()、random_message_attribute(ins)等函数。举例如下:

def random_person_id():
    """
    generate person id from exist person id
    also
    generate unexpected person id which cause exception
    """
    global exist_person_id
    id_choice = np.random.choice([0, 1], p=[0.1, 0.9])
    if id_choice == 1 and len(exist_person_id) != 0:
        id = random.choice(exist_person_id)
    else:
        id = random.randint(0, 1000)
    return id

数据正确性的验证则较为繁琐。对于图相关的算法,我们调用了networkX包,用于路径的存在性、连通块数目、最短路径计算等方面,其余的则虽不复杂,却较为繁琐,可以说几乎把Runner重写了一遍。

5 Java容器的选择

JML中给出的Arraylist只是给出了元素的逻辑结构,为了匹配所需要的性能,需要选择合适的容器。

  • 对于某些一定会成对出现的数据,如preson_id和person,message_id和message,emojiid和emojiheat等,可以考虑使用HashMap进行存储,从而可以实现查找效率为O(1),同时便于维护。
  • 此外,Person中的message,由于需要实现先进先出(FIFO),为方便插入和删除,我使用了LinkedList。
  • Dijkstra算法中,我同样使用了HashMap存储相应的结点与距离这一键值对;此外,距离缓存需要用到终点与权重这一组合,我手动构建了二元组(Tuple)。后来同学告诉我说,Java内部自己实现了Pair,可以确保成对数据的存储。
  • 对于Dijkstra中用到的小顶堆,我利用了Java内部的PriorityQueue容器,手动实现了比较器,即完成了小顶堆的实现。

6 Bug分析

第9次作业遇到的bug是在连通块合并时忘记把同一连通块内的所有点都指向该连通块,而是只修改了增加关系的两个顶点的连通块。
第10次作业遇到的bug是message在调用subList方法时,下标设置错误导致的;另外的错误是因为CPU超时,我按照上面的策略进行了较大规模的重构。
第11次作业遇到的Bug,一个是误把message的id和emoji的id搞混了;另外一个是因为整数舍入造成的精确度问题。

总结

本单元是最后一次互测的机会了,让人唏嘘不已。虽然我本单元作业平均分依然在60左右徘徊,但可能是前两单元做的很差的原因,本单元较之前得分还是增加了的,坚持了这么久,也算是有一些收获,希望以后会更好吧!
image
最后附上我最喜欢的一首歌,希望你会喜欢~

posted @ 2021-06-01 00:42  Stanlei  阅读(126)  评论(0编辑  收藏  举报
// 侧边栏目录 // https://blog-static.cnblogs.com/files/douzujun/marvin.nav.my1502.css // // // // // //