OO第三单元-JML

利用JML规格来准备测试数据

我对jml规格的理解,更像是:功能的外化

比如,对于addGroup()指令:

  • 其功能简单来说就是:将group添加到Network管理的groups
  • jml的描述简单来说就是:groups[i]的变化和限制要满足哪些条件

在本单元中,我在自测的时候,主要还是以:

  • 通过阅读每个方法的jml规格来理解任务实现的功能
  • 针对功能进行测试
  • 在针对功能进行测试的时候,每一条jml里的语句所反映的限制/情况/含义都应该考虑到。(在jml规格中,每一条ensures、每一条normal_behavior或者exceptional_behavior都是在给我们明确这个方法的功能。)

图模型构建和维护策略

第一次任务

对于qci id1 id2指令,其意义是查询id1id2是否能通过人与人之间的relation联系到一起。如果通过bfs/dfs进行遍历的话,其复杂度高达O(V^2)。

针对此,我使用了并查集的结构+启发式合并的优化。复杂度降为O(log(V))。

// 并查集相关
// 存放着所有根节点和其高度的对应关系,其size()就是blockSum
private final HashMap<Integer, Integer> rootId2height = new HashMap<>();
// 每个id与其父节点的映射关系map
private final HashMap<Integer, Integer> id2Fid = new HashMap<>();

如此一来,在ar的时候,就需要:

// 如果两个人不在一棵树中,则将这两个人所在树合并成一棵
int f1id = getFatherId(p1.getId());
int f2id = getFatherId(p2.getId());
if (f1id != f2id) {
    int h1 = rootId2height.get(f1id);
    int h2 = rootId2height.get(f2id);
    if (h1 < h2) {
        // 如果id1所在树比较矮,那就把它合并到id2的树里面
        id2Fid.put(f1id, f2id);
        rootId2height.remove(f1id);
    } else if (h1 > h2) {
        // 如果id2所在树比较矮,那就把它合并到id1的树里面
        id2Fid.put(f2id, f1id);
        rootId2height.remove(f2id);
    } else {
        // 如果两棵树一样高,那就将id1所在树合并到id2所在树里面,并将id2所在树的height+1
        id2Fid.put(f1id, f2id);
        rootId2height.remove(f1id);
        rootId2height.put(f2id, rootId2height.get(f2id) + 1);
    }
}

其中,getFatherId()为:

private int getFatherId(int id) {
    int tid = id;
    int fid = id2Fid.get(tid);
    while (fid != tid) {
        tid = fid;
        fid = id2Fid.get(tid);
    }
    return fid;
}

第二次任务

qlc id中,需要查询id所在的block的最小生成树。

public int queryLeastConnection(int id) throws PersonIdNotFoundException {
    MyPerson p = people.getOrDefault(id, null);
    if (p == null) {
        throw new MyPersonIdNotFoundException(id);
    }
    TreeSet<Edge> edges = new TreeSet<>();
    int mstEdgeNum = 0;
    for (MyPerson p2 : people.values()) {
        if (isCircle(id, p2.getId()) &&
            id != p2.getId()) { // 表示不需要向edges中加入 自己和自己linked对应的边
            p2.getAc().forEach(idt -> {
                edges.add(new Edge(p2, people.get(idt)));
            });
            ++mstEdgeNum;
        }
    }
    int ret = 0;

    // 此时,edges里面已经得到了id所在的连通图的所有边(按照value的顺序)
    // arr:arrive,存放已经抵达的点。
    // HashSet<Integer> arr = new HashSet<>();
    // edgesSel: edges Selected,存放已经选中作为mst的边
    // HashSet<Edge> edgesSel = new HashSet<>();
    int nowEdgeNum = 0; // 当nowEdgeNum为mstEdgeNum时,MST就生成好了!
    MyNetwork network = new MyNetwork();

    for (Edge edge : edges) {
        if (nowEdgeNum == mstEdgeNum) {
            break;
        }
        MyPerson p1 = edge.getPmin().clone();
        MyPerson p2 = edge.getPmax().clone();
        if (!network.contains(p1.getId())) {
            try {
                network.addPerson(p1);
            } catch (EqualPersonIdException e) {
                e.printStackTrace();
            }
        }
        if (!network.contains(p2.getId())) {
            try {
                network.addPerson(p2);
            } catch (EqualPersonIdException e) {
                e.printStackTrace();
            }
        }
        // !edgesSel.contains(edge)
        if (!network.isCircle(p1.getId(), p2.getId())) {
            // 说明这条边被选中了!
            // edgesSel.add(edge);
            try {
                network.addRelation(p1.getId(), p2.getId(), edge.queryValue());
            } catch (EqualRelationException e) {
                e.printStackTrace();
            }
            ret += edge.queryValue();
            // System.err.println("now ret: " + ret);
            ++nowEdgeNum;
        }
    }
    return ret;
}

