第三单元作业总结
Posted Expialidocious
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第三单元作业总结相关的知识,希望对你有一定的参考价值。
第三单元作业总结
设计策略
这个单元的作业因为有jml进行了严格的限制,绝大部分方法属性没有自主发挥的余地,下面主要列举了每次作业中需要自己好好设计的方法:
- 第一次作业
- isCircle 这个方法需要实现判断两个点之间的连通性,所以采用并查集的方法,如果两个人有相同的公共头结点,他们就位于一个连通分量内部,也就是可以连接的,否则不行。这里注意需要在addPerson和addRelation的时候动态维护并查集。
- queryBlockSum. 这个方法求连通分量,由于使用了并查集的数据结构,可以在几乎O(1)的时间之内查询两个点的连通性。于是我们可以动态维护当前所有人的连通区域熟练。如果addPerson则连通区域加一,如果addRelation的时候合并了两个连通区域,则连通区域数量减一。
- 第二次作业
- gueryGroupValueSum这个方法目的是查询一个group所有内部边权重之和。如果每次询问进行计算,那么就是O(n^2)的时间复杂度,显然是不满足需求的。于是我们采用动态维护的方法:addToGroup的时候加上新加入人与组内其他人连接的边的权重;delFromGroup的时候减去被删除的人和组内剩余的人的边的权重;addRelation如果连接的两个人位于同一个组内(注意不一定只有一个组同时包含两个人), 就加上两者边的权重。
- gueryGroupAgeVar这个方法计算方差,注意不能够使用概统课上学到的$$var(x) = E(x2)-E(x)2$$,我当时是按照方差定义计算的,O(n)时间复杂度可以接受。但看了同学的博客,我发现利用公式$$[(∑x2)−2E(x)(∑x)+nE2(x)]/n$$是一个等好的选择,只需要O(1)的时间。
- 第三次作业
- sendIndirectMessage这个方法的就是在就最短路,我才用迪杰斯特拉算法加优先队列优化,时间复杂度O((n+v)logn).虽然在极端稠密图的时候会恶化到O(n^2logn),但由于形成稠密图需要大量添加边,占用了许多指令,因此这个时间复杂度也是可以接受的。
测试的方法与策略
虽然jml给出了严格的形式化语言的规格限制,是可以针对每个方法进行单元测试,甚至可以通过数学的形式化验证来验证程序的正确性。但这种验证方法第一需要编写大量的测试代码,第二无法检验程序的时间性能,第三难以用来hack别人,因此我最终还是采取了随机+半手动生成数据+对拍的测试方法。
一般来说,随机化数据主要用于检验程序的正确性,而半手动生成的测试数据用于检验程序的性能。下面分享我半手动生成数据的几个策略:
-
破坏catch机制,加入(删除)一次查询一次
-
针对个别请求密集查询,不调用其他指令
-
通过数学计算的方式里算出addPerson, addRelation, query指令之间的理论最狠数量分配
-
针对最短路算法,构造只有一个长链的图,主要hack没有堆或者优先队列优化的迪杰斯特拉
-
构造越早访问边权越大的数据,增加平均入队次数。(如下图所示,从A到B,实际会复杂一些)
(
容器选择和使用经验
这三次作业除特殊情况外,都是用HashMap。HashMap对插入、删除、查找、判断是否存在的时间复杂度都接近于O(1). 除了HashMap,在记录每个人的receivedMessages时,因为要求有序性、每次头插、只询问前几个元素的特征,所以采用了LinkedList。
第三次作业迪杰斯特拉算法在写优先队列的时候,使用了PriorityQueue的结构,并建立了新的类,用于存储点到起点的当前距离,并实现了Comparable接口,用于PriorityQueue排序。
性能问题
本次作业最大的难点可以说是性能问题了。按照1秒钟cpu进行十亿次加成运算来计算,考虑大java的性能不如C语言,因此基本上要求把单条指令的时间复杂度限制在O(nlogn)以内,其中n时目前为止已经输入的指令条数。
具体的优化手段已经在设计策略给出了,下面就总结一下优化的思想:
- 使用catch机制
- 把算法中尽可能多的参数动态维护,避免重复计算
- 多使用HashMap,谨慎挑选容器,明白每种容器的适用范围,各种方法的时间复杂度。以空间换时间
架构设计
-
对于图的每一个节点,也就是MyPerson:
private int id; private String name; private int age; private int socialValue; private int money; private final HashMap<integer, person=""> acquaintance; private final HashMap<integer, integer=""> value; private final HashMap<integer, message=""> messageHashMap; private final LinkedList<message> messages; private final LinkedList<integer> groupsId;
其中,acquaintance的key为personId,value为person的引用,groupId记录了这个人属于哪些group的groupId
-
对于group:
private final int id; private final HashMap<integer, person=""> people; private int ageSum; private int valueSum;
-
Network:
private final HashMap<integer, person=""> people; private int blockSum = 0; private final HashMap<integer, object=""> headers; private final HashMap<integer, group=""> groups; private final HashMap<integer, message=""> messages; private final HashMap<integer, integer=""> hashHeatMap;
headers用于实现并查集。
悲惨的Bug
这次作业由于我没有关注到personId可以为负,我使用的并查集通过每一个节点的正负标记是否是头结点。如果节点的值为正,则表示他的头结点的id;如果节点的值为负,则表示当前节点为头结点,并且负数的绝对值表明当前连通区域的节点数量。可惜personId可以为负。唉
哭了!
之后debug就很简单了。原本headers存储的values为Integer,现在改成Object。自己再定义一个空类。如果当前节点是头结点,那么value就是这个空类。</integer,></integer,></integer,></integer,></integer,></integer,></integer,></integer,></integer,>
以上是关于第三单元作业总结的主要内容,如果未能解决你的问题,请参考以下文章