BUAA OO 2022 第三单元
一、利用JML规格构造测试数据
兼顾正常行为和异常行为
- 例如对于ar指令,exceptional_behavior是
!contains(id1) || !contains(id2) || getPerson(id1).isLinked(getPerson(id2))
- 生成指令时把people_id的随机数在总人数的基础上增加50,
"ar {} {} {}\n".format(id1, id2, randint(1, (people_num + 50)))
,从而保证正常行为和异常行为都可以覆盖到
根据Jml限制,设置边界数据
- 比如group的人数上限1111,getReceivedMessage()的条数为4条
- qgav当people=0的时候需要抛出异常,容易遗忘出现除0错误
高复杂度的指令测试是否超时
- queryGroupValueSum不限制条数,需要考虑动态维护的方式避免tle
- queryBlockSum和iscircle指令,如果没有使用并查集使用dfs会超时
一条指令触发多种异常时的行为
- 随机数据不能保证把各种异常类型的组合完全覆盖,需要手动构造特定异常的测试数据保证完备性。
二、架构设计:图模型构建和维护策略
hw9
PersonClass
- 维护一个集合groups:person所在的所有group
GroupClass
- 维护三个字段
private int ageSum;
private int squareAgeSum;
private int valueSum;
- atg()方法中维护上述四个字段:
public void addPerson(Person person) {
people.putIfAbsent(person.getId(), person);
int tmp = person.getAge();
((PersonClass) person).addGroups(this);
ageSum += tmp;
squareAge += (tmp * tmp);
people.forEach((k,v) -> valueSum += v.queryValue(person));
//所有和新进组的person有关系的人和新进组人之间的value加进groupValueSum
}
- 并且对于valueSum字段:在NetWork类的addRelation()方法也要维护:如果person1和person2在一个group,给此group加上两倍的value。
HashMap<Integer,GroupClass> g1 = sp1.getGroups();
HashMap<Integer,GroupClass> g2 = sp2.getGroups();
g1.forEach((k,v) -> { if (g2.containsKey(k)) { v.addValue(value); } });
- dfg()方法同理
- 需要注意在使用公式计算AgeVar时,关于整除的误差.
public int getAgeVar() {
if (people.isEmpty()) {
return 0;
}
long mean = age / (people.size());
long tmp = squareAge - 2 * mean * age + (people.size()) * mean * mean;
int ans = (int)(tmp / (people.size()));
return (ans);
//(squareAge / (people.size()) - 2 * mean * mean)为错误公式,会由于整除而产生误差。
}
NetworkClass
- 需要维护连通分支。采用并查集,维护连通分支的顶层父节点和最大连通分支数量,分别用于isCircle()和getBlockSum()。
- addPerson()时,需要在union中新建Person节点,自己为一个连通分支,最大连通分支数量++。
- addRelation()时,需要union.merge(person1,person2),找到两个人的顶层父节点。如果不同,合并两个连通分支,更新节点的父节点,最大连通分支数量--。
- isCircle()时,找到两个人的顶层父节点判断是否相同即可
hw10
queryLeastConnection()指令,需要找节点所在连通分支的最小生成树
- 处理策略是建一个SuperUnion类继承并查集Union类,使用基于并查集的Kruskal最小生成树算法
- 算法需要两个新维护的字段:每个顶层父节点的Id为key,维护好本连通分支的边集blockEdgeMap和节点的数量blockPoints。
private HashMap<Integer,ArrayList<MemEdge>> blockEdgeMap;
private HashMap<Integer,Integer> blockPoints;
- addRelation()调用union.merge()时:
- 找到对应父节点id的边集,加入关系这条边,边的MemEdge类保存两个节点和边权。
- 如果发生两个连通分支合并,需要把新的顶层父节点的边集和点集都进行更新合并。
- 基于并查集的Kruskal最小生成树算法伪代码:
从NetWork维护的并查集里取出people_id的所在的连通分量block;
为了判断最小生成树不成环,为这个block new一个基础并查集union;
取出block的 边集&点的数量;
sort(边集)
int nowEdges=0;//最小生成树已加入的边数
for(边集的每条边){
if(id1节点没有被加进过unionTmp){
unionTmp.addPoint(id1)
}
if(id2节点没有被加进过unionTmp){
unionTmp.addPoint(id2)
}
id1的父亲 = unionTmp.find(边的id1)
id2的父亲 = unionTmp.find(边的id2)
if(id1的父亲 == id2的父亲){
//成环
continue;
} else {
unionTmp.merge(id1,id2);
ans += 这条边的weight;
nowEdges++;
}
if (nowEdges ==( block点的总数-1) {
break;
}
}
return ans;
hw11
PersonClass
- 设置noticeDirty,如果未加入notice消息,则clearNotices()时不需要遍历,直接返回即可。
public void clearNotices() {
if (noticeDirty) {
messages.removeIf(message -> message instanceof NoticeMessage);
noticeDirty = false;
}
}
sendIndirectionMessage():堆优化的Dijkstra最短路
HashMap<Integer, Integer> distance = new HashMap<>();
//起点到各节点间的最短路
HashSet<Integer> points = new HashSet<>();
//找过的节点
PriorityQueue<NodeMessage> pq = new PriorityQueue<>(Comparator.comparing(NodeMessage::getDis));
//优先队列实现堆优化
pq.add(new NodeMessage(id1, 0));
distance.put(id1, 0);
//加入起点
while (!pq.isEmpty()) {
NodeMessage node = pq.poll();
int nextId = node.getId();
int thisDis = node.getDis();
if (nextId == id2) {
return node.getDis();
}
//最短路径更新到终点后,退出即可,不需要遍历全图
if (points.contains(nextId)) { continue; }
//节点已经被计算出最短路径,跳过
points.add(nextId);
//把未经过的节点标记为经过
Set<Integer> set = ((PersonClass)(people.get(nextId))).getAcquaintance().keySet();
//遍历这个点的所有通路
for (Integer next: set) {
if (points.contains(next)) { continue; }
//节点已经被计算出最短路径,跳过
int dis = thisDis;
try {
dis += queryValue(next, nextId);
} catch (Exception e) {
e.printStackTrace();
}
//没有计算过这个点的最短路或这个点的最短路需要更新
if ((!distance.containsKey(next)) || (distance.get(next) > dis)) {
distance.put(next,dis);
pq.offer(new NodeMessage(next,dis));
}
}
}
三、性能问题和修复情况
3.1 性能
- queryBlockSum和iscircle指令,如果没有使用并查集使用dfs会超时.
- queryGroupValueSum不限制条数,需要考虑动态维护的方式避免tle,在addToGroup()和addRelation()时动态维护。
3.2 规范
- jml阅读的细节:group的人数上限1111,getReceivedMessage()的条数为4条
- 异常的处理顺序:
```互测时被hack到一个点:由于EmojiIdNotFoundException, EqualPersonIdException两个异常的条件并不是互斥的,所以EqualPersonIdException的判断条件不应该使用else if
,应该使用if
。
public void addMessage(Message message) throws EqualMessageIdException,EmojiIdNotFoundException, EqualPersonIdException {
if (messages.containsKey(message.getId())) {
throw new MyEqualMessageIdException(pid);
}
else if (message instanceof EmojiMessage) {
if (!emojiHeat.containsKey(emojiId)) {
throw new MyEmojiIdNotFoundException(emojiId);
}
}
else if ((message.getType() == 0) && (message.getPerson1().equals(message.getPerson2()))) {
throw new MyEqualPersonIdException(message.getPerson1().getId());
}
//....
}
- 不能在遍历时remove。可以使用ArrayList.removeIf()方法。或者使用hashmap的迭代器
public int deleteColdEmoji(int limit) {
Iterator<Map.Entry<Integer,Integer>> iter = emojiHeat.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<Integer,Integer> entry = iter.next();
if ((entry.getValue()) < limit) {
int emojiKey = (entry.getKey());
iter.remove();
for (Integer id:emojiMessages.get(emojiKey)) {
messages.remove(id);
}
emojiMessages.remove(emojiKey);
}
}
return emojiHeat.size();
}
四、Network的扩展与JML规格
Producer:产品生产商增加产品
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < products.length; products[i].equals(Product));
@ assignable products;
@ ensures products.length == \old(products.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(products.length);
@ (\exists int j; 0 <= j && j < products.length; products[j] == (\old(products[i]))));
@ ensures (\exists int i; 0 <= i && i < products.length; products[i] == Product);
@ also
@ public exceptional_behavior
@ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length;
@ products[i].equals(Product));
@*/
public void addProduct(/*@ non_null @*/Product Product) throws EqualProductIdException;
NetWork: 特定Advertiser向所有存在联系的消费者发送特定产品的广告
/*@ public normal_behavior
@ requires containsProduct(productId) && contains(advertiserId)
@ assignable people[*].advs;
@ ensures ((\forall int i; 0 <= i && i < people.length && people[i] instanceof Customer && people[i].isLinked(getPerson(advertiserId))
@ (people[i].advs.length == \old(people[i].advs.length) + 1) &&
@ (\forall int j; 0 <= j && j < \old(people[i].advs.length);
@ (\exists int k; 0 <= k && k < people[i].advs.length; people[i].advs[k].equals(\old(people[i].advs[j])))) &&
@ (\exists int j; 0 <= j && j < people[i].advs.length; people[i].advs[j].product.equals(getProduct(productId)) &&
@ people[i].advs[j].advtiser.equals(getPerson(advertiserId)));
@ ensures (\forall int i; 0 <= i && i < people.length && (!(people[i] instanceof Customer) || !(people[i].isLinked(getPerson(advertiserId))));
@ (\forall int j; 0 <= j && j < people[i].advs.length; people[i].advs[j].equals(\old(people[i].advs[j]))));
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(productId);
@ signals (PersonIdNotFoundException e) containsProduct(productId) &&
@ !(contains(advertiserId));
@*/
public void sendAdvs(int productId, int advertiserId) throws ProductIdNotFoundException, PersonIdNotFoundException;
Customer:消费者购买广告中偏好匹配的产品来购买
/*@ public normal_behavior
@ requires contains(customerId)
@ assignable people[*].advs, people[*].saledAdvs;
@ ensures ((\forall int i; 0 <= i && i < getPerson(customerId).advs.length && getPerson(customerId).liked(getPerson(customerId).advs[i].product)
@ (\forall int j; 0 <= j && j < people[getPerson(customerId).advs.advertiser].saledAdvs.length;
@ (people[getPerson(customerId).advs.advertiser].saledAdvs[j].equals(getPerson(customerId).advs[i].product))) &&
@ (\forall int j; 0 <= j && j < \old(people[getPerson(customerId).advs.advertiser].saledAdvs.length);
@ (\exists int k; 0 <= k && k < people[getPerson(customerId).advs.advertiser].saledAdvs.length;
@ people[getPerson(customerId).advs.advertiser].saledAdvs[k].equals(\old(people[getPerson(customerId).advs.advertiser].saledAdvs[j]))));
@ ensures getPerson(customerId).advs.length == 0;
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) (contains(advertiserId));
@*/
public void buyLikedAdvs(int customerId) throws ProductIdNotFoundException, PersonIdNotFoundException;
五、学习体会
尽管jml在阅读理解时并不如文字性的描述理解地快,在刚刚学习第三单元时觉得很繁琐,尤其是在课上experiment时,经常由于jml的阅读速度慢造成写不完的情况。但是在进入第四单元的uml作业时,经过对比之后我非常明显地发现了jml无与伦比的优势:第三单元关于题目要求与实现的所有细节都可以通过jml获取,而第四单元的文字性叙述却带来了相当多的理解上的混乱与困扰,需要不断询问助教获取题目的细节。所以jml确实是一种非常高效明晰的编码规范,可以规避很多不必要的理解偏差,所以尽管阅读jml的能力需要花费一些时间来练习,但遵循jml规范来编码是非常值得的。