BUAA-OO-Unit3 Summary

第三单元总结--基于JML规格的社交系统

初识JML语言

 JML (Java Modeling Language),Java 建模语言,个人感觉,它的出现可以用类似数学语言的形式,明确所需的要求(即规格),因此JML的重要作用就在于消除自然语言的二义性。它利用前置条件,后值条件、副作用范围限定来对一个方法进行描述和限制,利用不变式和状态变化约束对一个类进行了规格化描述,使得开发者和用户能够进行一定层次上的沟通。
 而在本次训练中,我们除了可以根据JML来编写合乎规格要求的代码外,还可以利用JML来构建测试数据。我们可以根据JML所指出normal_behaviorexceptional_behavior来分情况构造可以激发其行为的数据,保证程序的鲁棒性。此外对于requires P语句时,构造的数据也应该满足,就需要我们写评测机的时候对于这一方面要进行一个检查。 By the way, 本单元中课下构建评测机来生成数据并找人对拍是非常有效的测试方法,主要是由于中测的数据较弱,测试的覆盖率不全。

架构设计和性能优化

基于并查集的qci与qbs

 这两个指令,分别是查询两个点是否联通和整张图的连通块个数,如果按照暴力的方法,对两个指令使用DFS或者BFS的话,最差的情况qci的时间复杂度会达到\(O(n^2)\), 而qbs的时间复杂度会达到\(O(n^3)\),如果qbvs和qci的指令占比高的话,确实有tle的可能,事实上对于某些DFS或者BFS没有注意砍掉一些搜索分枝的情况下,在互测中确实可以构造出这样的数据使得程序运行超时。于是,我们可以处理并查集的方法进行处理。
 并查集,顾名思义,我们将所有关系的点都弄到一个集合里,当两个点要建立联系时,将两个点所处在的集合合并为一个处理。(孤立点的集合可以视为只包含自己的点)。在这次作业中,我采用的是递归找并查集并合并的策略。
 具体为,对于每个集合用树形结构进行存储,对于每个点不断向上找其祖先,直到其不存在祖先,对比两个祖先是否相等,若相等则说明这两个点是连通的,不相等说明不连通。合并时,找到祖先并将其中一个树变成另一个树的子树。而这个并查集可以在addRelation()的时候进行维护。两点已经在一个集合里面就不管了,不再则递归找到其祖先,并进行合并。我所采用的合并策略为将集合中点数少的点连向点数较大的点,这样可以对并查集的层数有一定的限制。其层数决定了其时间复杂度,通过上述方法可以让其时间复杂度再最坏情况下也为\(O(logN)\)

// UnionSet
public class Community {
    private Community higher;
    private final Person lord;
    private int amount;
    
    Community(Person lord) {
        this.lord = lord;
        this.amount = 1;
        this.higher = null;
    }
    
    public Person getLord() {
        if (this.higher == null) {
            return this.lord;
        } else {
            return this.higher.getLord();
        }
    }
    
    public void setHigher(Community higher) {
        this.higher = higher;
    }
    
    public void setAmount(int amount) {
        this.amount = amount;
    }
    
    public Community getHighest() {
        if (this.higher == null) {
            return this;
        } else {
            return this.higher.getHighest();
        }
    }
    
    public int getAmount() {
        return amount;
    }
    
    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }
        if (!(other instanceof Community)) {
            return false;
        }
        Community myCommunity = (Community) other;
        return myCommunity.getLord().equals(this.getLord());
    }
}


而对于后面指令数的增加,利用递归可能会出现爆栈的风险,所以后两次作业我使用了非堆归的方法进行查询和合并。

private Person find(Person person, HashMap<Person, Person> communities) {
    Person temp = person;
    if (!communities.containsKey(temp)) {
        return person;
    }
    while (!communities.get(temp).equals(temp)) {
        temp = communities.get(temp);
    }
    Person updated = person;
    while (!communities.get(updated).equals(updated)) {
        updated = communities.get(updated);
        communities.put(updated, temp);
    }
    return temp;
}


 而对于qbs,可以维护一个变量blockSum,当addPerson时增加,当addRelation时,若两点不在一个集合中,blockSum减少,否则不变,这样qbs操作的时间复杂度就变成\(O(1)\)

// addRelation
if (!c1.equals(c2)) {
    blockCount--;
    // something else
}

// addPerson
people.add(person);
blockCount++;
Community community = new Community(person);
communities.put(person, community);

