1. 程式人生 > 其它 >BUAA-OO第三單元總結

BUAA-OO第三單元總結

目錄

總述

本單元的任務是實現簡單的社交網路關係的模擬和查詢, 包括人與人互動、訊息收發等操作。學習目標是理解JML規格在面向物件設計與構造中的重要意義,並掌握利用JML規格提高程式碼質量的能力。官方包已經通過JML給定了整個社交網路的基本功能規格,如何設計層次之間的互動方法甚至額外層次是本單元作業的關鍵。

一、基於JML規格準備測試資料

JML為測試提供了劃分依據和判定依據,程式碼需要實現的功能規格都已給出,可以針對每個具體方法構造單獨構造測試資料。依據前置條件分別構造滿足不同前置條件的資料型別,通過後置條件和不變式檢查結果,以及異常行為是否正確。而用Junit可以實現測試執行的自動化。

仍然需要注意構造邊界資料,比如 group 中人數上限是1111。針對age相關計算、不同型別訊息的收發操作構造特殊資料。

由於部分操作需要遍歷,如果沒有沒有采用合適的資料結構或演算法,所有的查詢和分析都要遍歷整個大圖,效能會極低,可以針對此類操作如qgav、 qbs、qlc等構造大量查詢的資料。

二、架構設計與圖模型構建和維護策略

2.1 第一次作業

  • query_circle query_block_sum

    構架一個類 UnionFindSet 來維護並查集,採用按秩合併路徑壓縮優化。person 的 id 為圖中的節點,用 HashMap 維護父節點圖和節點秩圖,通過判斷兩點的頂級父節點是否相同來判斷兩點是否聯絡,同時在在增加節點和合並節點維護 blocknum。實現如下:

    private final HashMap<Integer, Integer> parent; //key id - value parentId
        private final HashMap<Integer, Integer> rank; //key id - value rank
        private int blockNum;
    // add person
    public void insert(int id) {
            parent.put(id, id);
            rank.put(id, 1);
            blockNum = blockNum + 1;
    }
    // add relation
    public void union(int id1, int id2) {
            int root1 = find(id1);
            int root2 = find(id2);
            if (root1 == root2) {
                return;
            }
            if (rank.get(root1) < rank.get(root2)) {
                parent.put(root1, root2);
            } else if (rank.get(root1) > rank.get(root2)) {
                parent.put(root2, root1);
            } else {
                parent.put(root2, root1);
                int oldRank = rank.get(root1);
                rank.put(root1, oldRank + 1);
            }
            blockNum = blockNum - 1;
    }
    // isCircle
    public int find(int id) {
            if (parent.get(id) != id) {
                int newParent = find(parent.get(id));
                parent.put(id, newParent);
            }
            return parent.get(id);
    }
    

2.2 第二次作業

  • query least connection

    採用並查集優化 Kruskal 演算法,在 NetWork 中的 unionFindSet 中過載 union 方法,用 HashMap<Integer, ArrayList> 維護每個 block 中的邊集合和邊數,求最小生成樹時獲取當前查詢節點的頂級父節點,進一步獲取對應的 block 資訊,然後傳給 Kruskal 方法。在 Kruskal 演算法中增加邊時也用並查集來判斷兩節點的聯通性。實現如下:

    更新的 UnionFindSet:

    private final HashMap<Integer, ArrayList<Edge>> edgeBlocks; // parentId - edgeBlock
    private final HashMap<Integer, HashSet<Integer>> personBlocks; // parentId - personBlock
    
    public void union(int id1, int id2, int value) {
            int root1 = find(id1);
            int root2 = find(id2);
            Edge edge = new Edge(id1, id2, value);
            ArrayList<Edge> edgeList1;
            ArrayList<Edge> edgeList2;
            if ((edgeList1 = edgeBlocks.get(root1)) == null) {
                edgeList1 = new ArrayList<>();
            }
            if ((edgeList2 = edgeBlocks.get(root2)) == null) {
                edgeList2 = new ArrayList<>();
            }
            HashSet<Integer> personSet1 = personBlocks.get(root1);
            HashSet<Integer> personSet2 = personBlocks.get(root2);
            if (root1 == root2) {
                edgeList1.add(edge);
                edgeBlocks.put(root1, edgeList1);
                personSet1.add(id2);
                personBlocks.put(root1, personSet1);
                return;
            }
            if (rank.get(root1) < rank.get(root2)) {
                parent.put(root1, root2); // root1'parent becomes root2
                edgeList2.add(edge);
                edgeList2.addAll(edgeList1);
                edgeBlocks.put(root2, edgeList2);
                edgeBlocks.remove(root1);
                personSet2.addAll(personSet1);
                personBlocks.put(root2, personSet2);
                personBlocks.remove(root1);
            } else {
                parent.put(root2, root1); // root2'parent becomes root1
                if (rank.get(root1).intValue() == rank.get(root2).intValue()) {
                    int oldRank = rank.get(root1);
                    rank.put(root1, oldRank + 1);
                }
                edgeList1.add(edge);
                edgeList1.addAll(edgeList2);
                edgeBlocks.put(root1, edgeList1);
                edgeBlocks.remove(root2);
                personSet1.addAll(personSet2);
                personBlocks.put(root1, personSet1);
                personBlocks.remove(root2);
    
            }
            blockNum = blockNum - 1;
    }
    

    Kruskal:

    public int kruskal(Network network, ArrayList<Edge> edges, int total) {
            int sum = 0;
            int num = 0;
            UnionFindSet ufs = new UnionFindSet();
            edges.sort(Comparator.comparingInt(Edge::getWeight));
            for (Edge edge : edges) {
                if (num >= (total - 1)) {
                    return sum;
                }
                int id1 = edge.getId1();
                int id2 = edge.getId2();
                if (!ufs.contains(id1)) {
                    ufs.insert(id1);
                }
                if (!ufs.contains(id2)) {
                    ufs.insert(id2);
                }
                if (!ufs.isCircle(id1, id2)) {
                    ufs.union(id1, id2);
                    sum = sum + network.getPerson(id1).queryValue(network.getPerson(id2));
                    num = num + 1;
                }
            }
            return sum;
    }
    

