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時,可以通過畫圖、轉換自然語言等加快理解。
此外,在作業中鞏固了許多圖論演算法及其優化。