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

[BUAA OO]第三單元總結

[BUAA OO]第三單元總結

一.寫在前面

在這一單元,我們主要對規格化設計重要的實現者、可以避免二義性的語言JML(Java Modeling Language)進行了一定的瞭解,體會到了一些規格化設計的思想。三次作業以提高對JML的閱讀能力和對於演算法複雜度的把控為主要目標,以社交網路為主要載體,整體難度較之前兩單元稍小,讓我能騰出一口氣去處理越來越多的各科作業,感謝課程組

接下來,我將按照作業的問題順序展開我的部落格:

​ 資料的構造:完備性和邊界性

​ 架構設計:基本圖的實現與優化元素的維護

​ bug分析:效能與正確性

​ Network擴充套件

​ 學習體會

二.資料的構造

在本單元的自測中,我第一次作業自己寫了評測機,後面兩次作業則是借用朋友寫的評測機對拍(感謝zxy(o*))並且輔以一些特殊資料來測試邊界,雖然沒有全部自己寫評測,但是我對於評測資料的生成也進行了一定的思考。我認為對於作業來說,評測資料要保證完備性和邊界性。

完備性就是說要把所有可能的情況都測到,這在現實中很難做到。但是由於在作業中可能的指令、異常有限,所以如果願意肝的話還是可以做到的,這一點非常重要,可以在很大程度上保證你的正確性(尤其是在本單元)。因為人往往會不自覺地聯想、按照經驗辦事,在自己構造資料時就會去構造自己認為“難”的資料,而忽略了一些“顯然”的資料,但是往往自己認為顯然的事情,電腦不認為是顯然的,這就會導致漏情況,最終會因為沒考慮一些簡單的因素而出錯。比如對於JML閱讀的不認真或者程式碼的一個疏漏就可能會報錯異常、少加括號等問題的出現。所以在構造評測機的時候一定要把所有的指令、異常都測一遍。

在保證了完備性的基礎上,我們還需要保證資料的邊界性,就是需要構造資料測到資料的邊界。這個邊界有兩重含義,一重是限制的邊界,一重是效能的邊界。限制的邊界需要我們仔細閱讀指導書和JML規格,將其中的類似於1111、各種變數可能疏漏的、反直覺的定義都記下來,一方面閱讀自己的程式碼查驗,一方面構造資料時要針對性地構造相應的資料,比如1111就要針對地插入1112個。效能的邊界則是要求我們根據指導書給出的時間限制和閱讀JML找到的高複雜度方法來構造資料來試著讓自己的程式“超時”,進而檢驗我們對於方法的實現是否滿足了作業對於時間複雜度的要求。構造方法則是儘可能讓每次呼叫該方法都進行儘可能多的迴圈,針對性地構造一些鏈圖、稠密圖、完全圖等等。

本單元hack到三處bug,一處是完備性沒有做好導致的異常錯判,兩處是邊界性沒有做好導致的qgvs時間複雜度過高。

三.架構設計

不難發現,本單元是一個類似於社交網路的圖,Network是圖,Person是結點,relation是邊,按照JML規定的方法寫就可以構建出一個這樣的圖,區別僅在於容器的選擇,為了快速查詢,我基本上都使用了HashMap資料結構,只是由於massage強調放入的順序,所以我使用了List。

不過,在基礎的社交網路圖之上,為了實現演算法的優化,我新增了一些變數,比如為了實現最小生成樹,維護排序邊集private ArrayList<ArrayList<Integer>> sortedLine = new ArrayList<>();,主要是在ar方法中新增了對新邊的插入方法insertNewLine

public void insertNewLine(int id1,int id2,int value) {
        ArrayList<Integer> newLine = new ArrayList<>();
        ......
        newLine.add(id2);
        newLine.add(id1);
        newLine.add(value);
        if (sortedLine.size() == 0) {
            sortedLine.add(newLine);
        } else {
            int low = 0;
            int high = sortedLine.size() - 1;
            if (value <= getLineValue(0)) {
                sortedLine.add(0,newLine);
            } else if (value >= getLineValue(high)) {
                sortedLine.add(newLine);
            } else {
                //小心死迴圈 和 二分的條件
                while (true) {
                    //二分查詢
                }
                sortedLine.add(high,newLine);
            }
        }
    }

為了記錄上次計算出的ageMeanageVar,我採用了四個變數ageMean、isAgeMean、ageVar和isAgeVar來記錄值和是否可用,(其實通過查詢熟人來優化會很快,不過我是先用了訊號量,就懶得改了)。

