OO_Unit3

第三单元总结博客

第三单元相比前两个单元,难度小了几个数量级,也有一种“这是人能写出来的代码”的感觉了,我也敢在周四再动手去写了,但代价就是中测一旦弱的离谱,强测就大寄。前两次作业强测40+50,想来是中测一遍过太过自信导致的。第三次作业开始WA掉了第八个点,de了两个bug以后就全A了,满心以为这回强测能挽回一点颜面,然后周日早上还在床上就迫不及待打开了oo网站,看到的是

听说第四个单元就没有互测了,最后一次互测就这么被我错过了,属实是“掐头去尾”了。好消息是,我也不用担心被一堆变态的反人类数据hack了。

分析在本单元自测过程中如何利用JML规格来准备测试数据

(我能说其实我没怎么测试嘛)还是要讲一点。曾经听说过一个名词叫“单元测试”,不过这个概念可能对前两个单元那种天马行空,想怎么写方法就怎么写方法不太适用,但对这个单元相当实用。因为各个方法之间耦合度不是很高,保证了每个方法都准确无误,基本这一堆代码的正确率就很可观了。一个很直观的例子,便是我在de第三次作业第八个点时,可以完全认为我前两次作业的新增功能是正确的,并且完全认为我测试过的方法是正确的,顺着新增功能,一个一个测试。比如我找出的bug在dce(delete cold emoji),在这一个操作以前要经历很多很多操作(ap,ar,aem,sei,sm等等)但本着“单元化测试”的原则,我可以认为前面的一系列操作完全正确,只可能在删除不常用表情这一步出错,而不会出现消息没发出去,或者表情压根没加进来等等奇奇怪怪的bug,事实证明我是dce时把int当成Interger用了。
针对每一个方法,JML还会给出种种分支,包括正常情况和异常情况,正常情况还有很多if-else,测试的时候要针对每一个分支进行测试。比如有一条atg(add to group),提出了一个要求:当人数满1111人时,就不能再进人了(灵感是否来源于人数长期爆满500的6系水群呢),听很多人说写的时候没有考虑到这一条,互测就被打烂了,这就是没考虑测试的全面性。
从我主观的角度来看,JML对新手其实是不太友好的(仅代表个人观点,老师助教看了轻喷555),比如第二次作业最恶心人的指令qlc(query least connect),用人类的语言表达就是“找到节点所在分支的最小生成树”,但看它的JML规格,我相信你会感受到绝望:

所以其实我压根没看这个规格根据方法名来猜这个方法是干嘛的还是很重要的,只要写接口的人不是把方法都命名成aaa,bbb,rnm的变态,都能猜个八九不离十了,再结合JML去看,就会舒服很多。另外,JML比较人性化的一点是将各个情况各个分支都会用换行符分开,测试的时候可以一个一个情况去测,就很容易做到测试全覆盖啦。

梳理本单元的架构设计,分析自己的图模型构建和维护策略

架构设计

简单整理了一下这次作业的UML图:

逻辑关系还是比较清晰的,容易理解的方式就是将这次作业的系统看作是微信app,其中每个人都是一个person,人和人之间可以添加好友,只有添加了好友才可以发消息,(两个人之间还有一个value的东西,在qq里类似亲密度,不过鉴于微信更常用就以微信举例子了),几个人还可以建群,群聊有人数上限(微信是500,题中给的是1111),人和人之间还可以发消息,消息包括:普通消息(即文字消息),红包消息(这个稍微离谱一点,即一个人的钱数可以是负数,想想有人为了给你发红包透支信用卡,这是多么千载难逢的友谊...)(还可以群发红包,不过属于“普通红包”,即每个人得到的钱数是平均的,发红包的人自己也可以抢)表情消息(懒得打字了就发表情包,但发表情之前需要先盗图收藏表情,每个表情还对应一个emojiId,区别自己的id,不同的消息emojiId可以相同,即你给一个人反复发同一个表情(刷屏狗必si),同时一段时间你一定想要清理一些自己不常用表情(但自从微信不限制表情个数,这一好习惯正在逐渐消失),将那些几年都用不上的表情删除,等等。)通知消息(假期还能看到期末考试的通知一定很恼人,于是将无聊的通知消息统统删去)结合平时在微信上那些五花八门的操作,种种方法就没那么难以理解了。最后,整个app都是一张大网,存储着各种信息,群聊,好友,这个NetWork专业来说,应该是腾讯的服务器。一旦这个服务器崩了(即NetWork没了),可能上一秒你还在和npy风花雪月,下一秒连人影都找不到...
不过具体还是以题目为主,微信只是辅助。比如发消息那个地方就比较反常:需要先将消息add进整个社交网,再发出去,发出去的同时需要删除,以及表情维护的是全局的,而不是个人的。这些还需要和常识加以区分。

图模型构建和维护