第三次任务

sim id中,要寻找id这条消息的发送方到接收方的最短路径。我采用的是dijkstra算法+堆优化,复杂度为O((e + n)*log(n))。

public int sendIndirectMessage(int id) throws MessageIdNotFoundException {
    if (!containsMessage(id) || getMessage(id).getType() == 1) {
        throw new MyMessageIdNotFoundException(id);
    }
    MyPerson sender = (MyPerson) getMessage(id).getPerson1();
    MyPerson recver = (MyPerson) getMessage(id).getPerson2();
    try {
        if (!isCircle(sender.getId(), recver.getId())) { // O(log(e))
            return -1;
        }
    } catch (PersonIdNotFoundException e) {
        e.printStackTrace();
    }
    TreeSet<Node> nodesNotSel = new TreeSet<>(); // 装着所有未被选中的Node
    HashMap<Integer, Node> m = new HashMap<>(); // 存放该Network里面所有person的ID和Node的映射关系
    people.keySet().forEach(i -> {
        Node node = new Node(i,Integer.MAX_VALUE);
        nodesNotSel.add(node);
        m.put(i, node);
    }); // O(n*log(n))
    nodesNotSel.remove(new Node(sender.getId(),Integer.MAX_VALUE));
    Node start = new Node(sender.getId(),0);
    nodesNotSel.add(start);
    m.put(sender.getId(), start);

    int ret = 0;
    while (true) { // O(e*log(n))
        Node n = nodesNotSel.pollFirst();
        if (recver.getId() == n.getId()) {
            // 说明此时已经找到从sender到recver的最短路径了!
            while (n.getId() != sender.getId()) { // 循环次数:O(n)
                try {
                    ret += queryValue(n.getId(), n.getPreId());
                } catch (PersonIdNotFoundException | RelationNotFoundException e) {
                    e.printStackTrace();
                }
                n = m.get(n.getPreId());
            }
            MyMessage msg = messages.remove(id);
            ((MyPerson) (msg.getPerson2())).recMsg(msg);
            if (msg instanceof EmojiMessage) {
                int eid = ((EmojiMessage) msg).getEmojiId();
                emoji2heats.replace(eid, emoji2heats.get(eid) + 1);
            }
            return ret;
        }
        Node finalN = n;
        people.get(n.getId()).getAc().forEach(id2 -> { //
            try {
                Node n2 = m.get(id2);
                if (n2.getDis() > finalN.getDis() + queryValue(finalN.getId(), id2)) {
                    nodesNotSel.remove(n2); // 先取出来
                    n2.setDis(finalN.getDis() + queryValue(finalN.getId(), id2));
                    n2.setPreId(finalN.getId());
                    nodesNotSel.add(n2); // 修改后再放进去
                }
            } catch (PersonIdNotFoundException | RelationNotFoundException e) {
                e.printStackTrace();
            }
        });
    }
}

代码实现出现的性能问题和修复情况

第一次任务

互测bug

互测中,有两个同学的qbs复杂度为O(N^2).构造样例:

ap 1 p1 100
...
ap 600 p600 100
qbs
...
qbs

(加入600个人,再进行400次qbs)

这两位同学即会超时。

第二次任务

根据这次强测互测的要求说明,我估算出,stdin每次给出的指令引起的方法调用的时间复杂度要低于O(n^2)。

