BUAA-OO-Unit 3-Summary
BUAA-OO-Unit 3-Summary
一、综述
OO第三单元以完善一个存在个体、群组、消息的社交网络为目的。第一次作业考察基础的社交关系,引入并查集算法寻找个体之间的联系;第二次作业则增设了消息这一元素,并引入最小生成树算法;第三次作业迭代增设了新的消息种类,同时需要使用最短路径算法。通过三次作业的迭代开发,锻炼并考察了阅读JML规格的能力以及对一些涉及到图论的经典数据结构算法的掌握。
二、从JML角度构造测试数据
JML为代码提供了严格的约束,通过仔细阅读JML可以找到许多边界条件以及容易疏忽的细节。比如:
- 群组人数上限为
1111
人,不能超出。当超过时,不再往组里加人,但是不会抛出异常。 - 红包消息的金额计算,要严格按照JML的要求来,而不能想当然的写。
同时,JML便于观测方法的复杂度,从而可以构造极限数据进行测试,方便进行优化。
相比第二单元,随机数据又变得有效了起来,但是这会导致无法找出bug具体是在哪里出现。因此单元测试是十分有效且必要的,从JML约束的规格出发,对所有作业中涉及的指令进行全覆盖测试,从而更精准的定位bug。
三、架构设计
本单元作业中主体架构都在官方包中给出,因此无需进行额外的架构设计。对我个人而言,额外增设了edge
类表示边、UnionFind
并查集类、迪杰斯特拉算法类以及用于堆优化比较的Priority
类与Comparator
类。这些新增设的类具有单一功能,且解耦性都很好。
四、容器选择
MyPerson:
private final HashMap<Person,Integer> acquaintance;//key:person value:value。将熟人与社交值放在同一个容器中。
private final ArrayList<Message> messages;
MyGroup:
private final HashMap<Integer,Person> people;//key:id value:person
MyNetwork:
private final HashMap<Integer, Person> people;//key:id value:person
private final HashMap<Integer, Group> groups;//key:id value:group
private final HashMap<Integer, Integer> emojiMap;//key:id value:heat
可以看到,为了简化查找的复杂度,基本上都选择了HashMap
作为容器,有效优化了性能。
五、图模型构建与维护策略
-
并查集算法
代码如下:
public class UnionFind { private final HashMap<Integer,Integer> father;//key:节点id value:对应的父节点id private int countFather; public UnionFind() { father = new HashMap<>(); countFather = 0; } public void add(int id) { father.put(id,id); //构造一个新的节点,其初始父节点为自身 countFather++; } public int findFather(int id) { if (id == father.get(id)) { return id; } else { int newFather = findFather(father.get(id)); father.put(id,newFather); return newFather; } } public void union(int id1,int id2) { int father1 = findFather(id1); int father2 = findFather(id2); if (father1 != father2) { father.put(father2,father1); countFather--; } } public int getCountFather() { return countFather; } }
在一般的并查集上,进行了
路径压缩
,这样可以提高效率,同时为最小生成树算法提供便利。 -
最小生成树算法:
代码如下:
@Override public int queryLeastConnection(int id) throws PersonIdNotFoundException { if (contains(id)) { UnionFind newUnionFind = new UnionFind(); TreeSet<Edge> edges = getEdges(id, newUnionFind); HashSet<Edge> edgesLeast = new HashSet<>(); int sum = 0; for (Edge edge : edges) { if (newUnionFind.findFather(edge.getId1()) != newUnionFind.findFather(edge.getId2())) { edgesLeast.add(edge); newUnionFind.union(edge.getId1(), edge.getId2()); } } for (Edge edge : edgesLeast) { sum += edge.getValue(); } return sum; } else { throw new MyPersonIdNotFoundException(id); } }
我使用的是
kruskal
算法。首先,对于边集,我采取
TreeSet
这一容器进行存储,减少排序花费的时间,提升贪心算法的效率。值得注意的是这样的情况,当两条边权值一样但边节点不同时,两条边都要加入set中,因此进行比较时使用的equals方法与自己新增的Comparator需要格外注意。如下:@Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Edge edge = (Edge) o; return ((id1 == edge.id1 && id2 == edge.id2) || (id1 == edge.id2 && id2 == edge.id1)) && value == edge.value; } public class MyComparator implements Comparator<Edge> { @Override public int compare(Edge o1, Edge o2) { if (o1.getValue() == o2.getValue() && !o1.equals(o2)) { return 1; } else { return o1.getValue() - o2.getValue(); } } }
其次,使用
并查集
进行优化,方便查询两节点是否在同一集合中。 -
最短路径算法
我使用的是
迪杰斯特拉算法
。代码如下:
/*people:连通图里所有人 visitedMap:存储该节点是否被使用,初始均为0 disMap:存储最短距离的图。在构造方法中初始化,其中,与起始点不直接相连的点,距离设为最大。 priorities:用于进行堆优化的队列 */ public Dijkstra(Person p1,Person p2,HashMap<Integer,Person> peopleCircled) { this.people = peopleCircled; this.disMap = new HashMap<>(); this.visitedMap = new HashMap<>(); this.targetId = p2.getId(); this.priorities = new PriorityQueue<>(cmp); for (Person p : people.values()) { if (p.equals(p1)) { disMap.put(p.getId(),0); visitedMap.put(p.getId(),0); Priority priority = new Priority(p.getId(),0); priorities.add(priority); } else if (p1.isLinked(p)) { disMap.put(p.getId(),p1.queryValue(p)); visitedMap.put(p.getId(),0); Priority priority = new Priority(p.getId(),p1.queryValue(p)); priorities.add(priority); } else { disMap.put(p.getId(),100000000); visitedMap.put(p.getId(),0); Priority priority = new Priority(p.getId(),100000000); priorities.add(priority); } } } public int getMinDis() { while (true) { int min = 100000000; int minId = -1; Priority priority = priorities.poll(); if (visitedMap.get(priority.getId()) == 1) { continue; } min = priority.getValue(); minId = priority.getId(); if (targetId == minId) { return min; } visitedMap.put(minId,1); Person cur = people.get(minId); for (Person person : ((MyPerson)cur).getAcquaintance().keySet()) { int id = person.getId(); if (visitedMap.get(id) == 0 && disMap.get(id) > disMap.get(minId) + cur.queryValue(person)) { disMap.put(id,disMap.get(minId) + cur.queryValue(person)); Priority priorityNew = new Priority(id, disMap.get(minId) + cur.queryValue(person)); priorities.add(priorityNew); } } } }
对于与起始点不直接相连的点,距离设为整型的最大值,算是一个小trick。
使用
优先队列
得到最小权值点,可以有效提升效率。需要注意的是,更新距离后,要将更新后的值加入优先队列中。同时,通过支点更新距离时,只需要遍历支点的
acquaintance
即可,否则时间复杂度会高很多。
六、遭遇的性能问题与修复情况
遭遇的问题
-
qgvs
:该指令需要求出指定group中所有关系中value的和,如果完全按照jml来写,会用到二重循环,因此当数据量很大时会超时。
优化方法是,维护一个属性
valueSum
,在进行addPerson
、delPerson
、addRelation
时对该属性进行实时更新即可。 -
qgav
:与
qgvs
相似,该指令要得到组中年龄的方差,同样会出现二重循环导致复杂度高的问题。解决方法也是一样,维护一个属性
ageSum
实时更新即可。 -
最短路径算法中出现的问题:
在迪杰斯特拉的“更新”步骤中,我刚开始的写法遍历了联通图的所有节点,这样大大减少了效率。
解决方法是,只要遍历当前支点的
acquaintance
即可。
bug
第二次互测中由于qgvs
与qgva
的问题被hack了两次。
强测均没有出现问题。
七、Network扩展
题目要求
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)。
新增接口与方法
- Advertiser:继承Person,有productId的属性。
- Producer:继承Person,有productId的属性。
- Customer:继承Person,有preference的属性。
- Product:新开设的类,代表产品,有id以及money属性。
- AdvertisementMessage:继承Message,有advertiserId、producerId、productId的属性。
- PurchaseMessage:继承Message,有advertiserId、producerId、productId的属性。
- Network:需新增方法addAdvertiser、addProducer、addProduct、containsAdvertisementMessage、addAdvertisementMessage、sendAdvertisementMessage、containsPurchaseMessage、addPurchaseMessage、sendPurchaseMessage方法。
JML规格:
-
containsProduct
:/*@ ensures \result == (\exists int i; 0 <= i && i < productList.length; /*@ productList[i].getProductId() == productId); public /*@ pure @*/ boolean containsProduct(int productId);
-
addProduct
:/*@ public normal_behavior @ requires !(\exists int i; 0 <= i && i < products.length; @ products[i].getProductId() == product.getProductId(); @ assignable products; @ ensures (\forall int i; 0 <= i && i < \old(products.length); @ ensures (\exists int i; 0 <= i && i < products.length; products[i] == product); @(\exists int j; 0 <= j && j < products.length; products[j] == (\old(products[i])))); @ ensures products.length == \old(products.length) + 1; @ also @ public exceptional_behavior @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length; @ products[i].equals(product)); @*/ public void addProduct(Product product) throws EqualProductIdException;
-
sendAdvertisementMessage
:/*@ public normal_behavior @ requires containsMessage(id) @ && getMessage(id).getType() == 1 @ && getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()) @ && getMessage(id).getPerson1() instanceof Advertiser @ && ProductList.contain(product); @ assaignable product; @ assaignable people[*] @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 && @ (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id; @ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))); @ ensures (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==\old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue()); @ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);\old(people[i].getSocialValue()) @ == people[i].getSocialValue()); @ ensures money = \old(money) + money @ also @ public exceptional_behavior @ signals (MessageIdNotFoundException e) !containsMessage(id); @ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 && @ !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1())); @*/ public void sendAdvertisementMessage(Product product, int money, int id) throws MessageIdNotFoundException;
八、学习体会与建议
学习体会
本单元OO学习让我第一次接触到规格化编程。良好的JML为程序提供了严谨的约束,作为实现者,必须严格执行JML的规格要求,即进行契约式编程。相比前两个单元,本单元借助JML的帮助无需过多考虑架构的问题,所需要完成的需求也一目了然,任务更加清晰,完成起来属实是轻松愉快不少。
但是,在严格执行规格要求的同时,不能被JML拘束。通过三次作业的迭代开发,我深刻认识到JML告诉你需要达成什么样的目的,而非告诉你怎么完成目的。规格是不可逾越的达摩克斯之剑,在符合规格的前提下,更要灵活设计,擅用算法与容器进行优化维护,切忌仗着有JML就按部就班,不过脑子。
本单元的学习,第一感觉就是JML太棒了!但是在日后的开发过程中,不可能有现成的JML供自己参考;因此,本单元通过给定的JML规格编写代码的过程中,那些对面向对象更深入的理解,以及对于规格化编程裨益的认识,我想才是这单元最珍贵的收获吧。
建议
首先,和上单元一样,中测偏弱。尤其是前两次,甚至都存在一些指令没考察到的情况。因此还是建议增加一些强度,至少做到指令全覆盖。
其次,我个人认为这个单元以JML为主,但三次作业主要的难点还是在图论算法上,稍微有些懵逼。也许可以减少对算法的考察,而将根据JML进行单元测试的部分内容作为考察内容。