我翻开题目一查,这题目没有知识点,歪歪斜斜的每页上都写着“社交系统”几个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着两个字是“图论”!(本人原创)

第12次作业直接摊牌了,明确说这社交系统本质就是图(这让离散数学2学的一塌糊涂的我情何以堪)三次作业分别考察了三种算法来恶心人,以下分别来分析一下:

  • 并查集算法
    即isCircle,查找两个人能否通过有限的人脉资源连起来(简而言之就是我找到体育老师,体育老师找到中国足协,中国足协找到国际足协,国际足协找到法国足协,法国足协找到巴黎主席,明天我就能让梅西站到我面前的扯淡理论),这便是并查集算法。上网找了很多都不满意,干脆自己写。我维护了一个Arraylist(后来为了性能改成了哈希表),里面装的还是List,每个list就是一个并查集,当每个人刚加进来的时候,他只是一个个体,和谁都不认识,因此新建一个集合,当两个人建立了联系,就把其中一个集合的全部元素加到另一个里,并删除这个集合,就完成了并查集的合并。
      unionFindSet1.getMyPeople().addAll(unionFindSet2.getMyPeople());
      myNetwork.getUnionFindSets().remove(unionFindSet2);
    
    当查找isCircle时,直接查是不是在一个并查集就可以了。queryBlockSum更简单,直接返回并查集个数就可以了。
  • 最小生成树算法
    我开始采用的是Prim算法,从结点出发,找到一条边使得一个节点已经添加进来一个还没有添加,就一定不会产生回路。这个节点好解决,难就难在这个边怎么办。为此我新建了一个contact类,存储边的权值(即亲密度)和两侧的节点。(对边的权值还需要排序,所以实现了一个Comparable接口)
    public class Contact implements Comparable<Contact> {
      private final int id1;
      private final int id2;
      private final int value;
    
      public Contact(int id1, int id2, int value) {
          this.id1 = id1;
          this.id2 = id2;
          this.value = value;
      }
    
      @Override
      public int compareTo(Contact o) {
          return Integer.compare(this.value, o.getValue());
      }
    }
    
    因为Prim算法需要得到每一个节点所关联的边,所以每一个person还需要一个contact的容器,存放所有和自己相连的边。但事实上,我并没有采用prim算法,原因是这个算法太慢了,强测时间总被卡TLE。于是我换成了Kruskal算法,具体下一部分再讲。
  • 加权图的最短路径算法
    这个就无需多讲了,直接用Dijkstra算法。但暴力用dijkstra还不行,依旧会TLE(复杂度差不多是O(n^2)),于是用堆改进了一下,就能降到O(nlogn)。但算法还是Dijkstra算法,没人和老爷子抢名声。

按照作业分析代码实现出现的性能问题和修复情况

(好好的JML卡什么性能,不过想想也是,万一一个程序要100年才跑出来,程序员都去世了也跑不出个结果)(只是一点碎碎念)

  • 第一次作业
    第一次倒没什么性能问题,只是有一点我觉得有一些慢(事实证明第二次强测确实在这里出大问题),就是找到一个人对应的并查集,需要遍历每一个并查集中的每一个人,需要跑双重循环。改进方法也很简单,用一个Hashmap将人和并查集映射起来,ar时改变映射关系就可以(事实上,所有涉及查找相关的,你都可以无脑使用Hashmap,经常听说因为时间超了被TLE的,没听过空间开销太大爆了的,经典的空间换时间)
  • 第二次作业
    第二次的性能就有亿点点恼人了。首先是我彻底推翻了我的Prim算法,因为它实在是太慢了,取而代之的是Kruskal算法,从边出发。与此同时,我也无须维护每个人的Contact容器了。头疼的是,我没办法找到一种合理的方式判断图中是否带有回路(去年做数据结构最小生成树时,我就因为想不到怎么判断有没有回路,采用了Prim算法)。
    得益于第一次作业,我重新用了并查集算法。根据树的定义,将n个节点串起来需要至少n-1条边,那么如果出现了第n条边,就必然出现回路。借此办法,我在每个小分支中重新创建了并查集,思路类似,如果需要加入的边的两点在同一并查集中,添加就一定会产生回路,于是continue。
    还有一个恼火的指令是qgvs。用正常人的大脑去想,就是遍历每一个人,再遍历他们的好友列表,将value一个个相加。但偏偏有那么一群不正常的人发现这样的时间复杂度达到了O(n^2),数据一多必超时。于是只能将valueSum作为Group的一个属性,当dfg,atg和ar时再修改这个值,具体如下:
    if (myNetwork.getFindGroup().get(id1) != 0) {
              myNetwork.getGroup(myNetwork.getFindGroup().get(id1)).addValue(value);
    }
    if (myNetwork.getFindGroup().get(id2) != 0) {
        myNetwork.getGroup(myNetwork.getFindGroup().get(id2)).addValue(value);
    }
      
    
    public void addPeopleSum(Person person) {
          for (MyPerson acq: ((MyPerson)person).getAcquaintance()) {
              valueSum += person.queryValue(acq);
          }
      }
    
    public void subPeopleSum(Person person) {
        for (MyPerson acq: ((MyPerson)person).getAcquaintance()) {
            valueSum -= person.queryValue(acq);
        }
    }
    
  • 第三次作业
    第三次作业最短路径算法又被卡了,不多说直接上图了(搬运牛易明学长在讨论区的帖子)

