BUAA_OO_UNIT3_BLOG_JML

BUAA_OO_UNIT3_BLOG_JML

一、摘要

JML单元相较前两单元显得轻松了一些,从 JML 到 Java 更像是一个翻译的过程。只要按照官方给出了的 JML 撰写代码,正确性似乎难度较小,但是较大的数据量对算法的复杂度发出了 “无声” 的要求(OO 算法课),很多精力都放在 RTLE 和 CTLE 上。

 

二、测试

测试采用了 随机测试 + 边界测试的方法。

  • 随机测试。采用了随机生成的数据测试,基本可以保证程序的正确性。

  • 边界测试。通过极端数据,如最大量的算法指令或 Group 超 1111的数据进行测试,防止 RTLE 的出现。

(对拍找出了一些bug)

for i in range(100):
   print("\n\t Now is data" + str(i))
   os.system("time java -jar ../" + str(p+1) +".jar < ../data"+str(i)+".txt > output" + str(i) + ".txt")
   os.system("diff ../output" + str(i) +".txt output"+str(i)+".txt > diff-"+str(i)+".txt")

 

三、架构设计

三次作业各引入了一种算法。

第九次作业:不相交集合问题

采用了并查集算法,使用的数据结构

 private final ArrayList<HashSet<Person>> map = new ArrayList<>();

Hashset 中会覆盖重复的元素,在并查集算法中使用更加便捷、性能更好。

社交网络中,每一条 ap 指令(add person)都会在 Arraylist 中增加一个存放该 Person 的 Hashset 。

ar 指令(add relation)涉及并查集的维护,以下给出一个实例流程:

即将两个元素所在的集合合并。

 

第十次作业:最小生成树问题

算法:

最小生成树常见的算法有如下两种:

  • Prim算法,对节点操作,找和节点集合最近的点

  • Kruskal算法,对边操作,找最短边

考虑到测试数据量较大,需要对边集进行更新,因此选择了对边操作更加便捷的 Kruskal 算法,并用并查集的算法去维护边集。

(实测,Prim算法 + 不维护边集,一组强测限制下的数据需要 900 s)

此外,新建了一个 Side 类,存储边两端节点和长度等信息。

边集的更新时机:
  • 边的添加和边集的合并:这一部分添加到了上一次作业的算法中。

  • 边的删除(更新):Kruskal 算法每执行一次,对应边集中,没有用到的边将会删除。

(最小生成树现在未用到的边,将来也不会用到)。

数据结构:
 ArrayList<ArrayList<Side>> sides

和不相交问题不同,为了便于排序并且不会存在相同的边,内部也使用了 ArrayList 容器。

 

第十一次作业:最短路径问题

采用了 迪杰斯特拉算法(未进行堆优化,完全图为最坏情况,时间复杂度为O(n^2))。MyNetwork 类超出了 checkstyle 代码行数的限制,所以新建了一个 Algorithm 类,三次作业的算法都放在其中并设置为 static

数据结构:
 HashMap<Person, Integer> path = new HashMap<>();   //起始点到 Person 的路径长度
ArrayList<Person> flag = new ArrayList<>(); //标志 Person 是否被遍历过
ArrayList<Person> in = new ArrayList<>(); //找到路径的点
算法流程:

                                                                         

 

 

四、性能问题和修复情况

(褚老师 : 第三单元是拿分的好时机)

第九次作业:

强测、互测均未出现 bug。

第十次作业:

强测中出现两个测试点错误,互测时出现一个测试点错误,错误类型都是 RUNTIME_ERROR

bug分析:

没有考虑集合中只有一个人的情况,在维护并查集和边集时,并查集对应的边集会被删除,在之后维护边集时就可能会出现 IndexOutOfBoundsException

bug修复:

MyNetwork 类中的 findLeastTree 方法中添加了对一个元素集合的特判。

第十一次作业:

强测中出现一个测试点 CPU 运行超时。

这个测试点的错误我不是很能理解:

  • CPU使用时间 > 总体运行时间 ???

  • 本地运行这组数据时,只用了 4 秒,到服务器上却超时了 ???

 

