我来补这学期欠下的坑了,比起博客园,里面的文章会更侧重于关于做题的思路,因此有可能直接以最后一次题目来分析了(毕竟迭代开发),有关面向对象相关的内容,比如类、对象、方法、属性等等或许相对少这一单元来说,这一单元来说,就不说明JML的理解之类了,就是其中一些关键的优化算法,因此和博客园应该是一个子集(?)的关系
基于JML规范语言的社交系统
题目大意
搭建一个模拟社交系统,存在着Person,Group,Network,Message等类,每个类都有着自己的属性和操作,通过JML进行说明
重点优化
基于并查集的qci与qbs
这两个指令,分别是查询两个点是否联通和整张图的连通块个数,如果按照暴力的方法,对两个指令使用DFS或者BFS的话,最差的情况qci的时间复杂度会达到$O(n^2)$, 而qbs的时间复杂度会达到$O(n^3)$,如果qbvs和qci的指令占比高的话,有tle的风险,事实上对于某些DFS或者BFS没有注意砍掉一些搜索分枝的情况下,在互测中确实可以构造出这样的数据使得程序运行超时。于是,我们可以处理并查集的方法进行处理。
并查集,顾名思义,我们将所有关系的点都弄到一个集合里,当两个点要建立联系时,将两个点所处在的集合合并为一个处理。(孤立点的集合可以视为只包含自己的点)。在这次作业中,我采用的是递归找并查集并合并的策略。
具体为,对于每个集合用树形结构进行存储,对于每个点不断向上找其祖先,直到其不存在祖先,对比两个祖先是否相等,若相等则说明这两个点是连通的,不相等说明不连通。合并时,找到祖先并将其中一个树变成另一个树的子树。而这个并查集可以在 addRelation()
的时候进行维护。两点已经在一个集合里面就不管了,不再则递归找到其祖先,并进行合并。我所采用的合并策略为将集合中点数少的点连向点数较大的点,这样可以对并查集的层数有一定的限制。其层数决定了其时间复杂度,通过上述方法可以让其时间复杂度再最坏情况下也为$O(logN)$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// UnionSet
public class Community {
private Community higher;
private final Person lord;
private int amount;
Community(Person lord) {
this.lord = lord;
this.amount = 1;
this.higher = null;
}
public Person getLord() {
if (this.higher == null) {
return this.lord;
} else {
return this.higher.getLord();
}
}
public void setHigher(Community higher) {
this.higher = higher;
}
public void setAmount(int amount) {
this.amount = amount;
}
public Community getHighest() {
if (this.higher == null) {
return this;
} else {
return this.higher.getHighest();
}
}
public int getAmount() {
return amount;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof Community)) {
return false;
}
Community myCommunity = (Community) other;
return myCommunity.getLord().equals(this.getLord());
}
}
而对于后面指令数的增加,利用递归可能会出现爆栈的风险,所以后两次作业我使用了非堆归的方法进行查询和合并。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Person find(Person person, HashMap<Person, Person> communities) {
Person temp = person;
if (!communities.containsKey(temp)) {
return person;
}
while (!communities.get(temp).equals(temp)) {
temp = communities.get(temp);
}
Person updated = person;
while (!communities.get(updated).equals(updated)) {
updated = communities.get(updated);
communities.put(updated, temp);
}
return temp;
}
而对于qbs,可以维护一个变量 blockSum
,当 addPerson
时增加,当 addRelation
时,若两点不在一个集合中,blockSum
减少,否则不变,这样qbs操作的时间复杂度就变成$O(1)$
1
2
3
4
5
6
7
8
9
10
11
// addRelation
if (!c1.equals(c2)) {
blockCount--;
// something else
}
// addPerson
people.add(person);
blockCount++;
Community community = new Community(person);
communities.put(person, community);
维护qbvs
如果每次遇到啊一个qbvs就对所有点进行一次遍历,时间复杂度是$O(n^2)$, 显然是存在优化的方法的。注意到当且仅当以下三种情况发生时,Group的Sum of Value才会发现改变
- 当同一个Group中的两个Person使用了
addRelation
操作 - 当与Group中的一个Person有关系的一个Person也
addToGroup
- 当与Group中与别的Person存在关系的Person从Group中
delFromGroup
所以我们对于每个Group对象维护一个valueSum属性,记录qbvs的答案,对于每个Person记录一个 allGroups
容器,记录他参加的Group,每当 addRelation
之后就去遍历 allGroups
,如果存在重合就增加 sum
,同理,addToGroup
时也要做类似的操作。
最小生成树的qlc
最小生成树可以选择Prim或是Kruskal,这里我选择了 堆优化的Prim,堆导入优先队列来实现即可。感觉如果是完全图的话,Prim效率应该更优。所谓堆优化,本质也就是用PriorityQueue存着距离,这样就不用每次找最小了。其余就是正常的Prim算法,找最近的点纳入树中,以这个点为中转的点更新距离,直至纳入树的点数达到n个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public int queryLeastConnection(int id) throws PersonIdNotFoundException {
if (!people.containsKey(id)) {
throw new MyPersonIdNotFoundException(id);
}
int sum = 0;
int cnt = 1;
PriorityQueue<Edge> edges =
new PriorityQueue<>(Comparator.comparingInt(Edge::getValue));
HashSet<Person> visited = new HashSet<Person>();
MyPerson root = (MyPerson) people.get(id);
// total of the connected dots
int total = amounts.get(find(root, communities));
visited.add(root);
for (Person p : root.getValue().keySet()) {
edges.add(new Edge(root.queryValue(p), root, p));
}
while (cnt < total && edges.size() > 0) {
Edge e = edges.poll();
while (e != null && visited.contains(e.getP2())) {
e = edges.poll();
}
if (e == null) {
break;
}
MyPerson person = (MyPerson) (e.getP2());
visited.add(person);
sum += e.getValue();
cnt++;
for (Person p : person.getValue().keySet()) {
edges.add(new Edge(person.queryValue(p), person, p));
}
}
return sum;
}
最短路问题
利用堆优化的Dijkstra,Prim和Dijkstra蛮像的,也都是用优先队列进行优化的,就不过多赘述了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public int queryLeastConnection(int id) throws PersonIdNotFoundException {
if (!people.containsKey(id)) {
throw new MyPersonIdNotFoundException(id);
}
int sum = 0;
int cnt = 1;
PriorityQueue<Edge> edges =
new PriorityQueue<>(Comparator.comparingInt(Edge::getValue));
HashSet<Person> visited = new HashSet<Person>();
MyPerson root = (MyPerson) people.get(id);
// total of the connected dots
int total = amounts.get(find(root, communities));
visited.add(root);
for (Person p : root.getValue().keySet()) {
edges.add(new Edge(root.queryValue(p), root, p));
}
while (cnt < total && edges.size() > 0) {
Edge e = edges.poll();
while (e != null && visited.contains(e.getP2())) {
e = edges.poll();
}
if (e == null) {
break;
}
MyPerson person = (MyPerson) (e.getP2());
visited.add(person);
sum += e.getValue();
cnt++;
for (Person p : person.getValue().keySet()) {
edges.add(new Edge(person.queryValue(p), person, p));
}
}
return sum;
}
题外
本次练习的重点还是在于自动化测试吧,随机数据生成与找人对拍,可以找到很大的bug,还有就是JUnit的使用,但我还是不太会,如果以后掌握熟练了就填坑。