2.3 第三次作業

  • 注意區分什麼時候用 message 的 id,什麼時候用 emojiId。

  • send indirect message

    採用堆優化的 Dijkstra 演算法求最短路徑,用優先佇列 存放每個節點,需要注意 PriorityQueue 只有在增加或刪除元素時才會重新排序,僅修改其中元素的值並不會觸發排序。因為沒有考慮到這一點強測 WA 了5個點。

    public int dijkstra(Person src, Person dst) {
            PriorityQueue<MyPerson> pq =
                    new PriorityQueue<>(Comparator.comparingInt(MyPerson::getPath));
            ArrayList<Integer> over = new ArrayList<>();
            HashSet<Integer> visit = new HashSet<>();
            ((MyPerson) src).setPath(0);
            pq.add((MyPerson) src);
            visit.add(src.getId());
            while (!pq.isEmpty()) {
                MyPerson p = pq.poll(); // 取出 刪除 調整堆結構
                over.add(p.getId()); // 已找到最短路徑
                if (p.getId() == dst.getId()) {
                    return p.getPath();
                }
                for (Person nowAcq : p.getAcquaintance().values()) {
                    if (!over.contains(nowAcq.getId())) {
                        MyPerson acq = (MyPerson) nowAcq;
                        if (visit.contains(acq.getId())) {
                            acq.setPath(Math.min(p.getPath() + p.queryValue(acq), acq.getPath()));
                        } else {
                            acq.setPath(p.getPath() + p.queryValue(acq));
                            visit.add(acq.getId());
                        }
                        pq.remove(acq);//注意pq只修改元素但是不add 它不會重新排序!!!! 
                        pq.add(acq); // 加入堆尾 調整堆
                        //也可以直接add 在for之前用over加判斷條件
                    }
                }
      
            }
            return -1;
    }
    
  • 簡便的刪除操作

    deleteColdEmoji

    emojiIdHeat.values().removeIf(heat -> (heat < limit));
    messages.values().removeIf(message -> ((message instanceof EmojiMessage) && (!containsEmojiId(((EmojiMessage)message).getEmojiId()))));
    

    clearNotices

    getPerson(personId).getMessages().removeIf(message -> (message instanceof NoticeMessage));
    

三、效能問題和修復情況

  • query_group_age_var

    如果每次查詢時都計算可能會超時。在group 中 add person、del person 時 維護 ageSum、ageSqrSum,那麼在每次查詢時就無序遍歷。

    ageMean = ageSum / people.size()

    ageVar = (ageSqrSum - 2 * ageMean * ageSum + people.size() * ageMean * ageMean) / people.size()

    需要注意如果直接由數學公式推導,ageSum 可以替換成 people.size() * ageMean,但這樣會因為精度出現bug。

  • query_group_value_sum query_group_people_sum

    與 age 操作類似,但除了在group 中 add person、del person 時只需要一層迴圈遍歷維護 valueSum 外,add relation 時也需判斷是否需要更新 group 的 valueSum。

  • 演算法的效能,採用路徑壓縮並查集和堆優化的Dijkstra。

四、NetWork 的擴充套件

4.1 要求

假設出現了幾種不同的Person

  • Advertiser:持續向外傳送產品廣告
  • Producer:產品生產商,通過Advertiser來銷售產品
  • Customer:消費者,會關注廣告並選擇和自己偏好匹配的產品來購買 -- 所謂購買,就是直接通過Advertiser給相應Producer發一個購買訊息
  • Person:吃瓜群眾,不發廣告,不買東西,不賣東西

如此Network可以支援市場營銷,並能查詢某種商品的銷售額和銷售路徑等 請討論如何對Network擴充套件,給出相關介面方法,並選擇3個核心業務功能的介面方法撰寫JML規格(借鑑所總結的JML規格模式)。

4.2 規定

