#BUAA-面向对象设计与构造 ——第三单元总结#
——第三单元总结(JML)
单元主题
完成简单社交网络关系的模拟,并实现一些信息的查询,待完成的函数都已经通过JML规格给出,完全按照规格中的写法进行函数编写可以完全保证正确性,但于时间效率方面会出现问题。
架构设计
这个单元的架构关键在于建立图模型并实现动态图的维护,涉及了许多图论部分的知识,在其中比较重要的概念是——极大连通分量,整个网络可以看作是极大联通分量的集合,同时许多操作的查询和实现都基于此,如最短路径,最小生成树的查询。
本单元的图的构建具有动态性,整个社交网络是从空开始的,通过指令不断向其中增加结点,再在节点间增加关系,最后逐步形成网络,而且没有从网络中删除结点的操作。所以对整个图的维护只基于下面两个操作:
-
添加节点:
addPerson
-
添加关系:
addRelation
对于极大连通分量的处理,我没有采用并查集的策略,而是新建了一个类graph
,用以指代一个极大联通分量,同时graph
还成为了一个工具类,上文提到的最短路径和最小生成树的查询操作都封装在了这个类中,提高了扩展性,同时做到了解耦合。
-
当向图中仅仅增加一个结点(
addPerson
操作)时,就会创造一个新的联通分量,此时只需要新建一个graph
对象即可。所以判断两个person
是否在同一联通分量中就转化为判断他们是否在同一graph
对象中,免去了照搬规格中二重循环的写法,减少了时间复杂度。 -
另一方面,当为结点添加关系时,首先判断两个结点是否已经在同一个
graph
对象中了,如果成立,那么只需为两个结点添加关系即可;反之,采用基于重量的方式将两个节点的graph
对象合并(将包含节点数目少的graph
对象添加到多的对象中。
有关代码如下:
public void addPerson(Person person) throws EqualPersonIdException {
if (contains(person.getId())) {
throw new MyEqualPersonIdException(person.getId());
} else {
(this.peopleHash).put(person.getId(), (MyPerson) person);
Graph graph = new Graph(person.getId(), this.graphCounter);
this.graphs.add(graph);
this.graphCounter++;
this.qbs++;
}
}
public void addRelation(int id1, int id2, int value) throws
PersonIdNotFoundException, EqualRelationException {
if (contains(id1) && contains(id2) &&
!(getPerson(id1).isLinked(getPerson(id2)))) {
Graph graph1 = queryGraph(id1);
Graph graph2 = queryGraph(id2);
Graph graphDes = graph1;
if (!graph1.equals(graph2)) {
this.qbs--;
/* 不是一个联通分量的关系才需要更新 */
int size1 = graph1.getSize();
int size2 = graph2.getSize();
/* 基于重量的合并 */
if (size1 < size2) {
graph2.unionGraph(graph1);
graphDes = graph2;
this.graphs.remove(graph1);
} else {
graph1.unionGraph(graph2);
this.graphs.remove(graph2);
}
}
.......
性能问题和修复情况
粗略计算了一下cpu的时间限制,如果能够保证在作业中没有出现O(N^2)
的代码,应该就不会出现TLE
问题
复杂度较高的几个操作是:
-
查询联通分量的数目
query_block_sum
若照搬规格,复杂度为
O(N^2)
/*@ ensures \result == @ (\sum int i; 0 <= i && i < people.length && @ (\forall int j; 0 <= j && j < i; !isCircle(people[i].getId(), people[j].getId())); @ 1); @*/
-
如果采用上述方法,维护一个
qbs
变量,那么复杂度变为O(1)
-
-
查询两个person是否在同一个联通分量中
isCircle
-
采用
graph
对象的比较,复杂度将为O(1)
-
-
查询某个群组的价值总和
queryGroupValueSum
-
得益于图的逐步建构,因此在每个
group
中维护一个valueSum
值即可,当增加Person
和删除Person
时,对其进行相应改变,查询操作变为O(1)
public void addPerson(Person person) { for (MyPerson myPerson : this.people) { if (myPerson.isLinked(person)) { this.valueSum += 2 * myPerson.queryValue(person); } } ..... }
-
同时,关系具有自反性和对称性,因此算一半的就好。
-
-
最小生成树算法
堆可以用优先级队列
PriorityQueue
进行实现,容器中存储的类需要实现Comparable
接口。提供了
poll()
和offer()
方法。但需要注意的是:
PriorityQueue
用于寻找最小的元素很方便,却不能直接用于排序会出现即使容器内的元素相同,但调用
toString()
方法输出的列表却存在差异的情况-
Kruskal算法
核心是对边进行操作
-
按照边的权重建立最小堆
-
取出最小堆堆顶数据,并判断两端节点是否在同一集合
-
如不在,则将这两个节点添加到同一集合,接着将边加入生成边,如在,则不进行操作,为无效边
-
重复上面的操作,直到所有的边都检查完
-
-
Prim算法
核心是对结点进行操作
-
选定起始结点,并将与该节点相连的所有边建立成堆
-
从堆中取最小的边,然后判断to节点是否被访问过,如果没有,将这个边加入生成树,并标记该节点访问。
-
然后将to节点所相连的边添加到最小堆中。
-
循环上面的操作,直到所有的节点遍历完。
public int solveQlc(int fromId) { // notChange用于标识该联通分量自上一次查询后是否添加过元素或者新的关系 if (!notChange) { weightSum = 0; // 权重和,也就是qlc的值 // 核心数据结构,存储边的优先级队列 // 也就是到还没有连接上最小生成树上的结点的边 PriorityQueue<Edge> pq = new PriorityQueue<>(); // 用于标志某个结点是否已经加入了最小生成树中 HashMap<Integer, Boolean> in = new HashMap<>(); for (Integer integer : graph.keySet()) { in.put(integer, false); } in.put(fromId, true); nodeIn(fromId, in, pq); while (!pq.isEmpty()) { Edge edge = pq.poll(); int toId = edge.getToId(); if (in.get(toId)) { // 节点 to 已经在最小生成树中,跳过 // 否则这条边会产生环 continue; } // 将边 edge 加入最小生成树 weightSum += edge.getValue(); in.put(toId, true); nodeIn(toId, in, pq); } notChange = true; } return weightSum; } public void nodeIn(int nodeId, HashMap<Integer, Boolean> in, PriorityQueue<Edge> pq) { ArrayList<Edge> oneNode = graph.get(nodeId); // 加入与这个结点相连的所有边 for (Edge edge : oneNode) { if (in.get(edge.getToId())) { continue; } pq.offer(edge); } }
-
-
-
最短路径算法:
主要包含两个集合:
已加入路径的节点集合S
和未加入路径的节点集合U
-
确认起始结点s,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为”起点s到该顶点的距离
-
从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时,从U中移除顶点k。
-
更新U中各个顶点到起点s的距离。
在最后一次作业的最短路径算法中出现了二重循环,导致复杂度变成了
O(V*E)
,如果采用优先级队列进行优化,可将复杂度降为O(V*logE)
public int solveSim(int fromId, int toId) { HashMap<Integer, Integer> dis = new HashMap<>(); // 到连通分量每个结点的距离:结果 PriorityQueue<Edge> heap = new PriorityQueue<>(); for (Integer integer : this.graph.keySet()) { dis.put(integer, Integer.MAX_VALUE); } dis.put(fromId, 0); // 初始设置自己到自己是0 heap.add(new Edge(fromId, 0)); ArrayList<Integer> in = new ArrayList<>(); // 标志结点是否添加进了结点集 while (!heap.isEmpty()) { Edge edge = heap.poll(); assert edge != null; int now = edge.getToId(); int baseValue = edge.getValue(); if (in.contains(now)) { continue; } in.add(now); if (in.contains(toId)) { break; } ArrayList<Edge> oneNode = graph.get(now); for (Edge edge1 : oneNode) { int to = edge1.getToId(); int newValue = edge1.getValue() + baseValue; if (dis.get(to) > newValue) { dis.put(to, newValue); heap.add(new Edge(to, newValue)); } } } return dis.get(toId); }
-
测试
依然采用了手搓数据的方式,测试当满足JML规格
的前置条件时,是否能出现相应的后置条件。此处应该要保证覆盖所有的前置条件。
对于后置条件的正确性判定则相对比较困难:
-
如果是抛出异常行为的,看是否有相应异常抛出即可
-
需要注意一些细节,比如id按照大小进行排序是否实现等等
-
-
如果是给出了明确的可直接实现的
for
循环或其他后置条件,但又和自身函数实现有差异的情况,可以运用Junit
插件(有点类似计组的Testbench
,方便测试,可以不用考虑输入接口进行特定对象的构造),在插件里直接翻译JML规格
,然后与实际函数运行结果进行比较,正确性可以得到保证。-
Junit
-
会针对相应的代码文件生成与内部函数相对应的测试模块
@Test
-
起始和结尾会有
@Before
以及@After
,在这两部分填写的内容会在每次调用一个测试模块的前后运行
-
-
举个栗子
/*@ public normal_behavior @ requires contains(id); @ ensures (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && @ \result == people[i]); @ also @ public normal_behavior @ requires !contains(id); @ ensures \result == null; @*/
@Test public void testGetPerson() throws Exception { //TODO: Test goes here... MyNetwork myNetwork = new MyNetwork(); MyPerson myPerson = new MyPerson(3,"kll",99); myNetwork.addPerson(myPerson); MyPerson result = (MyPerson) myNetwork.getPerson(3); Assert.assertEquals(result,myPerson); result = (MyPerson) myNetwork.getPerson(88); Assert.assertNull(result); }
输出结果为
Before Method After Method 进程已结束,退出代码0
说明测试成功
-
-
对于比较复杂的规格,一定要进行层层拆分,可以以分号为单位进行剥离,就可以得到如下图的划分好层次的规格,这样理解错问题的概率就会大大降低
-
但是相应的,越复杂的规格的目的性更好从函数名中猜出,所具备的特征也就越明显,这样的情况找同学对拍是比较理想的测试方法
-
同时针对复杂度较高的函数构造了在所给数据限制内的测试数据,
对NetWork的扩展
假设出现了几种不同的Person
-
Advertiser:持续向外发送产品广告
-
Producer:产品生产商,通过Advertiser来销售产品
-
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
-
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)
类扩展
-
新增
Product
类public Product{ private int proId;//对于所有产品独一无二的id private String type;//用于识别商品种类 private double price;//该商品的价格 private double sellSum; // 该商品的销售额 private Producer producer; private ArrayList<Integer> advs; private ArrayList<Integer> cus; /* 用于记录销售路径 每售卖一次,在ArrayList中存放对应的广告商和生产者*/ }
-
新增三个类
Advertiser
,Producer
,Customer
继承自Myperson
,这三个类中均有对应的String type
,用以标记发送广告,生产,消费的商品种类 -
新增
AdvertiseMessage
类以及PurchaseMessage
-
新增相应的异常类
-
在NetWork类里需要新增下列内容
// @ public instance model non_null Product[] productList; // @ public instance model non_null AdvertiseMessage[] ads; // @ public instance model non_null Customer[] customers; //@ ensures \result == (\exists int i; 0 <= i && i <proIdList.length; proIdList[i].getId() == proId); public boolean containsProduct(proId);
核心方法
-
Advertiser:向所有的消费者发送产品广告
-
Customer:向其接受到的信息中的指定广告商购买商品
-
Product:查询某个商品的销售额
public interface Network {
// @ public instance model non_null Product[] products;
//@ ensures \result == (\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
public /*@ pure @*/ boolean containsProduct(int id);
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
@ ensures (\exists int i; 0 <= i && i < products.length; products[i].getId() == id &&
@ \result == products[i]);
@ also
@ public normal_behavior
@ requires (\forall int i; 0 <= i && i < products.length; products[i].getId() != id);
@ ensures \result == null;
@*/
public /*@ pure @*/ Product getProduct(int id);
/*@ public normal_behavior
@ requires contains(personId) && containsMessage(id) && getMessage(id) instance of AdvertiseMessage
@ assignable messages,customers[*].messages,getMessage(id).getProduct().ads
@ ensures (\forall int i;0<=i&&i<customers.length;
@ (\forall int j; 0 <= j && j < \old(customers[i].getMessages().size());
@ \old(customers[i].getMessages().getMessages().get(j+1) == \old(customers[i].getMessages().getMessages().get(j))));
@ ensures (\forall int i;0<=i&&i<customers.length;
@ \old(customers[i].getMessages().get(0).equals(\old(getMessage(id)));
@ ensures (\forall int i; 0<=i&&i<customers.length; \old(customers[i].getMessages().size() == \oldcustomers[i].getMessages().size()) + 1;
@ 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 int i;0 <= i < \old(getMessage(id).getProduct().ads.size()));
@ (\exist int j;0 <= j < getMessage(id).getProduct().ads.size();
@ \old(getMessage(id).getProduct().ads.get(i))
@ ==getMessage(id).getProduct().ads.get(j)
@ ensures (\exist int i;0 <= i < getMessage(id).getProduct().ads.size();
@ getMessage(id).getProduct().ads.get(j)==personId);
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(personId);
@ signals (MessageIdNotFoundException e) contains(personId) && !containsMessage(msgId)
@ signals (AdvertiseMessageNotFoundException e) contains(personId) && containsMessage(id) && !(getMessage(id) instance of AdvertiseMessage)
@*/
public void sendAds(int personId,int id) throw PersonIdNotFoundException,MessageIdNotFoundException,AdvertiseMessageNotFoundException;
/*@ public normal_behavior
@ requires contains(personId) && containsMessage(id) && getMessage(id) instance of PurchaseMessage
@ assignable messages,getMessage(id).getPerson2().messages,
@ getMessage(id).getProduct().customers,getMessage(id).getProduct().sellSum
@ getMessage(id).getPerson1().money
@ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
@ \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
@ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
@ 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 int i;0 <= i < \old(getMessage(id).getProduct().customers.size()));
@ (\exist int j;0 <= j < getMessage(id).getProduct().customers.size();
@ \old(getMessage(id).getProduct().customers.get(i))
@ ==getMessage(id).getProduct().customers.get(j)
@ ensures (\exist int i;0 <= i < getMessage(id).getProduct().customers.size();
@ getMessage(id).getProduct().customers.get(j)==personId);
@ ensures (\old(getMessage(id)).getPerson1().getMoney() ==
@ \old(getMessage(id).getPerson1().getMoney()) - \old(getMessage(id)).getProduct()).getPrice();
@ ensures (\old(getMessage(id)).getProduct().getsellNum() ==
@ \old(getMessage(id).getProduct().getsellNum()) + \old(getMessage(id)).getProduct()).getPrice();
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(personId);
@ signals (MessageIdNotFoundException e) contains(personId) && !containsMessage(msgId)
@ signals (PurchaseMessageNotFoundException e) contains(personId) && containsMessage(id) && !(getMessage(id) instance of PurchaseMessage)
@*/
public void sendPurMsg(int id,int personId) throws PersonIdNotFoundException,MessageIdNotFoundException,PurchaseMessageNotFoundException;
}
/*@ public normal_behavior
@ requires containsProduct(proId);
@ ensures \result == getProduct(proId).getsellNum();
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(proId);
@*/
public double querySellNum(int proId) throws ProductIdNotFoundException;
学习体会
但是有很多细节的地方需要注意,不然可能不小心就会产生bug,导致强测的结果不尽如人意,而且由于作业是迭代开发的,也需要小心某些方法的JML规格
发生了变化。
这个单元大致就是酱紫啦,UML
单元加油鸭!