JML拓展

假设出现了几种不同的Person
Advertiser:持续向外发送产品广告
Producer:产品生产商,通过Advertiser来销售产品
Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

前三种person均可以继承Person类,同时增加Product类,表示商品,每个product都有自己的id,并且advertiser有一定数量的product缓存。

  • 设置用户偏好
        /*@ public normal_behavior
        @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == Personid && people[i] instanceof Customer) &&
          (\exists int i; 0 <= i && i<= products.length;products[i].getId() == ProductId);
        @ assignable getPerson(personId).preferences;
        @ ensures (\forall Product i;\old(getPerson(PersonId).prefer(i));getPerson(PersonId).prefer(i));
        @ ensures getPerson(personId).prefer(ProductId);
        @ also
        @ public exceptional_behavior
        @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
        @          people[i].getId() == id && people[i] instanceof Customer);
        @ signals (ProductIdNotFoundException e) !(\exists int i; 0 <= i && i < products.length;
        @          products[i].getId() == ProductId);
        @*/
        public void setPreference(int PersonId, int ProductId) throw PersonIdNotFoundException, ProductIdNotFoundException;
    
  • 购买商品
        /*@ public normal_behavior
        @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == CustomerId && people[i] instanceof Customer) &&
          (\exists int i; 0 <= i && i<= people.length; people[i].getId() == AdvertiserId && people[i] instanceof Advertiser) &&
          (\exists int i; 0 <= i && i<= products.length;products[i].getId() == ProductId);
        @ ensures getPerson(CustomerId).money = \old(getPerson(customerId).mondy) - getProduct(ProductId).getPrice();
        @ ensures getPerson(AdvertiserId).getRemaining(ProductId) = \old(\getPerson(AdvertiserId).getRemaining(ProductId)) -1 ;
        @ also
        @ public exceptional_behavior
        @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
        @          people[i].getId() == id && people[i] instanceof CustomerId);
        @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
        @          people[i].getId() == id && people[i] instanceof AdvertiserId);
        @ signals (ProductIdNotFoundException e) !(\exists int i; 0 <= i && i < products.length;
        @          products[i].getId() == ProductId);
        @*/
        public void purchaseProduct(int AdvertiserId,int CustomerId,int ProductId) throw PersonIdNotFoundException,ProductIdNotFoundException;
    
  • 广告投放
        /*@ public normal_behavior
        @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == advertiserId && people[i] instanceof Advertiser) &&
          (\exists int i; 0 <= i && i<= messages.length;products[i].getId() == adMsgId && products[i] instanceof adMessage);
        @ assignable messages;
        @ ensures !containsMessage(adMsgId) && messages.length == \old(messages.length) - 1 &&
        @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != adMsgId;
        @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
        @ ensures (\forall int i; 0<= i && i < people.length; people[i] instanceof Customer ;
                  (\forall int j; 0 <= j && j < \old(getPerson(i).getMessages().size());
        @          getPerson(i).getMessages().get(j+1) == \old(getPerson(i).getMessages().get(i)) &&
        @          getPerson(i).getMessages().get(0) == \old(getMessage(adMsgId)) &&
        @          \old(getPerson(i).getMessages().size() == \old(getPerson(i).getMessages().size()) + 1));
        @ also
        @ public exceptional_behavior
        @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length;
        @          people[i].getId() == id && people[i] instanceof Advertiser);
        @*/
        public void castAdvertisement(int advertiserId,int adMsgId) throw PersonIdNotFoundException;
    

本单元学习体会

这个单元可以说难度是小了不少,至少没有像电梯月一样抓狂了,但强测分数也低了不少,甚至让我一度怀疑这中测是不是交了就是对的。但能力其实也就仅限于读一读JML规格语言了,真的让我自己写,还是会抓狂的。而且对于一些较为简单,但情况较多的情况,JML可以很严谨的表示出用户具体的需求是什么,但对于那些一两句话就能说清楚,但具体描述很复杂的情况,JML的实用性就值得推敲了。最好的做法是,将方法名,注释和JML结合起来看,注释起主要理解作用,JM来做补充,就不至于出现对着几篇JML头疼的情况了。

posted @ 2022-06-05 17:15  YiWforever  阅读(20)  评论(1编辑  收藏  举报