所以,在写完各个方法的代码后,我又对所有stdin指令进行了时间复杂度分析,如下所示。

n为节点数+边数(person数+relation数),n<=10000
m为group数,m<=20
"ap", "addPerson"               , O(n) // 涉及到由所有邻居增加对应的valueSum
"ar", "addRelation"             , O(logn+m) // 涉及到查找并查集父节点(O(logn)),以及共同组更新valueSum(O(m))
"qv", "queryValue"              , O(1)
"qps", "queryPeopleSum"         , O(1)
"qci", "queryCircle"            , O(logn) // 涉及到查找并查集父节点
"qbs", "queryBlockSum"          , O(1)
"ag", "addGroup"                , O(1)
"atg", "addToGroup"             , O(n) // 需要根据person更新ageSum,及其根据其所有邻居来更新valueSum
"dfg", "delFromGroup"           , O(n) // 需要根据person更新ageSum,及其根据其所有邻居来更新valueSum
"qgps", "queryGroupPeopleSum"   , O(1)
"qgvs", "queryGroupValueSum"    , O(1)
"qgav", "queryGroupAgeVar"      , O(n) // 方差要现算
"am", "addMessage"              , O(1)
"sm", "sendMessage"             , O(n) // 对于群聊消息,群内每一个人的socialValue要增加
"qsv", "querySocialValue"       , O(1)
"qrm", "queryReceivedMessages"  , O(1)
"qlc", "queryLeastConnection"   , O(nlogn) // 先选出person所在block的所有边(这里又会涉及到qci),再将这些边按照Kruskal算法生成mst

此外,有几个值得提到的点:

  • 执行添加指令的时间复杂度为O(1)时,执行查询指令的复杂度就为O(n^2)。此时,为了避免大量的查询指令导致TLE,需要在执行添加指令的时候进行一些处理(例如通过一些HashMap构建联系),这样在执行相关的查询指令时,时间复杂度就可以降到O(n)。

自测bug

1.

// stdin:
ap 1 p1 1
ap 2 p2 2
am 1 10 0 1 2
sm 1
sm 1
// acout:
Ok
Ok
Ok
rnf-1, 1-1, 2-1
rnf-2, 1-2, 2-2

// myout:
Ok
Ok
Ok
rnf-1, 1-1, 2-1
minf-1, 1-1

原因:

sendMessage()命令中,规格的描述是:

  • 对于normal behavior,要将这个message移出messages
  • 对于exceptional behavior,则无需移除

我则是:现将message移出messages,再进行判断。此时,若为exceptional behavior,我就直接throw new Exception了,却忘记,此时这个message不应该被取出messages。这样就会导致,则在下次sm时,会出问题

互测bug

bug1

// stdin
ag 1
ap 1 p1 1
atg 1 1
ap 2 p2 1
atg 2 1
ap 3 p3 1
atg 3 1
ap 4 p4 1
atg 4 1
...
ap 1111 p1111 1
atg 1111 1
qgvs 1
qgvs 1
...
qgvs 1

该hack样例为:向1个group里面加很多个人,再对该group进行很多次qgvs询问。

这为同学在getValueSum()方法中选择了时间复杂度O(n^2)的算法(如下),故会tle。

public int getValueSum() {
    int result = 0; //只有不同人之间的value
    for (Person p1 :peopleMap.values()) {
        for (Person p2:peopleMap.values()) {
            if (p1.isLinked(p2)) {
                result += p1.queryValue(p2);
            }
        }
    }
    return result;
}

bug2

// stdin
ag 1
ap 1 p1 1
atg 1 1
ap 2 p2 1
atg 2 1
ap 3 p3 1
atg 3 1
ap 4 p4 1
atg 4 1
...
ap 1111 p1111 1
atg 1111 1
qgav 1
qgav 1
...
qgav 1

该hack样例为:向1个group里面加很多个人,再对该group进行很多次qgav询问。

这为同学在getAgeVar()中的for循环体内调用了时间复杂度为O(n)的getAgeMean()方法,于是getAgeVar()的时间复杂度为O(n^2),故会tle。