為了擴充套件NetWork,做出如下規定:

  • 新增三個 Person 的子類為Advertiser、Producer、Customer,新增 Product 類,新增 ProductNotFoundException、EqualProductIdException、NotProducerException 異常類。
  • Product 有 price 屬性,有唯一的 id 屬性。Advertiser 有一個 sales 屬性,表示總銷售額,有 experience 屬性,表示經驗值。
  • 每個 Product 有一個唯一的id,Producer 可以生產 Product,之後可以向 Advertiser 傳送ProductMessage,之後 Advertiser 可以向 Customer 推銷產品,推銷成功後,Advertiser 的經驗值會加1。Customer 收到推銷之後,只有 money 大於 Product 的 price 才會購買該 Product。

為滿足新規定,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 containsProduct(id);
  @ ensures (\exists int i; 0 <= i && i < products.length; products[i].getId() == id &&
  @         \result == products[i]);
  @ also
  @ public normal_behavior
  @ requires !containsProduct(id);
  @ ensures \result == null;
  @*/
public /*@ pure @*/ Product getProduct(int id);

4.3 實現

  • 功能1:生產商品

    /*@ public normal_behavior
      @ requires !(\exists int i; 0 <= i && i < people.length; products[i].equals(product));
      @ assignable products;
      @ assignable ((Producer)getPerson(product.getProducerId())).getProducts();
      @ 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].equals(\old(products[i]))));
      @ ensures (\exists int i; 0 <= i && i < products.length; products[i].equals(product);
      @ ensures ((Producer)getPerson(product.getProducerId)).getProductsNum() = \old(((Producer)getPerson(product.getProducerId())).getProductsNum()) + 1;
      @ ensures (\forall int i; 0 <= i && i < \old(((Producer)getPerson(product.getProducerId())).getProductsNum()); 
      \exists int j; 0 <= j && j < ((Producer)getPerson(product.getProducerId())).getProductsNum(); ((Producer)getPerson(product.getProducerId())).getProducts().get(j).equals(\old(((Producer)getPerson(product.getProducerId())).getProducts().get(i))));
      @ ensures (\exists int i; 0 <= i && i < ((Producer)getPerson(product.getProducerId())).getProductsNum(); ((Producer)getPerson(product.getProducerId())).getProducts().get(i).equals(product));
      @ also
      @ public exceptional_behavior
      @ signals (EqualProductIdException e) containsProduct(product.getId());
      @ signals (PersonIdNotFoundException e) (!containsProduct(product.getId()) && !contains(product.getProducerId()));
      @*/
    public void produce(Product product) throws EqualProductIdException, PersonIdNotFoundException;
    
  • 功能2:向顧客推銷產品

    /*@ public normal_behavior
      @ requires containsProduct(productId) && contains(adId) && contains(customerId);
      @ assignable getPerson(customerId).money, ((Producer)getPerson(getProduct(productId).getProducerId())).sales, ((Advertiser)getPerson(adId)).exp;
      @ ensures (getPerson(customerId).getMoney() >= getProduct(productId).getPrice()) ==> ((getPerson(customerId).getMoney() == \old(getPerson(customerId).getMoney()) - getProduct(productId).getPrice()) && (((Producer)getPerson(getProduct(productId).getProducerId())).getSales() = \old(((Producer)getPerson(getProduct(productId).getProducerId())).getSales()) + getProduct(productId).getPrice()) && (((Advertiser)getPerson(adId)).getExp() == \old(((Advertiser)getPerson(adId)).getExp()) + 1));
      @ also
      @ public exceptional_behavior
      @ signals (ProductNotFoundException e) !containsProduct(productId);
      @ signals (PersonIdNotFoundException e) containsProduct(productId) && (!contains(adId) || !contains(customerId));
      @*/
    public void promote(int productId, int adId, int customerId) throws ProductNotFoundException, PersonIdNotFoundException;
    
  • 功能3:查詢某個 id 的 Producer 的銷售額

    /*@ public normal_behavior
      @ requires contains(id) && (getPerson(id) instanceof Producer);
      @ ensures /result == getPerson(id).getSales();
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id);
      @ signals (NotProducerException e) (contains(id) && (!(getPerson(id) instanceof Producer)));
      @*/
    public /*@ pure @*/ int querySales(int id) throws PersonIdNotFoundException, NotProducerException;       
    

五、心得體會

總體來說,本單元作業沒有前兩個單元難度大。所有方法的具體操作都由JML明確規定,但同時需要我們嚴格遵守規格。jml 作為一種介面規格語言,可以準確定義和表示方法行為的正確性,提供了一種通過邏輯來檢查程式碼的方式。

通過本單元的練習,我學會了jml的方法規格表示和基於規格的測試方法,以及型別層次下的規格分析設計,體會到了jml的優勢。但在閱讀一些比較複雜的jml描述時需要花很長時間理解,比如對qlc、qbs等方法的描述,面對複雜的jml時,可以通過畫圖、轉換自然語言等加快理解。

此外,在作業中鞏固了許多圖論演算法及其優化。