當然還有為了方便查詢統一連通分量的並查集jointSearchSet,在ap時要將該人加入維護並查集。

在為了優化而新增變數時,一定要記得維護變數的正確性,要理清這個變數和圖中元素的關係,從而找到該變數應該改變的操作並在其中正確地修改變數。

三.bug分析

本次作業由於在實現時已經考慮到了效能的問題並採用了助教推薦/討論區推薦的演算法,所以在效能上並沒有出現問題,正確性也由對拍進行了保證,所以在最後的評測中沒有出現問題,不過在對拍的過程中確實有兩個bug改了很久,讓我記憶猶新。這個bug出現在ageMean的計算中,是因為讀指導書時不認真,將先加再除寫成了先除再加,導致整數的保留出現問題。由於只有在數量較大的時候才會觸發該bug,所以在測試資料達到幾千條的時候才會出現該bug,debug就十分困難,不過還好該bug觸發時都有ageMean,所以檢視一會之後確認了位置就解決了。第二個bug出現在emojiMessage處,是我沒有維護好新增的變數,在刪除emojiMessage時忘了刪除對應的我新增的HashMap<Integer,Integer> messageEmojiId中的元素,結果導致與emojiMessage有關的指令會出錯,但是指令中沒有直接測試該容器的,所以在繞了很多彎之後才發現問題居然出現在這裡。這個bug可是de了足足兩個小時,原因就是因為在sendMessage裡沒有維護messageEmojiId枯了

發現的他人bug共三個,一個是異常處理錯誤,一個是qgvs時間超了,可見自測資料的全面性和邊界性十分重要。

下面我將具體介紹一下這三次作業為了改善效能所寫的三個主要演算法:並查集,最小生成樹,最短路徑Dijkstra。

public class JointSearchSet {

    private HashMap<Integer,Integer> parent = new HashMap<>();
    private HashMap<Integer,Integer> rootHeight = new HashMap<>();

    public JointSearchSet() {}

    public Integer numOfRoot() {
        return rootHeight.size();
    }

    public void addNode(Integer id) {
        parent.put(id,id);
        rootHeight.put(id,1);
    }

    public Integer find(Integer initialNode) {
        int node = initialNode;
        if (parent.get(node) == null) {
            addNode(node);
        }
        while (node != parent.get(node)) {
            ...
        }
        return node;
    }

    public void unionNode(int initialNode1,int initialNode2) {
        int root1 = find(initialNode1);
        int root2 = find(initialNode2);
        if (root1 == root2) {
            return;
        }
        if (rootHeight.get(root1) < rootHeight.get(root2)) {
            ...
        }
    }

    public boolean isConnected(int node1,int node2) {
        return Objects.equals(find(node1), find(node2));
    }

}

並查集是為了記錄圖中連通分量而存在的,在加入點(Person)時要將其也加入並查集,並查集會進行運算後更新自己,在使用時比較根節點即可。在並查集的實現過程中,我使用了遞迴下降和啟發式合併。簡單而言,前者是在搜尋時將樹變低,後者是在加入時選擇較低的樹構建。

 public static int kruskal(Integer createdLineNum,ArrayList<ArrayList<Integer>> sortedLineSet) {
        JointSearchSet kruskalJointSet = new JointSearchSet();
        int valueSum = 0;
        int countLines = 0;
        for (int i = 0;i < sortedLineSet.size();i++) {
            if (countLines == createdLineNum) {
                break;
            }
            int point1 = sortedLineSet.get(i).get(0);
            int point2 = sortedLineSet.get(i).get(1);
            if (!kruskalJointSet.isConnected(point1,point2)) {
                valueSum = valueSum + sortedLineSet.get(i).get(2);
                kruskalJointSet.unionNode(point1,point2);
                countLines++;
            }
        }
        return valueSum;
    }

最小生成樹的演算法我使用了kruskal,由於我自己維護了排序邊集,所以計算時十分方便。

