301
社区成员
发帖
与我相关
我的任务
分享本单元的主要内容是要求完成JML规格实现,我们需要理解体会规格语言对程序开发带来的帮助。此外,我们还需要根据时间复杂度的要求进一步优化实现方法,体会规格和实现上的区别
总体架构设计就是依照JML规格进行设计的,以下具体讲讲实现上的优化
专门设置一个统计异常的类Exceptioncounter,对于每一种异常都有一个变量存储总发生次数,都有一个容器存储对应id发生异常的次数,在每个异常的接口实现的类中直接进行对该类的统计方法的调用输出即可
本次第一次作业中用到了并查集,用于解决查询Person之间是否在一个关系圈,共有多少关系圈等方法。通过利用路径压缩来节省时间复杂度。在我的作业实现中在Edge类实现并查集的操作和查询。
其中难点在于并查集的动态维护,当遇到删边操作时,如果选择重建并查集无疑是一种暴力且耗时的行为,这里我们采用深搜,假设我们要删的边的两个节点为A、B,根据对并查集的定义,这两个节点必然拥有同一个father(路径压缩),或者说有同一个根,此时我们进行删边,不妨将A,B的father分别设置成自己(对于同一个圈子内的Person,father可以是他们中的任意一个人)。然后以A结点开始dfs,遍历所有从A结点出发可以遍历到的点,将他们的father设置为A结点,然后深搜后判断B结点的father是否为A结点,如果是,那么说明即使删了边,AB依然在同一个block中,反之则不在。
public void modifyEdge(int x, int y) {
HashSet<Integer> con = map.get(x);
con.remove(y);
map.replace(x, con);
con = map.get(y);
con.remove(x);
map.replace(y, con);
father.replace(y, y);
visit = new HashSet<>();
dfs(x, x);
if (father.get(y) != x) {
blockSum++;
}
visit = new HashSet<>();
dfs(y, y);
for (int i : map.get(x)) {
if (map.get(i).contains(y)) {
tripleSum--;
}
}
}
public void dfs(int x, int rt) {
visit.add(x);
father.put(x, rt);
for (int i : map.get(x)) {
if (!visit.contains(i)) {
dfs(i, rt);
}
}
}
在第二次作业中,需要我们查询两个节点间的最短路径,条件是边权为1,那么在边权为1的情况下,dijsktra算法其实就不是很优的考虑了,这里我们直接考虑广度优先搜索,其实双向bfs和bfs之间的差距并不是很大,这里我直接实现了双向bfs,至于时间上的差别并不大
public int queryShortestPath(int id1,int id2)
throws PersonIdNotFoundException, PathNotFoundException {
if (!containsPerson(id1)) { throw new MyPersonIdNotFoundException(id1); }
else if (!containsPerson(id2)) { throw new MyPersonIdNotFoundException(id2); }
else if (!edge.isCircle(id1, id2)) { throw new MyPathNotFoundException(id1, id2); }
else {
if (id1 == id2 || getPerson(id1).isLinked(getPerson(id2))) { return 0; }
Queue<Person> startQueue = new LinkedList<>();
Queue<Person> endQueue = new LinkedList<>();
HashSet<Integer> startVisited = new HashSet<>();
startQueue.offer(getPerson(id1));
HashSet<Integer> endVisited = new HashSet<>();
endQueue.offer(getPerson(id2));
startVisited.add(id1);
endVisited.add(id2);
int diss = 0;
int dise = 0;
int isPath = 0;
while (!startQueue.isEmpty() && !endQueue.isEmpty()) {
diss++;
isPath = bfs(startQueue, startVisited, endVisited);
if (isPath == 1) { return diss + dise - 1; }
dise++;
isPath = bfs(endQueue, endVisited, startVisited);
if (isPath == 1) { return dise + diss - 1; }
}
return 0;
}
}
public int bfs(Queue<Person> queue, HashSet<Integer> visited, HashSet<Integer> other) {
int si = queue.size();
for (int i = 0; i < si; ++i) {
MyPerson cur = (MyPerson) queue.poll();
for (Person j : cur.getAcquaintance().keySet()) {
if (!visited.contains(j.getId())) {
if (other.contains(j.getId())) { return 1; }
visited.add(j.getId());
queue.add(j);
}
}
}
return 0;
}
对于查询BestAcquaintance的操作,最清晰的方法便是维护每一个Person的优先队列,维护队首为最大值,每次查询返回队首即可,这里由于排序有两个优先级,第一优先级是value,第二优先级是id,因此要自定义cmp,这里容器我并没有采用优先队列(因为java没用过不知道咋用),我直接采用了TreeMap容器来维护
注意这里不要采用return o1.getId()-o2.getId()的形式,如果注意了数据范围,就会发现会爆
Comparator<PersonCmp> cmp = (o1, o2) -> {
if (o1.getValue() < o2.getValue()) {
return 1;
} else if (o1.getValue() > o2.getValue()) {
return -1;
} else {
return Integer.compare(o1.getId(), o2.getId());
}
};
priQue = new TreeMap<>(cmp);
有一些方法不需要图论的知识或是算法上的优化,只是需要动态维护修改和查询即可,这里便不细说了,目的是将查询的复杂度降为O(1),但是修改的复杂度从O(1)变成了O(n),但整体来说依然是优化
数据构造方面前两次作业是雷同的,只需要去构造一张图,尽可能的保证数据生成的图既有稠密图也有稀疏图,保证覆盖率
第三次作业的数据构造要考虑message,对比前两次需要考虑message的构造次数,在这里我采用了构造一定数量的各种类型的message,再进行delete操作,保证数据的普适性
这里需要根据JML规格上的要求进行全方位的断言,除了方法调用结果外,调用后状态是否改变啊等等都需要去断言,这里我采用的是影子,除了本身的network,在生成数据时同时深克隆一个network0作为影子,届时直接比较调用后的network的属性和network0即可
这三次作业强测均为满分通过且互测没有被hack,在hack别人时通过自己构造的极端数据或是自己在写代码时评测机测出的错误数据均有收获
JML规格只是一个框架和要求,只要满足该要求,如何实现都可以,在此基础上自然是时间越优的实现越能得到正收益。同时JML规格也有效的规定了我们能做的和不能做的,是个很有效的工具。