五、NetWork 扩展

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告

  • Producer:产品生产商,通过Advertiser来销售产品

  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息

  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等请讨论如何对Network扩展。

新的三类人,分别继承 Person 类, 并增加新的属性和方法。还需要一个新的广告类继承 Message。Network 中添加两个方法 queryVolume() 和 queryPath(Message advertise);

假设广告商可能有多层,即 生产商 ---> 广告商 ---> ... ---> 广告商 ---> 顾客

(以下 get 和 set 方法不完全)

public interface Advertiser extends Person {
   private Person upper; //上一级广告商或生产商
   public void sendRequest(Message advertise);
   public Message sendAdvertise();
}
public interface Producer extends Person {
   private Type volume;
   public Type queryVolume();
   public void produce();
}
public interface Customer extends Person {
   private type[] preference;
   public void sendRequest();
   public void setPreference();
}
public interface Advertise extends Message {
   private type preference;
   private Person advertiser;
   private Person producer;
   public Person getAdvertiser();
   public Person getProducer();
}

核心业务方法:

  • Customer 中的 sendRequest 方法

        /*@ public normal_behavior
         @ requires (messages.length > 0 && preferences.length > 0);
         @ assignable nothing;
         @ ensures (\forall int i; (0 <= i && i < messages.length) &&
         @ (messages[i] instanceof Advertise) &&
         @         (\exists int j; 0 <= j && j < preferences.length;
         @ preferences[j] == ((Advertise)message[i]).getPreference);
         @ (((Advertise)message[i]).getAdvertiser.sendRequest(message[i])));
         @*/
    public void sendRequest()
  • Advertise 中的 sendRequest 方法

        /*@ public normal_behavior
         @ requires (upper instanceof Producer);
         @ assignable nothing;
         @ ensures ((Producer)upper).produce();
         @ public normal_behavior
         @ requires (upper instanceof Advertiser);
         @ ensures ((Advertiser)upper).sendRequest();
         @*/
    public void sendRequest()
  • Network 中的 queryPath 方法

        /*@ public normal_behavior
         @ requires (advertise instanceof Advertise);
         @ assignable nothing;
         @ ensures (\forall int i; 0 <= i && i < result.length;
         @ (exists int j; 0 <= j && j < people.length; result[i] == people[j]);
         @ ensures (\forall int i; 0 <= i && i < result.length - 1;
         @ ((Advertiser)people[i].getUpper == people[i + 1];)
         @ ensures result[0] == (Advertise)advertise.getAdvertiser();
         @ also
         @ public exceptional_behavior
         @ requires !(advertise instanceof Advertise);
         @ signals NotAdvertiseException;
         @*/
    public List<Person> sendRequest(Message advertise);

查询产品的销量实际就是查询对应 Producer 的 getVolume() 方法,不做具体描述。

 

六、学习体会

从几次实验和作业中,感受到了 JML ---> Java 翻译的快乐和 JML 攥写规格的痛苦,但是也逐渐理解了 JML 和 规格存在的意义。

  • 契约式编程。对契约式编程有了初步了解,契约将方法(类)分为两方,即甲方、乙方。甲方依照契约向乙方发送任务,乙方有权利检查甲方的任务是否符合契约,不符合则拒绝执行。附上一篇相关博客:

     

     

  • 准确定义和表示方法的行为。如,在第一、第二单元中,架构设计较为复杂,类图只能描述架构的整体的框架,但细节到一些方法和规格,还是需要 JML 去进行准确定义、表示。

  • 提供了测试设计的依据。JML 提供了方法的前置条件和后置条件,可以作为测试正确性的依据之一。测试时,仅通过黑箱测试,很难保证测试数据的覆盖率。如,作业中,Group 中的人数有一个上限 1111,这处细节用随机生成的数据进行黑箱测试很难发现,需要单元测试的协助。

  • 设计与实现分离。从作业中也体会到了,课程组作为架构的设计者,我们作为架构的实现者。设计与实现相分离,这对我未来的程序编写也有很多启示,此外,我认为这一点在团队协作开发程序时也是较为重要的。

posted @ 2022-06-06 13:06  WIT23  阅读(17)  评论(0编辑  收藏  举报