BUAA OO 第三单元总结与反思

BUAA OO 第三单元总结与反思

写在前面

本单元主要考察了对JML规格的理解与运用,目的是实现一个小型的社交系统,支持建立小组、私发/群发消息、发送表情包、发送红包等功能,并具有相对完备的异常处理能力。

由于JML的规格已经由助教门给出,因此绝大部分内容只需要照猫画虎即可完成,实现的难度相较于前两个单元的确是小了不少。但同时要想取得满分,尤其是要避免被卡TLE,就要求我们充分理解特定方法的规格,而后进行无向图模型的建模,找到合适的图算法,并在能力范围内进行最大程度的优化

自测与数据

在本单元的学习中,我尝试学习了课程组推荐的Junit测试工具,了解了具体如何使用Junit进行测试,但由于任然需要自己编写测试数据进行测试,Junit相当于为我们提供了一个框架,因此我最终并没有大量使用它来进行测试,而是以自动生成测试数据+与同学进行对拍的方式进行测试。

对于数据整体覆盖的全面性,采用C++代码自动生成的方式生成大量测试数据:

int personId = 0, groupId = 0, messageId = 0;
int maxag = 20, maxap = 2500, maxqci = 100, maxqlc = 20;
int cntag = 0, cntap = 0, cntqci = 0, cntqlc = 0;

int type = 0; // 0 normal, 1 message, 2 group, 3 graph, 4 exception

如图是为了符合课程要求的一些限制条件(来自G同学的代码)。

同时对于特定的高复杂度指令,我也使python代码构造高强度数据进行测试防止出现TLE。

from numpy import random

addperson = ""
addrelation1 = ""
addrelation2 = ""
addtogroup = ""
qlc = ""
for i in range(1500):
    addperson = addperson + 'ap ' + str(i) + " name"+str(i) + ' 10\n'

for i in range(1500):
    addrelation1 = addrelation1 + 'ar ' + str(i) + " " + str(i+1) + ' 20\n'

for i in range(1980):
    addrelation1 = addrelation1 + 'ar ' + str(random.randint(0, 1500)) + " " + str(random.randint(0, 1500)) + ' ' + str(random.randint(1, 50)) +'\n'

for i in range(20):
    qlc = qlc + 'qlc ' + str(i) + ' \n'

fo = open("case3.txt", "w")
fo.write(addperson+addrelation1+qlc)

如图是针对 qlc 指令的高强度测试数据。

整体架构设计

模型构建

本单元的作业背景是社交网络模型,可以比较完美的抽象成无向图的模型。Person可以看作图中的点,Relation可以看作图中的边,Group可以看作图中特定点的集集合,Message可以看作 点/点集点/点集之间的交流。

容器选择

JML并没有限制我们使用哪种容器,因此需要我们自己根据该容器在JML中的使用特性来做出选择。对于大部分数据我都采用了Arraylist容器,但事实上归过头来看这并不是一个很好的选择,对大部分元素来说有唯一Id,因此选择使用Hashmap或者HastSet这样的哈希容器时更好的选择,因为它们拥有O(1)的查询复杂度,而Arraylist在查询元素时自带了O(n)的时间复杂度,因此对后续算法要求提高了很多,这可能也是导致我出现tle的原因之一。

图论算法

在本次作业中,部分指令用到了一些图论算法,这也是本单元作业的难点和性能优化的重点,例如qbs和qci使用到了并查集、qlc使用到了最小生成树算法、sim使用到了最短路算法。

路径压缩的并查集
    public void addper(int id) {
        pre[id] = id;
    }

    public boolean unite(int x, int y) {
        int rootx = findpre(x);
        int rooty = findpre(y);
        if (rootx != rooty) {
            pre[rooty] = rootx;
            return true;
        }
        return false;
    }

    public int findpre(int x) {
        if (pre[x] == x) {
            return x;
        }
        return pre[x] = findpre(pre[x]);
    }
kruskal 算法求最小生成树
        int ans = 0;
        int flag = 0;
        while (flag < related.size() - 1) {
            Edge edge = priorityQueue.poll();
            if (search1.findpre(edge.getId1()) != search1.findpre(edge.getId2())) {
                search1.unite(edge.getId1(), edge.getId2());
                flag++;
                ans += edge.getValue();
            }
        }
        return ans;