public static int dijkstra(int startId, int finalId, HashMap<Integer, Person> people,
                               JointSearchSet jointSearchSet) {
        HashMap<Integer,Integer> marked = new HashMap<>();
        HashMap<Integer,Integer> distance = new HashMap<>();
        for (Integer personId : people.keySet()) {
            if (jointSearchSet.isConnected(personId, startId)) {
                marked.put(personId, 0);
                distance.put(personId, -1);
            }
        }
        return dijkstraExe(startId, finalId, distance, marked, people);
    }

    public static int dijkstraExe(int startId,int finalId,HashMap<Integer,Integer> distance,
                                  HashMap<Integer,Integer> marked,
                                  HashMap<Integer, Person> people) {
        PriorityQueue<Node> priQueue = new PriorityQueue<>();
        priQueue.offer(new Node(startId, 0));
        distance.put(startId, 0);
        while (priQueue.size() != 0) {
            Node minNode;
            do {
                minNode = priQueue.poll();
                if (minNode.getPersonId() == finalId) {
                    return minNode.getCost();
                }
            } while (marked.get(minNode.getPersonId()) == 1);
            marked.put(minNode.getPersonId(), 1);
            HashMap<Integer, Integer> linkedEdges =
                    ((MyPerson)people.get(minNode.getPersonId())).getAcquaintances();
            for (Integer adjacent : linkedEdges.keySet()) {
                ...
        }
        return distance.get(finalId);
    }

最短路徑我採用了Dijkstra,並且使用了堆優化,因為本題中的資料限制,所以會有相當一部分資料為稀疏圖,堆優化的效果還是相當明顯的。雖然聽說PriorityQueue<Node>會比自己實現的小根堆慢,但是已經很快了,我也就懶得改了

四.NetWork擴充套件

假設出現了幾種不同的Person

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

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

從題目的要求來看,我認為應該新增六個介面和相應的異常類PurchaseDismatchException,AdvertiseDismatchException,ProductIdNotFoundException,Advertiser、Producer、Customer繼承Person,advertiseMessage、purchaseMessage繼承Message,Product不繼承已有介面。

Product中記錄產品的資訊:id、所需金錢數目等

advertiseMessage是Advertiser給Customer傳送的訊息型別,用來告知對方可購買的產品

purchaseMessageAdvertiser傳送給Producer的訊息型別,用來告知對方Customer要購買的產品

Advertiser需要新增一個廣告產品列表變數,記錄廣告的產品;新增一個addAdvertiseProduct方法,更新廣告產品列表;

Producer需要新增一個可生產產品列表,記錄可生產的產品;新增setProduct方法,修改可生產的產品;

Customer需要新增一個偏好列表,記錄自己的偏好;新增待處理廣告HashMap ,記錄收到的廣告及其廣告商,以待Network呼叫exeAdvertise方法時處理;新增setPriority方法,修改自己的偏好;新增judgeProduct方法,判斷自己是否要購買Advertiser的廣告內容;

Network需要讓自己的sendMessagesendIndirectMessage能夠傳送新增的訊息,能夠新增產品,能夠修改Advertiser、Producer、Customer中的產品列表,能夠查詢某Product的銷售額、銷售路徑等等。

此外,還可以根據需要增添一些設定,比如一個產品只有一個生產商可以生產等方便實現,也可以不增添,不過那樣就需要別的規則進一步規範。

JML規格:

對新增訊息的傳送

/*@ public normal_behavior
      @ ensures (\old(getMessage(id)) instanceof AdvertiseMessage) ==>
      @          (\exists int i; 0 <= i && i <= \old(getMessage(id)).getPerson2.exeAdvertiser.length - 1; \old(getMessage(id)).getPerson2.exeAdvertiser[i].equals(\old(getMessage(id)).getPerson1)
      @ ensures (\old(getMessage(id)) instanceof AdvertiseMessage) ==>
      @ (\forall int i; 0 <= i && i < \old(getMessage(id)).getPerson2.\old(exeAdvertiser).length;
      @         (\exists int j; 0 <= j && j < \old(getMessage(id)).getPerson2.exeAdvertiser.length;\old(getMessage(id)).getPerson2.exeAdvertiser[j].equals(\old(getMessage(id)).getPerson2.\old(exeAdvertiser)[i])));
      @ ensures (\old(getMessage(id)) instanceof AdvertiseMessage) ==>
      @          (\exists int i; 0 <= i && i <= \old(getMessage(id)).getPerson2.exeAdvertiser.length - 1 && \old(getMessage(id)).getPerson2.exeAdvertiser[i].equals(\old(getMessage(id)).getPerson1);(\forall Product j;\old(getMessage(id)).getPerson1.advertiseProducts.contains(i);\old(getMessage(id)).getPerson2.exeProducts[i].contains(j)))
      @ ensures (\forall int i;0 <= i && i < \old(getMessage(id)).getPerson2.exeAdvertiser.length;(\exists j;0 <= j && j < \old(getMessage(id)).getPerson2.\old(exeAdvertiser).length && \old(getMessage(id)).getPerson2.\old(exeAdvertiser)[j].equals(\old(getMessage(id)).getPerson2.exeAdvertiser[i])) ==> (\old(getMessage(id)).getPerson2.exeAdvertiser[i].equals(\old(getMessage(id)).getPerson1) ==> \old(getMessage(id)).getPerson2.exeProducts[i].equals(\old(getMessage(id)).getPerson1.advertiseProducts.addAll(\old(getMessage(id)).getPerson2.\old(exeProducts)[j])) && !(\old(getMessage(id)).getPerson2.exeAdvertiser[i].equals(\old(getMessage(id)).getPerson1) ==> \old(getMessage(id)).getPerson2.exeProducts[i].equals(\old(getMessage(id)).getPerson2.\old(exeProducts)[j])))
      @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
      @ (exists int i;0 <= i && i < producers.length;produces[i].equals(\old(getMessage(id)).getPerson2()))
      @ \old(getMessage(id)).getPerson2().getProfis() =  \old(getMessage(id)).getPerson2().(\old(getProfits())) + ((PurchaseMessage)\old(getMessage(id))).getAllProductProfits()
      @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
      @ successSellMessages.contains(\old(getMessage(id)))
      @ (\forall Message i;(\old)successSellMessages.contains(i);successSellMessages.contains(i))
      @ also
      @ public exceptional_behavior
      @ signals (AdvertiseDismatchException e) containsMessage(id) && (getMessage(id).getType == 1 || (getMessage(id).getType == 0 && (!getMessage(id).getPerson1 instanceof Advertiser || !getMessage(id).getPerson2 instaceof Customer)
      @ signals (PurchaseDisMatchException e) containsMessage(id) && (getMessage(id).getType == 1 || (getMessage(id).getType == 0 && (!getMessage(id).getPerson1 instanceof Advertiser || !getMessage(id).getPerson2 instaceof Producer)
      @*/
public void sendMessage(int id) throws
            RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException,AdvertiseDismatchException,PurchaseDisMatchException;

設定某Person的Priority

/*@ public normal_behavior
  @ requires contains(id1)
  @ ensures type == 1 ==>
  @ getPerson(id1).getPriority().contains(product)
  @ (\forall Product i;getPerson(id1).(\old)getPriority().contains(i); getPerson(id1).getPriority().contains(i))
  @ ensures type == 0 ==>
  @ !getPerson(id1).getPriority().contains(product)
  @ (\forall Product i;getPerson(id1).getPriority().contains(i); getPerson(id1).(\old)getPriority().contains(i))
  @ ensures type != 0 && type != 1 ==> 
  @ assignable \nothing;
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !contains(id1);
  @*/
public void setPersonPriority(int id1, int type, Product product) throws
    PersonIdNotFoundException;

查詢某產品的銷售額

/*@ public normal_behavior
  @ requires containsProduct(id)
  @ ensures result == (\sum Message i;successSellMessages.contains(i) && ((PurchaseMessage) i).getPerchaseProducts().contains(id);getProduct(id).getProfits())
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !containsProduct(id);
  @*/
public int queryProductProfis(int id) throws 
    ProductIdNotFoundException;

五.學習體會

經過本單元的學習,可以明顯感受到我對於JML的閱讀能力有了一個較為顯著的提升,不過我對於JML這一語言還是有許多疑惑,比如就我個人感覺而言,JML基本上不會讓事情變得更簡單,而是讓事情變得更復雜,比較經典的例子就是最小生成樹演算法的JML,就是把很容易懂的一個概念翻譯的晦澀難懂,不過我也知道由於我的水平比較低、眼界不開闊,可能看不到JML的優勢,所以JML到底是優是劣,還要留待以後檢驗了。

此外,在本單元的學習中,我初步接觸了契約式程式設計和防禦式程式設計。簡單來說,前者與JML類似,先驗條件滿足==>後驗條件滿足,程式設計師只需要考慮先驗條件,其他可能情況一概不管;後者則是要綜合考慮所有可能的輸入,儘可能保證無論在什麼條件下程式都不會崩潰。我認為這兩者更多的是一個概念的提煉,在實際的程式設計中應該結合自己程式的實現要求、實現目的綜合考量、綜合使用。

總而言之,本單元是比較輕鬆的一個單元,一方面是經過了半個學期的訓練,面向物件的思想和java的程式設計能力都有了一定的提升;另一方面,本單元的內容確實難度稍低。接下來就是OO的最後一個單元了,並且也是本學期的最後一個月,希望我能在疫情留校期間保持狀態,努力學習,給大二畫上一個不後悔的句號。