301
社区成员
发帖
与我相关
我的任务
分享
这个是最终作业的UML图,实现了一个社交关系网。
除了官方包中提供的Network、Person、Tag、Exception、Message等类,我还新建了Counter计数类,Operations操作类、Map并查集类。下面我将对每次作业的功能实现进行分析。
图构建过程:Network类中的persons容器作为图中点的集合,Person类中的acquaintance容器作为连通关系的集合。在添加人和关系的时候动态维护点和关系的容器即可实现图构建。后续的作业描述其实就是图维护过程,不过,维护的是图中存在的信息,而不是图本身。
主要目标:
实现方式:
三角关系:添加关系时遍历二人的熟人,寻找是否有相同的熟人,若有,则tripleSum++。
最大连通图:通过动态维护并查集实现。下面给出并查集的简单架构实现。
public class Map {
private HashMap<Integer, Integer> pre; // id1 -> id2(id2 is root)
private HashMap<Integer, Integer> rank; // id1 -> rank
public Map() {
this.pre = new HashMap<>();
this.rank = new HashMap<>();
this.blockSum = 0;
this.tripleSum = 0;
}
public void add(int id) { // 添加新人后的并查集更新方法
if (!pre.containsKey(id)) {
pre.put(id, id);
rank.put(id, 0);
Counter.getInstance().addBlockSum();
}
}
public void delete(int id1, int id2,
HashMap<Integer, HashMap<Integer, Person>> graph) { // 删除id1,id2的关系后,处理并查集的更新过程。
Set<Integer> visited = new HashSet<>(); // 存储熟人
pre.put(id2, null);
bfs(id1, visited, graph);
for (Integer integer : visited) {
pre.put(integer, id1);
} // 将id1设置为他所有熟人的根节点
if (!pre.get(id1).equals(pre.get(id2))) { // id2此时与id1不连通,重构并查集
// ...
// ... blockSum++;
}
}
public int find(int id) { // 寻找id的根节点,顺便压缩路径
if (pre.get(id) == id) {
return id;
} else {
int head = id;
while (pre.get(head) != head) {
head = pre.get(head);
}
int temp = id;
while (temp != head) {
int temp1 = pre.get(temp);
pre.put(temp, head);
temp = temp1;
}
return head;
}
}
public void join(int id1, int id2) { // 添加id1和id2的关系后,更新并查集的过程
int headOf1 = this.find(id1);
int headOf2 = this.find(id2);
if (headOf1 != headOf2) { // 二人根节点不同,不相连通
pre.put(headOf2, headOf1);
Counter.getInstance().subBlockSum();
}
}
private void bfs(int node, Set<Integer> visited,
HashMap<Integer, HashMap<Integer, Person>> graph) { // bfs不采用递归,速度比dfs更快
visited.add(node);
ArrayDeque<Integer> queue = new ArrayDeque<>();
queue.add(node);
while (!queue.isEmpty()) {
int v = queue.pollFirst();
HashMap<Integer, Person> acquaintance = graph.get(v);
for (Integer id : acquaintance.keySet()) {
if (!visited.contains(id)) {
visited.add(id);
queue.add(id);
}
}
}
}
}
是否连通:通过并查集实现,通过find函数查询根节点,若相等,则二者连通,若不等,反之。
主要目标
实现方式
queryBestAcquaintance:身边许多同学都是采用treeMap求解的,时间复杂度是每次新增关系时都要遍历整个树,故为O(n*logn),我觉得这样可能有点复杂且不必要了,于是采用的是动态维护,在Person类中维护bestId和bestValue,并设置一个脏位,负责一些难以维护———减小了最大关系的value值,不得不重新遍历的情况。这个架构可以在较少出现上述难以维护情况时保持O(1)的复杂度。出现上述情况,也仅仅在出现queryBestAcquaintance指令是才会遍历一遍容器Acquaintance。所以实际测试样例中基本不可能有如此复杂刁钻的样例可能攻击到此方法。
queryTagValueSum:本方法最初我用的直接求解法,时间复杂度为o($n^2$),无法通过强测,于是改成动态维护,将复杂度减小到o(n),才得以通过。下面将简述我的动态维护的策略。
综合考虑所有情况,可知valueSum的值只会在特定指令时才会发生改变——addRelation, addPersonToTag, modifyRelation, delPersonFromTag。
addRelation:添加关系时,可能会增加tag中的熟人关系,会改变value和。添加上新增的关系value即可。
addPersonToTag:在tag中添加人时,此人可能与原来的人存在关系,所以需要加上这些新增的关系value的和。
modifyRelation:第一种情况是修改关系value值但是不删除,那么只需要判断两人是否同时存在于某tag中,如果存在,则减少tag中的valueSum相应值即可。第二种情况是删除关系,若二人共同存在于某个tag中,则在tag中减去2*value值即可,若有一人是tag的拥有者,另一人是tag中的人,则需要减去后者于tag中所有的关系value值的和。
上面的理论叙述可能比较复杂晦涩,我将给出一些代码供参考了解。
private HashMap<Integer, HashMap<Integer, HashSet<Integer>>> tagFound; // 这个是我自建的索引表,可以从人出发,寻找到他包含在那些tag中。逻辑为:人->熟人->该人存在于熟人的tags集合
// 这个动态维护过程与图维护并存,不给出详细介绍
public void test(int id1, int id2, int value) {
for (Integer personId : this.tagFound.get(id1).keySet()) {
if (this.tagFound.get(id2).containsKey(personId)) { //二者有共同的熟人
for (Integer tagId : this.tagFound.get(id1).get(personId)) {
((MyTag) this.persons.get(personId).getTag(tagId)).
modifyValueSum(id1, id2, value);
}
}
}
} // 该函数用于addRelation和modifyRelation第一种情况,仅包含修改value 值但不包含删除tag中的人的情况。
// in Class Tag
public /*@ safe @*/ void delPerson(/*@ non_null @*/Person person) {
if (this.persons.containsKey(person.getId())) {
this.persons.remove(person.getId());
this.ageSum -= person.getAge();
this.ageSquareSum -= person.getAge() * person.getAge();
for (Integer personId : persons.keySet()) {
if (persons.get(personId).isLinked(person)) {
this.valueSum -= 2 * persons.get(personId).queryValue(person);
}
}
}
} // 每次删除都要遍历一次persons,删除所有相关的value值。
queryCoupleSum: 这一块我没有进行深度的动态维护,仅仅设置了一个脏位,仅维护了这个脏位。
private boolean coupleSumValid;
// 接下来是在addRelation和modifyRelation中修改脏位的过程
public void addRelation(int id1, int id2, int value)
throws MyEqualRelationException, MyPersonIdNotFoundException {
// 省略 ......
boolean flag1 = ((MyPerson) persons.get(id1)).addAcquaintance(persons.get(id2), value);
boolean flag2 = ((MyPerson) persons.get(id2)).addAcquaintance(persons.get(id1), value);
this.coupleSumValid = (flag1 || flag2) ? false : this.coupleSumValid;
test(id1, id2, value);
map.join(id1, id2);
}
public void modifyRelation(int id1, int id2, int value) throws MyEqualPersonIdException,
MyPersonIdNotFoundException, MyRelationNotFoundException {
// ...
boolean f1 = ((MyPerson) (persons.get(id1))).modifyValue(id2, value);
boolean f2 = ((MyPerson) (persons.get(id2))).modifyValue(id1, value);
this.coupleSumValid = (f1 || f2) ? false : this.coupleSumValid;
// ...
}
// 接下来就是求解coupleSum的方法
public int queryCoupleSum() {
if (this.coupleSumValid) {
return this.coupleCnt;
}
this.coupleCnt = 0;
for (Integer id0 : this.persons.keySet()) {
if (!((MyPerson) getPerson(id0)).getIsValid()) {
((MyPerson) getPerson(id0)).adjustTheBest();
}
Integer bestAqOfId0 = ((MyPerson) getPerson(id0)).getBestId();
if (bestAqOfId0 != null && !((MyPerson) getPerson(bestAqOfId0)).getIsValid()) {
((MyPerson) getPerson(bestAqOfId0)).adjustTheBest();
}
Integer bestAqOfId1 = null;
if (bestAqOfId0 != null) {
bestAqOfId1 = ((MyPerson) getPerson(bestAqOfId0)).getBestId();
}
if (bestAqOfId1 != null && bestAqOfId1.equals(id0)) {
this.coupleCnt++;
}
}
return this.coupleCnt /= 2;
}
主要目标
实现方式
本次Junit我采用了实验课上学习的参数化测试方法,下面将简要介绍一下参数化测试的写法。
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
// 三个导入包
@RunWith(Parameterized.class)
public class MyNetworkTest {
private Network network; // 代表执行方法后的network
private Network oldNetwork; // 代表执行方法前的network
// 相当于深克隆,前后对比,用于pure的判断
public MyNetworkTest(Network network, Network oldNetwork, int limit) { // 构造方法
this.network = network;
this.oldNetwork = oldNetwork;
}
@Test
public /*@ pure @*/ void queryCoupleSum() throws AcquaintanceNotFoundException, PersonIdNotFoundException {
// 这个是被测试的方法
Network network = this.network;
Network oldNetwork = this.oldNetwork;
int coupleSum = network.queryCoupleSum();
Person[] personsInNetwork = ((MyNetwork) network).getPersons();
Person[] personsInOldNetwork = ((MyNetwork) oldNetwork).getPersons();
boolean flag = true;
// code is here ...
// two for circle to check if network changed
flag = flag && personsInNetwork.length == personsInOldNetwork.length;
assertTrue(flag); // 前后network对比,测试pure
int sum = 0;
HashSet<Integer> hasAcquaintance = new HashSet<>();
// code is here ...
// calculate the value by JML, then compare with the method we made.
assertEquals(coupleSum, sum / 2);
}
@Parameterized.Parameters
public static Collection prepareData() throws EqualPersonIdException,
PersonIdNotFoundException, EqualRelationException {
// 准备测试样例
long seed = System.currentTimeMillis();
Random random = new Random(seed);
int testNum = 100;//测试次数,可根据需求调整
//该二维数组的类型必须是Object类型的
//该二维数组中的第一维代表有多少组测试数据,有多少次测试就会创造多少个PathTest对象
//该二维数组的第二维代表MyNetworkTest构造方法中的参数,位置一一对应
Object[][] object = new Object[testNum][];
for (int i = 0; i < 50; i++) {
List<Network> networkList = generateNetwork(100);
Network network = networkList.get(0);
Network oldNetwork = networkList.get(1);
object[i] = new Object[]{network, oldNetwork};
}
for (int i = 50; i < 100; i++) {
List<Network> networkList = generateNetwork(10);
Network network = networkList.get(0);
Network oldNetwork = networkList.get(1);
object[i] = new Object[]{network, oldNetwork};
}
return Arrays.asList(object);
} // 将generate产生的信息进行整合,参数化处理
public static List<Network> generateNetwork(int percent) throws EqualPersonIdException,
PersonIdNotFoundException, EqualRelationException {
long seed = System.currentTimeMillis();
Random random = new Random(seed);
Network network1 = new MyNetwork();
Network network2 = new MyNetwork();
List<Network> networkList = new ArrayList<>();
for (int i = 0; i < personNum; i++) {
int id = i;
String name = generateRandomString();
int age = random.nextInt(upperAgeBound);
Person person1 = new MyPerson(id, name, age);
Person person2 = new MyPerson(id, name, age);
network1.addPerson(person1);
network2.addPerson(person2);
// code here ...
// 生成两个相同person,相当于深克隆
}
for (int i = 0; i < personNum; i++) {
for (int j = i + 1; j < personNum; j++) {
int truth = random.nextInt(percent);
if (truth == 1) {
int value = random.nextInt(10 - 1) + 1;
network1.addRelation(i, j, value);
network2.addRelation(i, j, value);
}
}
}// code here ... 添加network中的关系
networkList.add(network1);
networkList.add(network2);
return networkList;
} // 这个方法目的是产生测试对象的基本信息。
}
规格设计是给出方法的基本设计目标,包含一些边界情况的细节考虑。
实现则是采用自己的方法进行代码编写,目的是实现规格给出的目标,并且考虑到其中的边界和细节问题。
一般情况实现都不会严格按照规格的思路编写,因为规格给的是最基础的方法,具体实现还需要考虑到性能上的需求,所以会选择最合适的方式来编写代码。
本单元主要目标就是训练我们读写jml语言的能力,并且在作业任务中带我们回顾了数据结构中的图算法,针对性训练了动态维护,脏位设置相关的性能问题。
另外,我们还学习了Junit测试的编写,学习了参数化测试的写法和测试的针对性设计。