维护qbvs

 如果每次遇到啊一个qbvs就对所有点进行一次遍历,时间复杂度是\(O(n^2)\), 显然是存在优化的方法的。注意到当且仅当以下三种情况发生时,Group的Sum of Value才会发现改变

  • 当同一个Group中的两个Person使用了addRelation操作

  • 当与Group中的一个Person有关系的一个Person也addToGroup

  • 当与Group中与别的Person存在关系的Person从Group中delFromGroup

 所以我们对于每个Group对象维护一个valueSum属性,记录qbvs的答案,对于每个Person记录一个allGroups容器,记录他参加的Group,每当addRelation之后就去遍历allGroups,如果存在重合就增加sum,同理,addToGroup时也要做类似的操作。

最小生成树qlc

PriorityQueue

 优先队列,本质上利用堆来保证队首到队尾优先级依次递减,我们只需poll()可以将队首元素(优先级最高)弹出,利用add()新增元素,对于自定义类,我们用Comparator自定义比较方式即可

实现

 这个的难度其中要有一部分给到JML的阅读上,它是靠着两个点表示一条边的。对于最小生成树可以选择Prim或是Kruskal,这里我选择了 堆优化的Prim,感觉如果是完全图的话,Prim效率应该更优。所谓堆优化,本质也就是用PriorityQueue存着距离,这样就不用每次找最小了。其余就是正常的Prim算法,找最近的点纳入树中,以这个点为中转的点更新距离,直至纳入树的点数达到n个

 public int queryLeastConnection(int id) throws PersonIdNotFoundException {
    if (!people.containsKey(id)) {
        throw new MyPersonIdNotFoundException(id);
    }
    int sum = 0;
    int cnt = 1;
    PriorityQueue<Edge> edges =
            new PriorityQueue<>(Comparator.comparingInt(Edge::getValue));
    HashSet<Person> visited = new HashSet<Person>();
    MyPerson root = (MyPerson) people.get(id);
    // total of the connected dots
    int total = amounts.get(find(root, communities));
    visited.add(root);
    for (Person p : root.getValue().keySet()) {
        edges.add(new Edge(root.queryValue(p), root, p));
    }
    while (cnt < total && edges.size() > 0) {
        Edge e = edges.poll();
        while (e != null && visited.contains(e.getP2())) {
            e = edges.poll();
        }
        if (e == null) {
            break;
        }
        MyPerson person = (MyPerson) (e.getP2());
        visited.add(person);
        sum += e.getValue();
        cnt++;
        for (Person p : person.getValue().keySet()) {
            edges.add(new Edge(person.queryValue(p), person, p));
        }
    }
    return sum;
}

最短路sim

最短路问题,利用堆优化的Dijkstra,Prim和Dijkstra蛮像的,也都是用优先队列进行优化的,就不过多赘述了

public int queryLeastConnection(int id) throws PersonIdNotFoundException {
    if (!people.containsKey(id)) {
        throw new MyPersonIdNotFoundException(id);
    }
    int sum = 0;
    int cnt = 1;
    PriorityQueue<Edge> edges =
            new PriorityQueue<>(Comparator.comparingInt(Edge::getValue));
    HashSet<Person> visited = new HashSet<Person>();
    MyPerson root = (MyPerson) people.get(id);
    // total of the connected dots
    int total = amounts.get(find(root, communities));
    visited.add(root);
    for (Person p : root.getValue().keySet()) {
        edges.add(new Edge(root.queryValue(p), root, p));
    }
    while (cnt < total && edges.size() > 0) {
        Edge e = edges.poll();
        while (e != null && visited.contains(e.getP2())) {
            e = edges.poll();
        }
        if (e == null) {
            break;
        }
        MyPerson person = (MyPerson) (e.getP2());
        visited.add(person);
        sum += e.getValue();
        cnt++;
        for (Person p : person.getValue().keySet()) {
            edges.add(new Edge(person.queryValue(p), person, p));
        }
    }
    return sum;
}

bug及其修复

第九次作业

 本次没有出现bug,在互测中也没有出现bug。

互测看到同房间的人都用了并查集就没去hack,但后面发现他们有些合并没有做好,会弄成一条链导致出错,房间也有别的人用这个点成功hack了

第十次作业

 我的强测和互测都出现了bug,一个极其愚蠢的bug。在sendMessage中,对于person-group这种类型的消息,我把这个socialValue当作每个Group的属性,但并不是如此,group中的人是变化的,所以信息的socialValue应该还是要加在每个人的属性当中,是我想转化JML时对这一性质理解不到位出现的问题。

其实是写了评测机的,但看来就是随机数据写的不是很好,生成的数据不够全面。
这次依靠构建的一些边界数据成功hack了,利用一条链卡了qbvs\(O(n^2)\)的实现。