Dijsktra 算法求(单源)最短路
        //dij
        ans[idroot] = 0;
        while (true) {
            int flag = 0;
            Point point = null;
            while (!priorityQueue.isEmpty()) {
                point = priorityQueue.poll();
                if (ans[point.getIdto()] > point.getValue()) {
                    ans[point.getIdto()] = point.getValue();
                    dirt[point.getIdto()] = 1;
                    flag = 1;
                    break;
                }
            }
            if (dirt[mapping.get(tmp.getPerson2().getId())] == 1 || flag == 0) {
                break;
            }
            MyPerson person = (MyPerson) people.get(point.getIdto() - 1);
            for (int i = 0; i < person.getAcquaintance().size(); i++) {
                if (dirt[mapping.get(person.getAcquaintance().get(i).getId())] == 0) {
                    priorityQueue.add(new Point(mapping.get(person.getId()),
                            mapping.get(person.getAcquaintance().get(i).getId()),
                            ans[point.getIdto()] + person.getValue().get(i)));
                }
            }
        }
        messages.remove(tmp);
        return ans[mapping.get(tmp.getPerson2().getId())];

数据缓存

这是本次作业的一个重要思想,对于需要频繁查询的数据进行缓存处理,将时间复杂度分摊到其他的指令,从而避免了tle的出现。

以qgvs为例:

首先在MyGroup类中增加 valuesum 属性。

