BUAA-OO第三单元:基于JML规格的程序设计
第三单元为根据JML规格语言进行编程,难度相对前两个单元有所下降,不过JML作为一个陌生的语言,在一开始理解它还是有一定的困难的,需要对照着手册不断查询,并且一些复杂的方法用JML语言描述出来是非常冗长的,作业体验有一丢丢差~
架构设计
hw9
UML图
代码架构及优化思路
第九次作业要求实现一个MyNetwork
类,可以在其中添加人,即MyPerson
类,在两个人之间可以添加关系并设置权值,即无向有权图,在课程组给出Runner
类中会查询其中的相关数值。同时,我们需要自己实现相关异常的抛出,即UML图右边的Exception并打印相关信息。本次作业难度较小,只需对应课程租给出的规格书写即可,但所有方法全部暴力求解是有TLE风险,下面介绍一些优化思路。
并查集优化算法
在MyNetwork
类中有isCircle
方法,判断两个节点是否相连,我们可以采用bfs
或dfs
遍历,是O(V + E)的复杂度。同时,这里使用并查集算法,可以将查询复杂度降到O(1)。其思路如下:
我们可以将并查集理解为在某些节点中,它们两两之间都有一条路径可以到达彼此,我们便可以从这些节点中选择一个节点作为代表,或者作为祖先节点,如果有新的点加入进来,只需将其祖先节点设为这些节点中的某一个即可,因为递归查找某个节点的祖先节点最终都会得到同一个代表,可见下面的具体实现。因此,在判断两个节点是否连通,只需要查询其祖先节点是否一致即可,在初始时,将每个节点的祖先节点都设为自己。
public class Disjoint {
private HashMap<Integer, Integer> path;
private HashMap<Integer, Integer> depth;
...
public void addPerson(int id) {
if (!path.containsKey(id)) {
path.put(id, id);
depth.put(id, 0);
}
}
public int find(int id) {
int res = id;
while (res != path.get(res)) {
res = path.get(res);
}
int now = id;
while (now != res) {
int temp = path.get(now);
path.replace(now, res);
now = temp;
}
return res;
}
public int merge(int id1, int id2) {
int res1 = find(id1);
int res2 = find(id2);
if (res1 == res2) {
return 0;
}
path.put(res1, res2);
return -1;
}
...
}
并查集动态维护
然而,在对关系进行删除时,原来存在的并查集便需要重新构建,可以采用删除后重新对所有店遍历构建并查集的方法,这里采用了Yanna学姐博客的方法:
当在modifyRelation()
中检测到需要删掉某条边的时候,就对这条边的两个端点进行DFS 染色,通过判断染色结果来确定设左边点的 id
为 leftId
,右边点的 id
为 rightId
。
- 首先,将并查集中的
father[rightId]
设置为null
; - 接着,DFS 遍历并查集将所有与点
leftId
相连的点的father[]
标记为leftId
; - 然后,检查并查集中的
father[rightId]
是否为leftId
。如果father[rightId] != leftId
,则blockSum++
;反之,blockSum
不变。 - 最后,DFS 遍历并查集将所有与点
rightId
相连的点的father[]
标记为rightId
。
具体实现如下:
private void MaintainDis(int id1, int id2) {
disjoint.set(id2, id2);
dfs(id1);
if (disjoint.find(id2) == id1) {
disjoint.set(id2, id1);