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规范来编码是非常值得的。

posted @ 2022-06-01 16:11  Mmmusel  阅读(40)  评论(0编辑  收藏  举报