OO第三单元总结

OO第三单元总结

图模型架构

基础元素

  • 顶点的维护:使用hashmap<Integer, Person>,以id为key实现O(1)查询

  • 边的维护:使用HashMap<Edge, Integer>,其中Edge是边,知道两个顶点时可以实现O(1)查询

  • 并查集维护:首先在内部维护一个变量按加入顺序给person编号,编号代表顶点在根数组中的位置,根数组存的是父节点(该节点不是根节点)或者是秩的负数(该节点是根节点),合并的过程中按秩合并,并且使用路径压缩。其中根节点储存的是秩的负数是在《数据结构与算法分析》一书中所见,感觉是一个比较好的想法,既能判断该节点是不是根节点,也能节省额外储存秩的空间。

    private void union1(int id1, int id2) {
           int root1 = find1(flag.get(id1));
           int root2 = find1(flag.get(id2));
           if (root1 == root2) {
               return;
          }
           if (troots[root1] < troots[root2]) {
               troots[root2] = root1;                                          //按高度求并
          } else if (troots[root1] > troots[root2]) {
               troots[root1] = root2;
          } else {
               troots[root1] = root2;
               troots[root2]--;
          }
      }

复杂指令

  • qci:查询两个顶点是否在同一集合中,并查集维护正确即可,时间复杂度为O(1)

  • qlc: 查询包含特定顶点的最小生成树。最小生成树主要有两个算法,Prim算法和Kruskal算法。其中Prim算法主要采用堆优化找到最短的割边,优化后的时间复杂度为O(ElogV),适合于稠密图。Kruskal算法可以采用并查集优化查找两个顶点是否同时加入了最小生成树,堆优化可以快速找到当前最小的边,优化后的时间复杂度为O(|Elog|E|)。在课下测试时,发现以强/互测的数据规模,Kruskal的表现更优,所以我选择的是Kruskal算法。

  • sim:查询最小加权路径主要采用的是使用堆优化的Dijkstra算法,时间复杂度为O(ElogV )

动态维护

本次作业互测主要的hack点就在于hack没有动态维护的O(n2)的算法,所以实现时这是一个很需要注意的问题。

  • qbs:添加person时集合的数量加1,合并时集合的数量减1,即可实现O(1)查询

  • qgvs:在atg和ar时增加边值,在dfg时减少边值,避免使用jml规格上的方法

  • qgav:需要注意的是平均年龄只需要查询一次,否则该算法会编程O(n2)

数据构造与测试

本单元由于实现比较简单,只要读懂了jml利用一些基础的图算法就能完成,所以大量时间都放在了测试上。

数据构造思路

第三单元的数据大体上可以分为增加信息类型的数据(例如ap、ar、am等),以及发送、查询类型的数据(qv、qbs等),为了能够正确的查询信息,避免异常,所以需要将已经增加的信息存在数据生成器内部的容器当中,然后针对性的生成查询,发送消息的数据。

添加类型

public String getMessage() {
       String name;
       int id;
       int age;
       name = getName();
       id = random.nextInt(500000);
       while (idSet.contains(id)) {
           id = random.nextInt(500000);
      }
       idSet.add(id);
       age = random.nextInt(201);
       return String.format("ap %d %s %d", id, name, age);
  }

以ap指令为例,利用HashSet保存已经增加的personId信息,避免生成相同id的ap指令,同理,ag、am等指令也将groupId等信息存储。

查询类型

@Override
   public String getMessage() {
       if (linkSet.size() >= idSet.size() * (idSet.size() - 1) * 2) {
           return null;
      }
       ArrayList<Integer> list = new ArrayList<>(idSet);
       int id1 = list.get(random.nextInt(list.size()));
       int id2 = list.get(random.nextInt(list.size()));
       while ((id1 == id2)||hasLink(id1, id2)) {
           id1 = list.get(random.nextInt(list.size()));
           id2 = list.get(random.nextInt(list.size()));
      }
       add(id1, id2);
       int value = random.nextInt(1001);
       return String.format("ar %d %d %d",id1, id2, value);
  }

查询以ar为例,因为该指令既要使用已经存入的id(本质上也是一种查询),也会存入新的信息(person的关系),封装了一个类来判断,本质上是一个无序的二元组,避免重复添加边。

public Link(int id1, int id2) {
       this.id1 = id1;
       this.id2 = id2;
  }

   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Link link = (Link) o;
       return id1 == link.id1 && id2 == link.id2;
  }

异常类型

由于已经存了所有的信息,所以产生异常的信息也比较简单,例如PersonIdNotFoundException异常只要查询一个idSet中没有的id即可。需要注意的点在于,不能简单的全部产生异常指令,需要一些梯度非异常指令,再逐步产生异常指令,才能更好的保证测试的有效性,和异常的覆盖性。

覆盖率保证

覆盖率的保证主要是理清楚各条指令的关系,然后将可能相互产生影响的指令交互,例如message有关的指令(am、arem、anm、aem、sim、sm)可以放在一起测试,ar、qv等指令也可以放在一起测试。

本次作业比较遗憾的一个点在于,由于时间关系,没有做很细致的覆盖率分析,和系统化的分类标准,主要是评感觉保证覆盖性。

自动化脚本

    os.system("java -jar DataGen-uml.jar")
   os.system('javac -encoding UTF-8 -cp {0} $(find . -name "*.java")'.format(lib))
   for j in range(first, last):
       filein = open("{1}data{0}.txt".format(j, back), "r")
       fileout = open("A{0}.txt".format(j), "w")
       time2 = time.time()
       p = subprocess.Popen(
           'java {0}.java'.format(main),
           shell=True,
           stdin=filein,
           stdout=fileout,
           encoding="utf-8"
      )
       p.wait(10)
       time2 = time.time() - time2

 