public int getAgeMean() {
    if (people.size() == 0) {
        return 0;
    } else {
        int sum = 0;
        for (int i = 0; i < people.size(); i++) {
            sum += people.get(i).getAge();
        }
        sum = sum / people.size();
        return sum;
    }
}

public int getAgeVar() {
    if (people.size() == 0) {
        return 0;
    } else {
        int sum = 0;
        for (int i = 0; i < people.size(); i++) {
            sum += (people.get(i).getAge() - getAgeMean()) *
                (people.get(i).getAge() - getAgeMean());
        }
        sum = sum / people.size();
        return sum;
    }
}

第三次任务

e:边数
n:人数
g:组数
m:消息数
em:emoji种类

| add_person               | ap   |     O(1)
| add_relation             | ar   |     O(g + log(e)) ;p1和p2共同在的group的valueSum要更新;如果两个人不在一棵树中,则将这两个人所在树合并成一棵
| query_value              | qv   |     O(1)
| query_people_sum         | qps  |     O(1)
| query_circle             | qci  |     O(log(e)) ;涉及到查询并查集父节点是否为同一个
| query_block_sum          | qbs  |     O(1)
| add_group                | ag   |     O(1)
| add_to_group             | atg  |     O(e);涉及到由所有邻居增加对应的valueSum
| del_from_group           | dfg  |     O(e);涉及到由所有邻居减去对应的valueSum
| query_group_people_sum   | qgps |     O(1)
| query_group_value_sum    | qgvs |     O(1)
| query_group_age_var      | qgav |     O(n);方差需要现算
| add_message              | am   |     O(1)
| send_message             | sm   |     O(n);对于群聊消息,要遍历群内的每个人(addSocialValue,以及判断是否为发红包)
| query_social_value       | qsv  |     O(1)
| query_received_messages  | qrm  |     O(1)
| query_least_connection   | qlc  |     O(e*log(e));最小生成树
| add_red_envelope_message | arem |     O(1)
| add_notice_message       | anm  |     O(1)
| clean_notices            | cn   |     O(m); 需要遍历该人的messages
| add_emoji_message        | aem  |     O(1)
| store_emoji_id           | sei  |     O(1)
| query_popularity         | qp   |     O(1)
| delete_cold_emoji        | dce  |     O(em + m); 需要遍历messages和emoji2heats
| query_money              | qm   |     O(1)
| send_indirect_message    | sim  |     O((e + n)*log(n)); dijkstra最短路径算法

互测bug

public void addMessage(Message message) throws
    EqualMessageIdException, EmojiIdNotFoundException, EqualPersonIdException {
    //a分支
    if (containsMessage(message.getId())) {
        throw new MyEqualMessageIdException(message.getId());
    }
    //b分支
    else if (message instanceof EmojiMessage) {
        int emojiId = ((EmojiMessage)message).getEmojiId();
        if (!containsEmojiId(emojiId)) {
            throw new MyEmojiIdNotFoundException(emojiId);
        }
    }
    // c分支
    else if (message.getType() == 0 && message.getPerson1() == message.getPerson2()) {
        throw new MyEqualPersonIdException(message.getPerson1().getId());
    }
    messages.put(message.getId(), message);
}

如果进入a,b分支后并未抛出异常,而此时应该进入c分支。但是,上述代码在进入b分支后,就不会进入c分支了。从而引发bug。

修复办法:将上面代码中的c分支的else if改为if

针对ppt内容对Network进行扩展

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告
  • Producer:产品生产商,通过Advertiser来销售产品
  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等

请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)


添加新的指令

add_producer id(int) name(String) age(int) price(int)
add_advertiser id(int) name(String) age(int)
add_customer id(int) name(String) age(int)

add_ad_message id(int) producer_id(int) type(int) advertiser_id(int) customer_id(int)|group_id(int)
add_buy_message id(int) customer_id(int) producer_id(int)

query_sales_quantity producer_id(int)
query_sales_distribution producer_id(int)

新建三个接口

// Producer.java
public interface Producer extends Person {
    // public instance model price;
    // public instance model non_null Customer[] customers;
    // public instance model non_null int[] quantity;
    