第十一次作业

 本次强测未出现bug,但互测被hack惨了,就一个很傻很傻的bug,dce应该是要把不在EmojiList的emoji消息给remove掉,但我少打了一个!,完全相反的意思了,不懂为啥对拍评测机(30w的数据了)和强测都没有测出来。

Network拓展

 对Person类进行了扩展,有以下几类

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

发送广告

/*@ public normal_behavior
  @ requires containsAdvertisement(id);
  @ assignable advertisements, people[*].advertisements;
  @ ensures !containsAdvertisement(id) && advertisements.length == \old(advertisements.length) - 1
  @ ensures (\forall int i; 0 <= i && i < \old(advertisements.length) && \old(advertisements[i].getId()) != id;
  @             (\exists int j; 0 <= j && j < advertisements.length; advertisements[j].equals(\old(advertisements[i]))));
  @ ensures (\forall int i; 0 <= i && i < people.length; people[i].isFavorable(id) ==> 
  @             (\exists int j; 0 <= j && j < people[i].advertisements.length; people[i].advertisements[j] == id) &&
  @              people[i].advertisements.length == \old(people[i].advertisements.length) + 1);
  @ ensures (\forall int i; 0 <= i && i < people.length; !people[i].isFavorable(id) ==> 
  @             (\forall int j; 0 <= j && j < people[i].advertisements.length; people[i].advertisements[j] != id)) && 
  @              people[i].advertisements.length == \old(people[i].advertisements.length));
  @ ensures (\forall int i; 0 <= i && i < people.length; 
  @             (\forall int j; 0 <= j < \old(people[i].advertisements.length)
  @                 (\exists int k; 0 <= k < people[i].advertisements.length;  
  @                     \old(people[i].advertisements[j]) == people[i].advertisements[k])));
  @ public exceptional_behavior
  @  signals (AdvertisementIdNotFoundException e) !containsAdvertisement(id);
  @ */
public void postAdvertisement(int id) throws AdvertiseIdNotFound;

购买商品

/*@ public normal_behavior
  @ assignable getPerson(personId).money,  getSaler(salerId).getProduct(productId).getLeftNum()  
  @ requires contains(personId);
  @ requires containsProduct(productId);
  @ requires containsSaler(salerId);
  @ ensures getPerson(personId).money = \old(getPerson(personId).money) - getProduct(productId).getValue;
  @ ensures getSaler(salerId).getProduct(productId).getLeftNum() = \old(getSaler(salerId).getProduct(productId).getLeftNum()) - 1;
  @ also
  @ public exceptional_behavior
  @ signals (PeronIdNotFoundException) !contains(personId);
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException) !containsProduct(productId);
  @ also
  @ public exceptional_behavior
  @ signals (SalerIdNotFoundException) !containsSaler(salerId);
  @*/
public void purchaseProduct(int personId, int productId, int salerId);

偏好设置

/*@ public normal_behavior
  @ requires contains(personId);
  @ requires containsProduct(productId);
  @ ensures getPerson(personId).isFavorable(productId) == true;
   @ also
  @ public exceptional_behavior
  @ signals (PeronIdNotFoundException) !contains(personId);
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException) !containsProduct(productId);
  @*/
public /*@ pure @*/void saleProduct(int personId, int productId);

体会与感想

 在这一单元第一次接触到了JML,刚开始觉得它很繁琐,后面感觉到它确实可以对每个类和方法制定清晰的规格。而阅读JML也不只是单纯的转换和翻译,应该要从规格中得出这个方法的目的和预期的效果。这一单元也帮助复习了一些算法,我也学习了JUnit的使用。但在使用的时候感觉用的不是那么丝滑,还是依靠自动测评机加对拍进行正确性的检验。也确实帮助我检查出了许多bug,但显然写的还不够好,每次都有一个bug没有检查出来。
$emsp;从对比角度来讲,本单元的挑战性和难度较前两单元有所下降,但是尽管如此,想要取得高分也非易事,首先本次所需功能指令的数量增加,bug出现的概率也随之增加;而且对于指令的理解纯凭借对JML的理解,理解错误就是满盘皆输。此外琐碎繁杂的需求使得测试和debug变得困难,尽管写了自动化测试,但是没能达到100%数据全覆盖,对于极端数据和压力数据更是需要靠手动构造,这都为强测与互测带来了风险。

posted @ 2022-06-02 21:34  logiclee0902  阅读(32)  评论(1编辑  收藏  举报