1. 程式人生 > 其它 >BUAA OO 2022 第三單元

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規範來編碼是非常值得的。