301
社区成员
发帖
与我相关
我的任务
分享第三单元的内容主要是契约式编程,根据官方给定的JML进行代码的编写。本单元相较于前两单元的腥风血雨可谓是温和了许多,因为给出了方法的前置条件,后置条件等,只需读懂JML就行。但如果真的按照JML一字一句去翻译,就会喜提TLE大礼包。因此,在实现时还要注意性能,实现规格与实现分离。
黑箱测试:黑箱也就是只关注输入和输出,不考虑怎么实现的,检测是否符合预期功能。OO评测实际上就是一个黑箱。
白箱测试:与黑箱相反,全面了解程序内部逻辑结构,允许测试人员利用内部逻辑结构构造测试用例对程序所有逻辑路径进行测试。
单元测试:是指对软件中的最小可测试单元进行检查和验证。至于“单元”的大小或范围,并没有一个明确的标准,“单元”可以是一个函数、方法、类、功能模块或者子系统。单元测试通常和白盒测试联系到一起。
功能测试:测试功能是否符合预期。
集成测试:在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。
压力测试:(简单来讲感觉就像是强测)向软件系统加压,观测软件在极限数据下的运行情况;目的一般是测试软件性能缺陷。hw10中采用qtvs狂轰滥炸就是压力测试的典型。
回归测试:是指修改了旧代码后,重新进行此前已通过的测试,以确认修改未引入新的Bug。OO的bug修复就是回归测试。
数据构造。在hw10互测时,我通过阅读其他人代码发现有好多人忽略了ageVar除零的问题,hw11中sm也有人有这样的问题。这时就可以构造这样的数据去测试其他人。还有一些极端情况,比如int_max - int_min这种。多考虑实现细节,根据细节中漏洞去构造数据。
本单元实际上就是构建并且维护一个社交网络图。每个人都有自己的关系网,我们要去维护这个网络中相关内容。
第一次作业复杂度主要是query_circle、query_block_sum和query_triple_sum,尤其是后者如果正常按照JML来写复杂度更是O(N^3)。
为了避免复杂度太高,我使用Hashmap存储person等,并且动态维护blockNum和tripleNum。使用并查集维护是一个比较好的方法。
具体做法是在addPerson时,把父节点设成自己;如果addRelation,就把两个人的父节点设置成同一个(join)。同时遍历person容器,如果某个人同时和这两个人有联系,tripleNum++。
public int find(int x) {
if (pre[x] == x) { return x; }
return pre[x] = find(pre[x]);
} //同时实现了路径压缩。
//但是用递归不是很好,因为可能会爆栈。
//建议while循环
public void join(int x,int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
blockNum--;
pre[fx] = fy;
}
}
在调用modifyRelation时,如果需要删除某条边(两个人之间的联系),遍历person容器,如果某个人同时和这两个人有联系,tripleNum--。同时需要对并查集进行重建。我一开始想的是对person1和person2,分别找到他们的一个熟人,然后令父节点等于熟人的父节点。最后再比较person1和person2的父节点是否相等,如果不等blockNum就增加一个。后来想明白,这种方法是错误的,可能不会计算出正确的blockNum。
正确方法:需要对并查集进行重建,具体做法是对person1和person2做DFS,也即遍历包含person1的最大连通子图。然后把所有结点的父节点都设置成person1。person2同理。然后再判断person1和person2的父节点是否是同一个,如果不是,blockNum需要++。visited数组使用Hashmap比ArrayList要快上不少时间。
public void modifyRelation() {
//...
pre[personMap.get(id1)] = personMap.get(id1);
pre[personMap.get(id2)] = personMap.get(id2); // 把两个人父节点分别设置成自己
dfs(person1,find(personMap.get(id1)),visited); //bfs遍历,把所有和person1连通的person父节点都设置成person1
if (find(personMap.get(id2)) != personMap.get(id1)) {
dfs(person2, find(personMap.get(id2)), visited);
blockNum++; //如果person2和person1父节点不同,最大连通子图数目需要++
}
//...
}
public void dfs(Person person,int parent,HashMap<Integer,Boolean> visited) {
Person[] people = ((MyPerson)person).getAcquaintance().values().toArray(new Person[0]);
int len = people.length;
for (int i = 0; i < len; i++) {
if (!visited.containsKey(people[i].getId())) {
visited.put(people[i].getId(),true);
pre[personMap.get(people[i].getId())] = parent;
dfs(people[i],parent,visited);
}
}
}
判断iscircle时,只需要判断父节点是不是同一个人就行。
通过使用并查集这种技巧,避免了嵌套for循环遍历,但addRelation和modifyRelation的复杂度有所上升,如果只改不查,这种方法就失去了意义。只能说鱼和熊掌不可得兼。
第二次作业相比第一次多了一个Tag类。我自己的理解是类似微信的标签。其中最花时间的是coupleNum、valueSum和shortPath。
对于coupleNum,我也没有想到什么比较好的维护方法,也就正常按照JML去完成。不过如果真的按照JML上遍历两遍去写的话,我只能说这样子大概率会有CTLE。有一个O(n)的方法,就是找到这个人的bestAcquaintance,然后看他bestAcquaintance的bestAcquaintance是不是这个人。遍历完一遍之后记得除二。
对于valueSum,在读JML的时候直观上感觉如果两层for循环会TLE,所以需要动态维护。直接在addRelation和modifyRelation后需要遍历所有tag,如果某个tag同时含有这两个人,那么这个tag的valueSum需要增加或者修改对应的数值(一开始我就是ar和mr没有遍历所有tag导致出错)。att和dft需要对这个tag中的人做一个遍历,如果某个人和这个人有关系valueSum就增加或减去对应数值。
shortPtah我一开始用的Dijkstra,后来发现是无权图,就换成了bfs。但是之前写的也没删掉,留下了邻接矩阵,导致我强测数组越界狂砍0分,让我捶胸顿足夜不能寐唏嘘不已。(曾经有一份真挚的一百分摆在我面前我没有珍惜,等到失之交臂的时候才追悔莫及。如果OO能再给我一次机会...)
ageVar维护比较简单,直接在att和dft时修改∑(age^2)就行。就是要注意计算精度的处理还有personSize为0的问题。互测时发现同房好几个人都有除零这个问题。
本次作业新增内容和Message有关。Message具体分为4类,普通(ordinary),RedEnvelopMessaeg,NoticeMessage,EmojiMessage。person可以sendMessage,receiveMessage等。这次作业感觉没有什么需要特别维护的内容,也没有涉及到什么性能问题,老老实实按照JML去完成就行了。
一开始求最短路径我用的Dijkstra算法,定义了一个邻接矩阵,大小是400 * 400。后来发现定义的太小,全部数组越界;再大一点就直接爆栈。后来采用了bfs来实现。此外,对那些时间复杂度高的方法,动态维护是一个不错的选择,维护部分在上面,不多赘述。
规格是为了明确需求,方便我们编写测试,但实现不仅仅要考虑正确性,还要考虑性能、可读性等多方面因素。因此有些功能我们不能老老实实地按照JML逐字逐句翻译,我们读懂规格之后,通过自己设计实现功能。
有些部分可能直接按照规格去完成,比如addTag。但有些复杂的方法需要分离,比如上面的queryTripleNum等方法,我们把实现这一功能的代码写在了不同的方法里面,去动态维护,完成了规格与实现分离。如果对着规格翻译的话大概率会CTLE。
一开始我并不明白JUnit要怎么写,我以为只写一个queryTripleNumTest方法就可以了。但是看实验代码写了prepareData方法。然后我明白了JUnit实际上是我写一个数据生成器和qtn方法去测试。testQueryTripleNum方法按照JML规格写就行,保证每一个ensures都有验证。生成数据的时候要保证一定的随机性和普遍性,能覆盖所有情况。比如,第二次作业需要保证有孤立点,第三次作业需要保证有各种类型的message。
还有就是老生常谈的深浅克隆问题。对MyNetwork和Person[] 都需要深克隆。这里我是直接生成了两个network和personp[],一个作为对照组,一个去调用方法,然后进行验证。
本单元作业我理解了契约式编程的含义,先读懂规格再去实现,磨刀不误砍柴工。在Junit测试中学会了如何根据规格测试程序以及怎样根据规格生成测试数据。
同时,我在写代码时也更加关注性能,寻找复杂度更小的方法,追求更小的代价更好的效果。
本单元作业我重新回顾了图算法,这些算法也涉及到许多细节变化,让我对图论这部分掌握更加扎实。