    //@ ensures \result == price;
    public /*@ pure @*/ int getPrice();
}

// Advertiser.java
public interface Advertiser extends Person {

}

// Customer.java
public interface Customer extends Person {
    // public instance model non_null Producer[] producers;
    // public instance model non_null int[] quantity;
}

一些关键方法的jml更新(下图只包含更新的部分)

/*	  @ public normal_behavior
	  @ requires (contains(id) && getPerson(id) instanceof Producer);
	  @ ensures \result ==  (\sum int i; 0 <= i && i < ((Producer) getPerson(id)).quantity.length; quantity[i]);
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id) || !(getPerson(id) instanceof Producer);
@*/
public /*@ pure @*/ int querySalesQuantity(int id) throws PersonIdNotFoundException;


/*	  @ public normal_behavior
	  @ requires (contains(id) && getPerson(id) instanceof Producer);
	  @ ensures \result.size() == ((Producer) getPerson(id)).customers.length;
	  @ ensures (\forall int i; 0 <= i && i < \result.size(); 
	  \result.containsKey(((Producer) getPerson(id)).customers[i].getId()) &&
      \result.get(((Producer) getPerson(id)).customers[i].getId()) == ((Producer) getPerson(id)).quantity[i] 
	  )
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id) || !(getPerson(id) instanceof Producer);
@*/
public /*@ pure @*/ Map<Integer, Integer> querySalesDistribution(int id) throws PersonIdNotFoundException;


/*      @ requires  (getMessage(id) instanceof BuyMessage ==> 
        @           (getMessage(id).getPerson1() instanceof Customer && getMessage(id).getType() == 0 ==> getMessage(id).getPerson2() instanceof Producer));
        @ signals (PersonIdNotFoundException e) (getMessage(id) instanceof BuyMessage ==> 
        @           (!(getMessage(id).getPerson1() instanceof Customer) || (getMessage(id).getType() == 0 ==> !(getMessage(id).getPerson2() instanceof Producer))
        @              ));
@*/
public void addMessage(Message message) throws EqualMessageIdException, EmojiIdNotFoundException, EqualPersonIdException;


/*  @ public normal_behavior
    @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
    @          getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
    @          getMessage(id).getPerson1() != getMessage(id).getPerson2();
    @ ensures (\old(getMessage(id)) instanceof BuyMessage) ==> 
    @         (\old(getMessage(id)).getPerson1().getMoney() ==
    @           \old(getMessage(id).getPerson1().getMoney()) - ((BuyMessage)\old(getMessage(id))).getProducer().getPrice() &&
    @           \old(getMessage(id)).getPerson2().getMoney() ==
    @           \old(getMessage(id).getPerson2().getMoney()) + ((BuyMessage)\old(getMessage(id))).getProducer().getPrice());
	@*/
    public void sendMessage(int id) throws
            RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;

单元学习体会

在完成作业的过程中,每次阅读jml时,我都感觉这些jml写得长而啰嗦。仔细读完一大段jml代码之后,会发现原来这个函数的功能其实就是一项很简单的操作。

对于涉及较复杂算法的某些方法(比如task1中的isCircle(),task2中的queryLeastConnection(),task3中的sendIndirectMessage()),其jml更加冗长。我感受到了使用jml来描述方法功能,对一个方法的描述会非常具体、清楚、不产生歧义,但是相比用自然语言描述,给读者/程序员的理解难度增大了很多。

就像课上提到的,往往写jml要比写方法体本身更加复杂。本单元和前两个单元不同也体现于此。

  • 前两个单元都是告诉你整个程序要实现的功能,让你自己去建立类、建立方法。难点在架构的建立。(前两个单元的讨论区里活跃着同学对于架构建立的各种思索和探讨)
  • 本单元则是告诉你每一个方法的jml,让你通过阅读jml理解其功能,填充已经有的方法体即可。难点在对于jml的准确理解。(本单元的讨论区冷清了很多...因为正确的理解只有一种,似乎没有什么可交流讨论的)
posted @ 2022-06-04 16:25  wlc000  阅读(54)  评论(1编辑  收藏  举报