北航2022OO第三单元博客作业
如何自测
自测的要求
以往的OO作业中,作为作业的程序和自测程序都会得到答案。不同在于,我们写的作业所得答案是不保证正确的、有待验证的;而自测程序得到的答案是生成数据时的某些参数,或是利用现有可靠的工具得到答案,或者是检测我们所得答案的上下逻辑是否正确,是完全的另一套逻辑,并且正确性很有保证。所以我们可以自己写一套完整的自测程序,它以完全不同于作业代码的逻辑来判断我们的答案是否正确,避免陷入一种逻辑的bug里。
但本单元难以找到另一种逻辑来检测我们答案的正误。如果要生成数据时获得答案,那么其获得答案的逻辑与作业代码是相近的,相当于用代码A的结果检查代码A的结果,是徒劳;如果是利用现有可靠的解析工具来获取答案,可惜暂时没找到;如果是检测作业答案的上下逻辑,可惜本次作业的答案并无上下逻辑的关联,全程是对静态状态的查询……
既然不能从一个人的两种逻辑出发验证,那么可以考虑多个人的一种逻辑进行对拍。当对拍人数较多时,辅以大量的测试数据,可以容易地查出各参与对拍者的bug。于是本次作业的自测只需要生成有强度的数据,然后几人同时跑结果并对比即可。感谢郭鸿宇同学为对拍提供的支持。
构造数据
生成数据,首要看的是数据限制。因为在不要求性能的情况下,只要满足正确性和某些限制即可,所以没有必要处处吹毛求疵。而在JML语言准确、多人对拍的情况下,不考虑个人偶然的因未理解JML造成的逻辑上的错误(这样的错误很容易在大量对拍中发现),即正确性可以无忧。于是要考虑是否可能违背某些限制,如超时、溢出等。
第一次作业
指令数不多于1000条,那么某一个方法只要其复杂度不是O(n2)则不会出现超时的情况。在完全正确实现JML描述的情况下,本次作业有两个操作有可能造成超时。
一,查询两个元素是否连通。这个操作使用优化或不优化的并查集都可以轻松处理,但有的同学使用搜索。使用搜索固然没问题,可以做到O(n)的时间复杂度,但是不排除有同学处理环不恰当导致陷入循环无法退出,因此可以构造带环的数据。
二,查询有多少个连通分量。最坏最坏的情况是未优化的并查集并且每次查询遍历所有节点,进行getFa操作。有可能达到O(n2)。可以构造使得并查集经常进行递归查询的数据。
至于溢出问题,由于JML几乎处处都是使用的int规定,并且检查各个方法的中间过程,几乎确保不会出现。
总体来说第一次作业没有多少找毛病的地方。
第二次作业
指令条数不多于10000/5000,于是即使是O(n)的复杂度都不算稳妥,对时间复杂度的要求更高了。同时,也给溢出问题提供了存在空间。当然,得益于对数据的其它限制,只要稍加留意,便不会超时。由于JML的规格依然都是int,所以只要留意中间过程即可避免溢出。在完全正确实现JML描述的情况下,本次作业有以下几个地方可能出问题。
一,求一个最小生成树。比较经典的是Kruskal和Prim算法,这俩都需要加点优化,加了优化即可保证即使针对性地造数据也能在10s内跑完。不加优化的话随便造一个图都可以TLE。
二,query_group_value_sum是一个隐藏的坑。如果按照JML描述实现该方法,可以针对性地构造出数据造成TLE。在JML描述中,一个group最多1111人,它求sum的方式是两层枚举,对人枚举。如果构造互测数据,最多5000条,第1条增加一个group,然后增加1111人并都加入该group,剩下的2777条全部用于查询,那么就是1111*1111*2777的运算。经过实测,按照JML实现的代码跑此数据需要1min以上。
第三次作业
依然是10000/5000的数据规模,在完全正确实现JML描述的情况下,仍然最需要考虑溢出与超时的问题。前者仍然由于JML处处使用int型而不需过于担忧,只需关注中间过程即可。应主要关注后者。在本次作业中,新出现了以下的超时可能:
一,send_indirect_message,本质上是求最短路。对于使用dijkstra算法而不用堆优化的同学,可以类似第二次作业,构造数据使得 n2 * 查询数m 最大。经过实测,如此构造的数据会使未用堆优化的dijkstra算法跑12s左右。由于SFPA算法“风评”慢于dijkstra且理解上比dijkstra复杂,故认为几乎不会有人使用SFPA。
构建与维护
构建
只需要按照官方包的要求实现类即可。只要严格按照JML规格实现,必定保证正确,难度并不大。但出于对效率的考虑,也需要一些抽象的理解。比如对于JML描述中“数组”的抽象理解——它们并不一定仅是一个普通的数组,只需结果与JML描述等效即可。
比如JML中“存在”之类的描述,难道一定要for循环遍历数组找到它吗?通过使用HashMap可以快速完成。比如存在一种“前后连接”关系,难道找到对应的元素一定要依次安排在一个一维数组里吗?通过自行建立“边”类,可以使用图论算法解决问题。再比如某些元素均在某个单独的数组中,这个单独的数组甚至都不需要——并查集可以多次等效地完成工作。
此外,为了使代码易读易理解,尽可能把方法的子功能“下放”给成员,确保每个操作在每一级的简洁直观。
但同时要确保“决定权”握在本类手中,即对于是否要执行子功能,要在本类决定;成员默认条件符合要求,只管义无反顾地执行任务,不需要决定是否执行任务。这样每一级的逻辑比较完整。
对于每个方法,严格按照JML描述详细地列出各种if...else if条件。
维护
首要是对于所有上一次作业实现的方法,逐一对照最新的JML规格或数据规模,检查是否有变动之处。一些隐藏的问题,比如JML规格未变,但数据规模增加,导致中间过程可能溢出,需要修改具体实现方法;再比如对某些方法自行抽象出来的实现方式,也许在新的规格下不再满足要求等。
对于新增的内容,遵照构建的思路即可。
性能问题与修复情况
性能问题
此处默认“性能”是指正确性可以保证,仅看程序运行时间。
由于没有性能分,且时间限制很宽(基本在10s以上),所以不必吹毛求疵。一些可能的超时情况已经在上面分析过,不再赘述。
修复情况
暂没有出现过性能问题。但是协助同学修复了一些可能超时的情况。
扩展
对Network的扩展
设定
1,新增一个类Product,表示产品。它有属性productId。
2,如果一个Producer拥有一个Product类成员,表明它可以无限地供应productId一类的产品,但该产品的售价不同Producer不一定相同。
Advertiser的操作
默认Advertiser继承自Person,并且成员包含若干Producer。Advertiser可能的操作有:
1,向所有Person类对象发出广告消息。
2,接收来自Producer的广告信息。
3,接收来自Customer的购买消息,处理后又传递给相应的Producer一个购买消息。
4,为自己增加一个Producer。从此该Producer的销售信息可以传递给自己。
Producer的操作
Producer也应该继承自Person,成员包含若干Advertiser。Producer可能的操作有:
1,增加一种产品供应。该操作是销售的前提。
2,为自己增加一个Advertiser。
3,销售一件产品。指定若干Advertiser,向他们发送销售消息。
4,接收Advertiser反馈的购买消息。
5,根据购买信息,主动与Customer联系处理购买。
6,统计某种商品的销售额。由于购买是由Producer亲自处理的,所以可以方便地统计。
7,统计某种商品的销售信息发给了哪些Advertiser。
8,接收Customer的购买消息。
Customer的操作
Customer继承自Person。Customer可能的操作有:
1,增加一条需求。接收后在所有广告消息中寻找符合需求的内容,有则发送相应购买消息。
2,当接收到一条广告消息时,除了加入消息集合,还要遍历需求集合,寻找是否满足某个需求,有则发送相应购买消息。
3,增加一个偏好。
Person操作
不变。
三个核心业务功能实现
1,getSaleValue(int personId, int productId)。用于查询指定生产商的指定类别商品的销售额。这里设定新增一个异常ProductIdNotFoundException;Nerwork类新增一个数组productIdList[ ]存放所有种类Product的id;并且Producer有一个方法querySaleVleue可以返回指定产品的销售额,如果该Producer不供应该产品,则返回值应该为0。
1 /*@ public normal_behavior 2 @ requires contains(personId) && getPerson(personId) instanceof Producer && 3 @ (\exists int i; 0 <= i && i < productIdList.length; productIdList[i] == productId); 4 @ ensures \result == ((Producer) getPerson(personId)).querySaleValue(productId); 5 @ also 6 @ public exceptional_behavior 7 @ signals (PersonIdNotFoundException e) !contains(personId) || 8 @ (contains(personId) && !(getPerson(personId) instanceof Producer)); 9 @ signals (ProductIdNotFoundException e) contains(personId) && !(getPerson(personId) instanceof Producer) && 10 @ !(\exists int i; 0 <= i && i < productIdList.length; productIdList[i] == productId); 11 @*/ 12 public /*@ pure @*/ int getSaleValue(int personId, int productId) 13 throws PersonIdNotFoundException, ProductIdNotFoundException;
2,sendAdvertisementMessage(int personId, int messageId)。让广告商向全体Person类对象发送一个广告消息。设定新增一个AdvertisementMessage类,继承自Message类。
1 /*@ public normal_behavior 2 @ requires contains(personId) && getPerson(personId) instanceof Advertiser && 3 @ containsMessage(messageId) && getMessage(messageId) instanceof AdvertisementMessage; 4 @ assignable people[*].messages; 5 @ ensures (\forall int i; 0 <= i < people.length; 6 @ \old(people[i].getMessages().size) + 1 == people[i].getMessages().size() && 7 @ (\forall int j; 0 <= j < \old(people[i].getMessages().size()); 8 @ \old(people[i].getMessages()).get(i) == people[i].getMessages().get(i+1)) && 9 @ people[i].getMessages().get(0).equals(getMessage(messageId))); 10 @ also 11 @ public exceptional_behavior 12 @ signals (PersonIdNotFoundException e) !contains(personId) || 13 @ (contains(personId) && !(getPerson(personId) instanceof Advertiser)); 14 @ signals (ProductIdNotFoundException e) contains(personId) && getPerson(personId) instanceof Advertiser && 15 @ (!containsMessage(messageId) || 16 @ (containsMessage(messageId) && !(getMessage(messageId) instanceof AdvertisementMessage))); 17 @*/ 18 public int sendAdvertisementMessage(int personId, int messageId) 19 throws PersonIdNotFoundException, MessageIdNotFoundException;
3,setPreference(int personId, int productId)。指定消费者,令其当前偏好为指定商品。设定Customer的成员变量包含preference,表示目前该消费者需要的商品。
1 /*@ public normal_behavior 2 @ requires contains(personId) && getPerson(personId) instanceof Customer && 3 @ (\exists int i; 0 <= i && i < productIdList.length; productIdList[i] == productId); 4 @ assignable ((Customer) getPerson(personId)).preference; 5 @ ensures ((Customer) getPerson(personId)).preference == productId; 6 @ also 7 @ public exceptional_behavior 8 @ signals (PersonIdNotFoundException e) !contains(personId) || 9 @ (contains(personId) && !(getPerson(personId) instanceof Customer)); 10 @ signals (ProductIdNotFoundException e) contains(personId) && getPerson(personId) instanceof Customer && 11 @ !(\exists int i; 0 <= i && i < productIdList.length; productIdList[i] == productId); 12 @*/ 13 public int setPreference(int personId, int productId) 14 throws PersonIdNotFoundException, ProductIdNotFoundException;
本单元学习体会
通过本单元学习,初步理解了契约式编程,它在于系统中元素(类、方法等)之间的协作、功能等的确定与划分。今后的任务中,也可以采用契约的思想,使用规格化的语言确定协作与任务,提高效率。
然而,规格化的语言并不代表单一的实现方式。规格只是目的,我们仍应该在种种限制下调整自己的实现方式。
此外,本单元的自测并不很容易,主要在于难以仅靠自己检验答案正误。比如第一单元,一个逻辑是自己的作业代码,一个逻辑(或者说不同的途径)是Python自带的库化简,两者可以对比。第二单元,只需要写一个程序,将输出答案按照输入的顺序和相关限制检查即可判断正误。而本单元作业,只能在生成数据时便获取答案,但这一过程的逻辑与作业代码完全一致,即处理输入/生成数据,得到结果/生成答案,相当于“自己测自己”,显然是无用功。唯一的方法是多人间对拍,虽然可能出现多人犯同一个错误的情况,但已经是较好的方法。
感谢老师与助教们的辛勤付出以及为对拍提供支持的各位大佬。