BUAA-OO-第三单元总结

北航计算机学院面向对象第三单元总结

由于本单元三次作业非常相似,都是基于上一次的功能和指令进行迭代扩展,因此以下总结都以第三次作业为例。

一、 利用JML规格生成数据并测试

本单元作业是契约式编程,不需要在架构设计上花太多心思,作业中容易出现Bug的情况是对官方给定的JML理解有误,以及设计的算法时间复杂度过高。因此本单元的自动测试基本是为了验证程序的功能正确性,而压力测试主要靠针对实现代码使用的算法手动构造数据,比如对并查集、最短路径的压力测试等。生成数据后的自动测试比较简单,与第二单元的多线程并行不同,本单元程序的运行结果是固定、可预测的,因此只需要找几个小伙伴对拍、逐行对比输出即可。(但要小心大家是不是同一个Bug

生成的数据分别对Message的相关操作、其余的操作进行测试,主要是因为Message的相关操作相对于其余的操作而言,结构和功能上都相对独立一些。

对Message的测试数据

# 首先初始化图以及表情列表
# add person
for i in range(30):
    instrlist += "ap"
        ……    
# add relation
for i in range(300):
    instrlist += "ar"
        ……
# 增加两个组
instrlist += "ag"
        ……
for i in range(25):
    # 25个人在组里,5个人不在组里以验证异常
    instrlist += "atg"
        ……
for i in range(15):
    # 初始15个表请
    instrlist += "sei "
        ……
# 对Message的相关指令进行随机性功能检测
for i in range(950):
    instr = random()
    if instr < 0.4 and len(msgId) != 0:
        instrlist += choice(["sm","sim"])
        ……
    elif instr < 0.8:
        cnt += 1
        if cnt == 5:
            # 每五个人增加一个msgId缺失,保证对异常抛出的检测
            msgId.append(randint(1001, 5000))
            cnt = 0
        type = randint(0, 1)
        instrlist += choice(['arem','am','anm','aem'])
        ……
    elif instr<0.9:
        instrlist += "qm"
        ……
    elif instr<0.95:
        instrlist += "qp "
        ……
    elif instr < 0.97:
        instrlist += "qrm "
        ……
    elif instr<0.98:
        instrlist+="dce "
        ……
    else:
        instrlist += "cn"
        ……    

对其余操作的测试数据

除开Message以外指令关联性较强,因此选择在同一张图上进行随机测试

# 初始化图
for i in range(people_num):
    instrlist += "ap"
    ……
for i in range(group_num):
    instrlist += "ag"
    ……
# 随机枚举指令,进行数据生成
for i in range(LENGTH - people_num):
    instr = choice(instrs)
    ……
    if instr == "qci":
      qci_num += 1
    if instr == "qlc":
      qlc_num += 1
    if instr == 'ar':
      Instrlist += ' '
      ……
    elif instr == 'atg' or instr == 'dfg':
      ……
    elif instr == 'qv' or instr == 'qci':
      ……
    ……

而针对时间的压力测试,本单元只针对qbs指令(是否使用并查集、并查集是否路径压缩),qlc指令(是否对最小生成树朴素算法进行优化)构造了零图和完全图。(第三次互测摸了

 

二、 架构设计及维护策略

UML类图

考虑到简洁性,上述UML类图没有包含异常类

架构设计

本单元的架构以及图的模型,基本上由官方包搭建好了,只要阅读并正确理解JML描述,大家的架构应该相差不大。阅读官方包不难发现,本单元作业主要是维护一个社交网络,以Person为图的节点,以Relation为图的无向边。针对这个图进行诸如:查询连通、最小生成树、最短路径等一系列操作。而我们要重点关注的,应该是使用怎样的数据结构、算法、容器来对图的一些属性进行维护,以便能够简洁、迅速地完成指令对图的修改和查询。

维护策略

路径压缩的并查集

通过阅读JML规格,不难发现本单元作业中的qci指令即为查询图中两点是否连通,qbs指令为查询图中连通分支数量,qlc指令为查询包含某点的最小生成树。这三个查询都能够借助并查集实现。相信并查集的相关实现大家都不陌生,在本单元作业中具体而言就是,在add person时向并查集中添加节点并将其设为一个源点(也就是它所在集合里的boss),在add relation时检查是否需要合并两个集合,更新源点。使用路径压缩的写法能够防止图退化成一条链时,多次查询首尾两个点连通关系可能会导致超时。同时以下给出的写法采用的是非递归的形式,这能避免上面描述的链结构时可能出现的爆栈情况。

public int find(int x) {
    int cur;
    int tmp;
    int root;
    root = x;
    while (root != origin.get(root)) {
        root = origin.get(root);//查找根节点
    }
    cur = x;
    while (cur != root)             //非递归路径压缩操作
    {
        tmp = origin.get(cur);        //用tmp暂存parent[cur]的父节点
        origin.replace(cur,root);        //parent[x]指向根节点
        cur = tmp;                    //cur指向父节点
    }
    return root;         //返回根节点的值
}

基于基数排序优化的克鲁斯卡尔算法

第二次作业中的qlc指令实际上是查询包含某节点的最小生成树,最小生成树算法一般为prim和kruskal算法,但两算法算法的朴素算法时间复杂度都是O(n²)/O(e²),在本次作业中1e4量级的数据会超时。考虑到作业中稀疏图出现的概率应该是大于稠密图的,因此最终选择使用了kruskal算法。而针对该算法的优化就是对遍历的边按权值进行排序。其实使用堆排序优化就可以,java还有现成的优先队列用,但自己想尝试写写看基数排序效果怎么样,结果发现不如同学的堆排序(小丑竟是我自己。

具体写法其实和朴素算法几乎一样,不同的是需要在遍历前对边进行排序,而找边就不需要每次通过遍历寻找最小边,直接从当前索引下标开始,寻找能连通两个非连通分支的边即可。

public int getValue() {
        // 获取连通分支中的所有无向边
        for (Person person1:people) {
            ......
            ArrayList<Person> persons = ((MyPerson) person1).getAcquaintance();
            for (int i = 0;i < persons.size();i++) {
                ......
                edges.addEdge(new Edge(value.get(i),id1,id2));
            }
        }
     // 对边进行升序排序 edges.radixSort(); ......
while (cnt < sum && index < sumOfEdge) { Edge edge = edges.getEdge(index); // 选择能将两个非连通分支连通的最短边 while (isCircle(edge)) { index++; edge = edges.getEdge(index); } rst += edge.getValue(); ...... //更新并查集
       searchSet.merge(searchSet.find(edge.getFrom()),searchSet.find(edge.getTo()));
...... } return rst; }

堆优化的迪杰斯特拉算法

第三次作业中的sim指令实际上是查询两个连通点之间的最短路径,最短路径的算法有弗洛伊德算法(时间复杂度O(n³),pass),迪杰斯特拉算法(朴素写法时间复杂度O(n²),pass;堆优化后O((m+n)logn),可以考虑)以及SPFA算法(平均O(m),最坏O(mn)),由于对于网络流的算法不是很熟悉,同时在网上查阅资料发现有很多前辈指出SPFA算法的时间复杂度很玄学,因此最后选择使用了堆优化的迪杰斯特拉算法。

三、 分析代码中的性能问题

比较容易发现的性能问题在维护策略中已经给出,但除此之外这次作业中还有不少可能因为性能问题而超时的地方。

说到底JML仅仅是通过规格化的语言描述了方法和类的功能、执行条件、过程。规格化语言是死板的,但写程序的人是灵活的,单纯依照JML的描述编写的代码很有可能是性能最差的(尤其是比较复杂的方法,涉及到多重循环时)。在阅读JML编写程序时,不应该不加以思考,盲目地跟从JML的描述coding,而是要充分理解其描述的功能,思考某些循环是否真的有必要?某些嵌套循环是否能通过别的方法去掉?笔者认为只要在编写过程中对每个描述持有怀疑态度,就基本不会出现性能问题。

除开维护策略中提到的以外,JML给出的写法可以优化的地方大概有以下几点:

  • 善用容器

本单元作业出现的各个类的对象都有其唯一的id,善用HashMapHashSet等容器,可以去除JML中出现的很多循环。比如Group类中的hasPerson()方法,JML描述为

ensures \result == (\exists int i; 0 <= i && i < people.length; people[i].equals(person));

看起来是一个O(n)的方法,实际上使用HashMap作为存储Person的容器,利用HashMap.containsKey()方法,就能优化至O(1)的复杂度。诸如此类的优化本单元作业里还有很多,这里就不再赘述。

  • 小心嵌套调用

有的函数从表面上看只是O(n)的时间复杂度,然后其中却暗藏杀机,它在每次循环的时候调用了某个方法,而当该方法也是0(n)的复杂度时,实际上最后运行下来就会是O(n²)的复杂度,存在TLE的风险。比如Group类中的getAgeVar()方法,JML描述为

ensures \result == (people.length == 0? 0 : ((\sum int i; 0 <= i && i < people.length;(people[i].getAge() - getAgeMean()) * (people[i].getAge() - getAgeMean())) / people.length));

这个描述看上去时间复杂度只有O(n),但它每次循环都调用了getAgeMean()方法,该方法的JL描述同样为O(n)的时间复杂度,这样嵌套调用最终就导致了getAgeVar()方法的复杂度变成了O(n²)。实际上第二次作业互测同组就有同学对于qgvs指令没有考虑函数嵌套调用出现了tle。

四、 Network扩展

任务分析

  • 对于Advertiser、Producer、Customer可以分别继承抽象类Person,共有id、money等属性,sendMessage()等方法。
  • 对于消费者,可以添加一个commodityType[] commodityPrefer属性记录偏好,可根据偏好搜索商品。
  • 对于Advertiser,可以使用单例的模式,将其视为一个“平台”会更加方便。
  • 对于advertise、produce、purchase可以分别继承类Message,将这三个行为视为一个Person对象发送的信息。

方法JML

 

sendAdvertisement()
/*@ public normal_behavior
  @ requires containsMessage(id) && (getMessage(id) instanceof Advertisement);
  @ assignable messages;
  @ assignable people[*].messages;
  @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
  @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
  @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
  @ ensures (\forall int i; 0 <= i && i < people.length; person[i].likeType(\old(((Advertisement)getMessage(id))).getType) ==> 
  @         ((\forall int j; 0 <= j && j < \old(person[i].getMessages().size());
  @          person[i].getMessages().get(j+1) == \old(person[i].getMessages().get(j))) &&
  @         (person[i].getMessages().get(0).equals(\old(getMessage(id)))) &&
  @         (person[i].getMessages().size() == \old(person[i].getMessages().size()) + 1)));
  @ also
  @ public exceptional_behavior
  @ signals (MessageIdNotFoundException e) !containsMessage(id);
  @ signals (MessageNotAdverException e) containsMessage(id) && !(getMessage(id) instanceof Advertisement);
  @*/
public void sendAdvertisement(int id) throws MessageIdNotFoundException, MessageNotAdverException;
sendPurchase()
/*@ public normal_behavior
  @ requires containsMessage(id) && (getMessage(id) instanceof Purchase);
  @ requires (getMessage(id).getPerson1() instanceof Customer) && (getMessage(id).getPerson2() instanceof Producer);
  @ assignable messages;
  @ assignable getMessage(id).getPerson1().money;
  @ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().money;
  @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
  @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
  @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
  @ ensures (\old(getMessage(id)).getPerson1().getMoney() ==
  @         \old(getMessage(id).getPerson1().getMoney()) - ((Purchase)\old(getMessage(id))).getMoney() &&
  @         \old(getMessage(id)).getPerson2().getMoney() ==
  @         \old(getMessage(id).getPerson2().getMoney()) + ((Purchase)\old(getMessage(id))).getMoney());
  @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
  @          \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
  @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
  @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
  @ ensures getProduct(\old((Purchase)getMessage(id)).getProductId()).getNum() == \old(getProduct(\old((Purchase)getMessage(id)).getProductId()).getNum()) + 1;
  @ also
  @ public exceptional_behavior
  @ signals (MessageIdNotFoundException e) !containsMessage(id);
  @ signals (MessageNotPurchaseException e) containsMessage(id) && !(getMessage(id) instanceof Purchase);
  @ signals (PersonNotCustomerException e) containsMessage(id) && (getMessage(id) instanceof Purchase) && 
  @         !(getMessage(id).getPerson1() instanceof Customer);
  @ signals (PersonNotAdvertiserException e) containsMessage(id) && (getMessage(id) instanceof Purchase) && 
  @         (getMessage(id).getPerson1() instanceof Customer) && !(getMessage(id).getPerson2() instanceof Producer);
  @*/
public void sendPurchase(int id) throws
        MessageIdNotFoundException, MessageNotPurchaseException, PersonNotCustomerException, PersonNotAdvertiserException;
querySale()
/*@ public normal_behavior
  @ requires containsProduct(id);
  @ ensures \result == getProduct(id).getNum();
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !containsProduct(id);
  @*/
public /*@ pure @*/ int querySale(int id) throws ProductIdNotFoundException;

五、 学习体会

  • 切实体会到JML语言的严谨性带来的力量,能够零歧义地描述一个模块的功能和运行流程,我认为是一个很了不起的作品。
  • 同时JML语言一定程度上也存在局限性和缺点,在面对较为复杂的方法时,JML的阅读和书写都会变得非常复杂。以qlc方法为例,刚开始写第二次作业时没有注意到指导书上提示的“最小生成树”算法,初见qlc方法的JML描述不得不让我望而生畏。层层剖析它的括号,用纸笔一点点推导,二十多分钟后才终于恍然大悟,这xx不就是最小生成树吗?如果用数学语言甚至一行公式就能描述清楚。。。

 

posted @ 2022-06-01 17:00  Kazeya_y  阅读(17)  评论(1编辑  收藏  举报