本次测试以对拍为主,在写对拍之前,观看了其他大佬的自动化测试脚本,得出了一些经验。

  • java编译:直接使用java编译产生可执行文件,省去打包的过程。

  • log:采用日志方式,既可以避免短路,也可以准确定位出错的位置。

  • subprocess:该模块的Popen()函数用起来非常方便,可以指定输入输出管道,可以控制运行时间等等。

性能分析和Hack策略

个人分析

本次作业的性能尚可,大部分可以优化的点都优化了,所以性能上没有出现太大问题。唯一一个在互测中被hack的点是一个懒删除的问题,在实现dce时使用了懒删除,但是没有考虑到一个消息删除之后可能会以同样的id重新添加进来,所以出了问题。其他地方均没有发现bug。

他人bug

但是在互测中发现,本单元虽然比较容易实现正确,但是在性能上很容易出现问题。三次作业中均有hack成功,去掉同质bug共计6刀。

  • qbs没有实现动态查询:出现这个问题的同学均是按照jml实现,而没有动态维护集合的数量。

  • **qgvs没有实现动态查询:**这个问题在前问分析qgvs已经提到过,主要是出现了O(n2)的算法

  • qgvs使用缓存:这个实在阅读代码过程当中发现的,该同学使用了缓存来实现qgvs查询,本质上还是一个复杂度为O(n2)的查询,只需要在每次查询之后更新信息再次查询即可。另外在这个点中还发现了疑似java编译时优化,如果只在group中添加person而完全没有ar,即使是O(n2)的算法cpu时间也非常短,而只要稍微增加少量arcpu时间就会大幅度增加,以达成hack的目的。猜想是由于java虚拟机进行了一些分支预测优化。

  • qgav:该指令也提到过了,本质上还是一个复杂度为O(n2)的查询。

Network扩展

新增三个AdvertiserCustomer继承PersonProducer继承了Customer

其中Producer内部维护了一个商品信息(以String表示),以及生成该商品所需要的其他商品,所以可以查询某个商品的生产链。

新增AdvertiseMessageTrdingMessage来表示广告和销售,并且继承Message

完成业务逻辑的核心方法jml如下

/*    @ public normal_behavior
     @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
     @ &&@(getMessage(id) instanceof RedEnvelopeMessage)       getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
     @         getMessage(id).getPerson1() != getMessage(id).getPerson2();
     @ assignable messages;
     @ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
     @ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue;
     @ 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().getSocialValue() ==
     @         \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
     @         \old(getMessage(id)).getPerson2().getSocialValue() ==
     @         \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
     @ ensures \old(getMessage(id).getPerson2().canTrading(getMessage(id).getProduct()))==true;
     @ 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;
     @ also
     @ public normal_behavior
     @ requires containsMessage(id) && getMessage(id).getType() == 1 &&
     @ (getMessage(id)) instanceof RedEnvelopeMessage &&         getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1());
     @ assignable people[*].socialValue, 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 Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==
     @         \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue()) && p.getMessage(id).getProduct() == true;
     @ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
     @         \old(people[i].getSocialValue()) == people[i].getSocialValue());
     @ also
     @ public exceptional_behavior
     @ signals (MessageIdNotFoundException e) !containsMessage(id);
     @ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
     @         !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
     @ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
     @         !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
     @*/
void sendAdvertise(int id) throws
           RelationNotFoundException, AdvertiseMessageIdNotFoundException, PersonIdNotFoundException;
/*@ public normal_behavior
     @ requires contains(id) && getPerson(id) instanceof Producer;
     @ ensures \result == getPerson(id).getProductPath();
     @ also
     @ public exceptional_behavior
     @ signals (producerIdNotFoundException e) !contains(id);
     @*/
List<String> queryProductPath(int producerId);
als (producerIdNotFoundException e) !contains(id);
/*@ public normal_behavior
     @ requires !(\exists int i; 0 <= i && i < produtIdList.length; produtIdList[i] == id);
     @ assignable produtIdList;
     @ ensures (\exists int i; 0 <= i && i < productIdList.length; productIdList[i] == id);
     @ ensures productIdList.length == \old(productIdList.length) + 1;
     @ ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
     @         (\exists int j; 0 <= j && j < productIdList.length; productIdList[j] == \old(productIdList[i]);
     @ also
     @ public exceptional_behavior
     @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < emojiIdList.length;
     @                                     productIdList[i] == id);
     @*/
   public void storeProductId(String id) throws EqualProductIdException;

单元学习体会

不足之处

  • 数据构造有所疏漏。由于从上学期计组课程开始大量写数据生成器,写到本单元的时候已经有所疲惫,所以这个单元构造数据出现了没有考虑到的情况,导致在互测中被hack了一次。之后希望通过构建数据分析工具来全面完善的分析覆盖率。

  • 数据自动分析能力不足。之前构造数据出现问题之后都是人为分析错误,之后可以根据对数据的理解以及可能出现的bug尽量在对拍之后有一个完善的分析环节。

能力提升

  • 阅读jml的能力得到提升,设计更具规范化。

  • 算法能力提升,由于长时间没有写纯算法的题,所以导致许多算法及数据结构的知识有所忘记,本单元正好使我重新回顾了很多算法知识。

  • 脚本能力提升,本单元直接使用java编译,并且使用了一些linux命令行的方法完善了自动化测试脚本。

posted @ 2022-06-06 14:34  shliba  阅读(46)  评论(0编辑  收藏  举报