301
社区成员
发帖
与我相关
我的任务
分享JML规格化编程总结
第三单元以模拟一个社交网络为背景,让我们初步了解并学习了JML规格化设计语言,培养能够根据JML编写高安全性及便于测试的代码程序。
本单元需要注意的核心是基于JML进行算法的优化。JML的编写中不存在算法的选择,而我们在编写程序时则要细致地考虑如何在满足规格的条件下进行时间上的优化,否则很容易在强测中爆点。
和前两个单元一样,本单元的数据构造依然采用大量随机生成以及手动构造极端数据的方式。大量随机生成能够提高检查出代码错误的概率,而手动构造极端数据能够反映程序的优化效果以及抗压能力,极端数据对于本单元的优化非常重要。
int边界测试在编写比较方法或比较器的过程中,部分人会使用return (int)a - (int)b;这种方法,但明显如果本身数据处于边界,这种方法会爆掉。
例如对于qbs,qtvs,qcs等操作,如果连续进行大量的查询,在没有进行动态维护的情况下非常容易出现CTLE。
本单元总架构如下

hw9中qci要求判断两个人是否连通,qbs简单来讲就是求连通分支的总数。这两个指令操作很容易让人想到并查集算法。
但随之而来的问题是,mr操作可能会删除两个人之间的联系,从而改变原先并查集的结构。举例来说,其中一个连通分支是A-B-C-D,若删除了B与C的联系,那么就分裂成了2个连通分支。而加关系的操作ar同样会有类似的改变并查集的问题。
并查集算法本身并不支持删除一个集合中的元素。能想到的一种思路是在每一次改变关系时重建并查,但如果全部重建,时间复杂度依旧很大。因此,我们考虑了一种基于DFS的并查集局部重建的方式。
//基于DFS的并查集局部重建
//以删除了a与b之间的关系为例
HashSet visit;
DFS(a, visit); //深度遍历所有与a连通的所有节点,将这些节点的父节点全部设为a,并将这些节点存入visit中
if (!visit.contains(b)) {
DFS(b);
} //如果visit中不包含b了,说明这个关系的删除导致a与b不再连通,从而我们模仿a的重建流程,对与b连通的节点进行DFS遍历,并将这些节点的父节点设为b。这样就完成了并查集的局部重建
实现局部重建后,对于连通分支的总数我们也能很好的进行动态维护。这些操作我们统一在一个新建类MyMap中进行操作。
对于三元环总数的动态维护是非常容易的,我们以添加a与b的关系为例,伪代码分析。
tripSum; //应是全局变量
addRelation(a,b); // 添加了a与b的关系
if(size(a.aquaintance) < size(b.aquaintance)) {
遍历a.quaintance;
if(a.quaintance[i].isLinked(b)) {
tripSum++;
} //如果存在一个a的好友i,i与b也是好友,那么就构成了一个三元环,三元环总数加1即可
} else {
同样的方法对b遍历与维护
}
qsp操作要求计算从a和b两人间最短的路径,这里是一个无权的路径,已经并不需要使用dijstra算法。一个普通的BFS就可以实现要求。
tagValueSumqtvs操作要计算一个tag里所有人之间的关系值之和。如果不动态维护一个tag的tagValueSum,而是在每次qtvs操作时进行遍历计算,在强测时会被爆点。因此,我们需要去动态维护每一个tag的这个属性。
具体的维护流程并不复杂,当a和b的关系发生改变时,我们都需要去维护与之有关系的tag。我们以稍复杂一些的a与b关系被删除为例:
// 首先对a和b的tags中有对方的tag进行处理
for (Tag tag : a.tags) {
if (tag.hasPerson(b)) {
遍历该tag中所有人,从valueSum中删去关系值
}
}
for (Tag tag : b.tags) {
if (tag.hasPerson(a)) {
遍历该tag中所有人,从valueSum中删去关系值
}
}
step2 : 遍历所有tag,若tag中有a和b,则删去关系值。
//一个优化是遍历a和b两个人中人最少的acquaintance
treeMap动态维护每个人的关系最佳者当涉及qba指令时,需要知道每个人的最佳好友。诚然可以通过每次复杂度为o(n)
的遍历进行维护。但是总归来说效率也依然比较低。因此,我选择了一种天然具有顺序的容器treeMap。对每个人的好友按照value-id键值进行插入排列。
使用treeMap需要自定义一个比较器,这并不难写:
private TreeMap<PersonKey,MyPerson> treeMap = new TreeMap<>(new Comparator<PersonKey>() {
public int compare(PersonKey p1, PersonKey p2) {
if (p1.getValue() > p2.getValue()) {
return 1;
} else if (p1.getValue() < p2.getValue()) {
return -1;
} else {
if (p1.getId() < p2.getId()) {
return 1;
} else if (p1.getId() == p2.getId()) {
return 0;
} else {
return -1;
}
}
}
});
这里的PersonKey就是单独创建的一个键值类。
最后一次作业并没有涉及到特殊的优化算法。按照JML完成代码编写就足够。
优化是本单元的一个核心。幸运的是本人在3次作业中均未出现性能问题,顺利通过强测。
JML提供给程序员一个代码规格,这个规格规定了一些属性、容器以及方法的逻辑,同时也规定了一些可变与不能够改变的变量。但是JML在编写上并不是唯一的实现方法,也大概率不是最佳的实现方法。因此,我们必须在考虑实际情况下,按照JML规格进行实现的优化。
例如就容器而言,JML只会给出类似Person[]的声明。但这并不意味着我们的代码就必须使用数组的形式,否则我们的代码效率必然很低。我们当然可以选择HashMap等更适合的容器进行存储。
此外,在保证JML的完备性下,我们不一定严格按照JML实现方法,可以自由寻找更高效合适的思路。
Junit测试本单元中还要求我们对特定的方法进行Junit测试的编写。
我认为主要有2个方面的事情需要我们完成——实现和规格的一致性检查以及数据构造的完备性。
关于这个方面,最好的编写方法是将JML规格逐个进行检查代码的编写。
在采取随机的构造条件下,由于Junit的特殊性,可以进行多次检查。即选择合适的时机,对同一组数据的不同位点进行多次检查。这样能覆盖到更多种数据的情况。
这一单元的难度相比前两个单元有了适当的降低。核心在于理解JML并学会高效的阅读与代码的翻译编写。JML作为一种保证代码高安全性、高准确率的工具,值得我们深入学习体会。