private int valuesum;
在addPerson方法中维护`valuesum
    @Override
    public void addPerson(Person person) {
        people.add(person);
        for (Person person1 : ((MyPerson) person).getAcquaintance()) {
            if (hasPerson(person1)) {
                valuesum += person.queryValue(person1) * 2;
            }
        }
    }
在delPerson方法中维护valuesum
    @Override
    public void delPerson(Person person) {
        int i;
        for (i = 0; i < people.size(); i++) {
            if (people.get(i).equals(person)) {
                break;
            }
        }
        people.remove(i);
        for (Person person1 : ((MyPerson) person).getAcquaintance()) {
            if (hasPerson(person1)) {
                valuesum -= person.queryValue(person1) * 2;
            }
        }
    }
在addRelation方法中维护valuesum
    @Override
    public void addRelation(int id1, int id2, int value) throws
            PersonIdNotFoundException, EqualRelationException {
        if (!contains(id1)) {
            throw new MyPersonIdNotFoundException(id1);
        } else if (!contains(id2)) {
            throw new MyPersonIdNotFoundException(id2);
        } else if (getPerson(id1).isLinked(getPerson(id2))) {
            throw new MyEqualRelationException(id1, id2);
        }
        MyPerson p1 = (MyPerson) getPerson(id1);
        MyPerson p2 = (MyPerson) getPerson(id2);
        p1.addfriend(getPerson(id2), value);
        p2.addfriend(getPerson(id1), value);
        if (search.unite(mapping.get(id1), mapping.get(id2))) {
            qbs--;
        }
        for (Group group1 : group) {
            if (group1.hasPerson(getPerson(id1)) && group1.hasPerson(getPerson(id2))) {
                ((MyGroup) group1).setValuesum(((MyGroup) group1).getValuesum() + value * 2);
            }
        }

    }
最后在qgvs指令中只需找到对应group返回其 valuesum 即可
    @Override
    public int queryGroupValueSum(int id) throws GroupIdNotFoundException {
        for (Group value : group) {
            if (value.getId() == id) {
                return value.getValueSum();
            }
        }
        throw new MyGroupIdNotFoundException(id);
    }

问题与修复

第九次作业

在第九次作业中,我由于未读清 EqualRelationException 异常的描述,误以为id1与id2相等之后就仅需输出一次;同时也未缓存qbs的数据,而是完全按照JML的规格完成,未完全理解qbs所求的是Network中联通分支数量这一抽象概念。从而导致了强测中挂掉了4个点。

第十次作业

在第十次作业中,我在提交截止前加强的测试,生成了大量数据与同学进行对拍,确保了万无一失,最终做的还算不错,强侧与互测都未出现问题。

第十一次作业

在第十一次作业中,我虽然采用了Dijsktra算法并实现了堆优化+并查集优化,将时间复杂度由O(m + n^2) 降为 O(n \* logn + m) (其中n为点数目,m为边的数目)但由于在执行算法的过程中写了一条十分愚蠢的代码,从而提升了复杂度最终使得强侧因为sim指令而导致了tle,互测中也因此被hank了两个点。在之后的bug修复环节,我一度以为自己时的dij写的不够好,最终花了大量时间(几乎不亚于完成这次作业的时间),最终才在这个最不可能的地方找出了这个小小的bug,这让我我十分懊恼。

Network扩展

假设出现了几种不同的Person

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

① 新增Good类表示可交易产品,包括售价、折扣等等属性。

② Advertiser、Producer和Customer可以继承Person类;Producer需要增加一个属性来表示产的产品的id集合,Advertiser需要增加一个属性来表示其向外发送广告的产品的id集合,Customer需要增加两个属性,一个表示其偏好的产品的id集合,另一个来表示对偏好id集合中每个id的偏好值。

③ 新增广告信息 AdvertisementMessage 和 PurchaseMessage 两个类,它们继承Message类。

④ 当Customer对某物品的偏好值大于等于某个阈值时,通Advertiser过向该产品的Producer发送PurchaseMessage。

设置消费者偏好

/*@ 
  @ public normal_behavior     
  @ requires contains(personId);
  @ requires getPerson(personId) instance of Customer;       
  @ ensures (\forall int i; 0 <= i && i < \old(favorGoods.length);
  @          (\exists int j; 0 <= j && j < favorGoods.length;favorGoods[j] == 			          
  @           (\old(favorGoods[i]) && favorValues[j] == (\old(favorValues[i]))));
  @ ensures (\exists int i; 0 <= i && i < favorGoods.length; favorGoods[i] == 			          
  @          favorGoodId && favorValues[i] == (\old(favorValues[i])) + value);
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !contains(personId);
  @ signals (PersonWrongTypeException e) !(getPerson(personId) 
  @                                         instance of Customer);
  @*/    
public /*@ pure @*/void addFavor(int personId, int favorGoodId, int value) throws PersonIdNotFoundException, 
PersonWrongTypeException;

消费者购买物品

/*@
  @ public normal_behavior
  @ requires contains(personId);
  @ requires getPerson(personId) instance of Customer;      
  @ assignable getPerson(personId).favorGoods;          
  @ ensures (\forall int i; 0 <= i && i < getPerson(personId).favorGoods.length); 
  @           favorValues.get(i) < limit); 	
  @ ensures (\forall int i; 0 <= i && i < getPerson(personId).\old(favorGoods).length
  @			  && \old(favorValues).get(i) >= limit; 			 
  @             sendPurchaseMessage(getPerson(personId).favorGoods.get(i))
  @          ); 	
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !contains(personId);
  @ signals (PersonTypeWrongException e) !(getPerson(personId) instance of Customer);
  @*/    
 public void buyGoods(int personId ,int limit) throws PersonIdNotFoundException, PersonTypeWrongException;

查询生产者销售额

/*@
  @ public normal_behavior
  @ requires contains(personId);
  @ requires getPerson(personId) instance of Producer;
  @ assignable nothing;
  @ ensures \results == (\sum int i; 0 <= i && i <((Producer) 
  @                       getPerson(personId)).\old(saleGoods).length;  			 
  @             		  saleGoods.get(i).getPrice() * salenumbers.get(i)); 
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !contains(personId);
  @ signals (PersonTypeWrongException e) !(getPerson(personId) instance of Customer);
  @*/    
 public /*@ pure @*/ int querySaleValue(int personId) throws PersonIdNotFoundException,     personTypeWrongException;

心得体会

本单元的难度总体来说相比于前两个单元并不能算大。主要的任务就是阅读 JML 约束并遵照它进行代码的书写,只要仔细阅读并遵照它的规则指引,几乎就不会犯什么错误(除非像我一样阅读指导书出了错)。与之相比,本单元更大的工作量都体现在还是在性能优化和测试上。尤其是对图论相关知识的复习和再次学习,甚至都超过了曾经的数据结构课程。同时在写的过程中我亦感受到Java这门高度封装的语言相比C语言的优势——更简洁的代码书写,一个用C语言需要近百行的图论算法用java在50行之类一定能轻松解决,且拥有更好的可读性。

本单元作业是OO课程的最后的互测机会,相比于前两个单元,我由于更多被hank,因此也被迫激发了自己hank的动力,结合自动化生成数据和针对性构造样例,成功hack到许多的bug。每周的hankday结束之后,都能感觉到莫名的轻松自如。

总的来讲,本单元的OO作业完成的并不好,出现了好一些不该出现的低级错误,但是在这样的打击中也让我的心态得到了一些历练,在今后面对意外情况的时候希望自己能处理的更加完美。OO还剩最后一个单元的内容在等着我,希望自己能够抓住机会,细心对待,不要再留下更多的遗憾。

posted @ 2022-06-05 10:30  Arosy_24  阅读(48)  评论(2编辑  收藏  举报