OO第三单元总结

OO第三单元总结

1. 测试数据准备

1.1 如何利用JML规则

  • 在进行自测的过程中,JML规格可以帮助我们快速找出容易超时的函数。因为作业中给出的JML限制往往是采用循环遍历+函数嵌套调用的说明逻辑, 可以很容易的找出O(n^2)复杂度及以上的函数.

    • 例如:第二次作业的中的getValueSum()函数. 很显然的看到了两个循环遍历:\sum int i ...\sum int j ...所以,自测的时候应该专门构造含有大量qgvs指令的数据. 即:先构建一个组,向这一个组内加入大量的Person,然后紧跟着大量的qgvs,检测是否超时.
  • 重视JML规格中的反常识设定. 生成对应数据来检查实现的逻辑.反常识设定也包括对于一些及特殊情况的规定.

    • 反常识设定例如: addToGroup()指令最多只能加入1111Person,不注意到这点一定会被hack. 自测的时候也要加以用心.
      • 比如,虽然我在实现这个函数的时候注意到了1111这个限制条件,但最开始把"组内人数小于1111"误写成了"当前存在的组数小于1111",这个bug是在自测中才发现的.
    • 特殊情况规定,例如: Psrson中的isLinked()方法规定了与自己是默认链接的. queryValue()方法,规定对于不在acquaintance[]中的人结果返回0. 这两条行为直接影响着"查询最小生成数权值和"这个操作的逻辑. 因此,可以构造数据,查询单个节点最小生成数的权值, 检查实现逻辑是否正确.
  • 注意JML的精度问题. 这是我觉的第三单元作业最让我难受的一个点,明明有更高精度且更简洁的写法,却因为了精度问题导致写法变得更加复杂.这种问题如果懒得动脑子,只能构造大量数据进行测试.

    • 例如:计算方差时,最好的方法应该是平方的平均减去平均的平方,但为了精度问题,只能采用更麻烦的写法.

    • 例如:发送群发红包时,JML中是让发出者的红包直接变为最终数额.但其实还有一种更简单的写法,就是先让发出者的money减少红包的钱数, 然后再让组内的所有人(包括发出者)都获得对应份额的红包. 如果懒得从数学角度分析精度,那就要构造一些数据验证精度是否等同.

      • \old(getMessage(id)).getPerson1().getMoney() == \old(getMessage(id).getPerson1().getMoney()) - i *(\old(getMessage(id)).getGroup().getSize() - 1)
        

1.2 如何确定测试的重点

​ 在三次作业的迭代过程中, 实现的方法越来越多, 因此不可能每一次都对全部指令进行覆盖性测试(即便是自动化测试,随机生成数据也可能强度不够). 因此, 我们要想办法确定测试的重点方法.而确定测试重点主要有三个方法.

  1. 向同学和学长问易错点. 这个其实是最简单也最有效的方式
  2. 根据JML找测试重点. JML循环层数多的,要构造大量数据重点测试时间. JML规格特别冗长的, 应该多构造不同的边缘数据进行多人对拍测试.
  3. 对于之前作业中实现的函数,且当次作业没有变化的,就不需要多加测试,随机生成即可.

1.3 获取测试数据的方式

​ 对于自动化生成的数据, 方法数量一旦变多,对于某些特定指令的测试强度就会下降. 因此, 在写随机生成数据脚本的时候, 应该为每一类指令单独设置出现频率. 比如, 涉及group的指令,涉及Message的指令等设为多个类, 统一调节某些类别指令的出现频率,从而达到强测的目的.

2. 图模型构建和维护的策略

2.1 基本数据结构的维护

  1. 在这三次作业中,维护的重点结构就是路径压缩后的并查集. 并查集同时也可以便于我们找到某个连通分支内的所有节点.

    private final HashMap<Integer, ArrayList<Integer>> set = new HashMap<>();
    
  2. 基于这个并查集的父节点, 还可以维护联通分支的父节点到该连通分支内部所有边的映射. 这样在求最小生成树权值的时候,可以用O(1)的复杂度找到该连通分支内涉及道德所有边. 这样在使用kruskal的时候可以更加方便,减少遍历的次数

    private final HashMap<Integer, ArrayList<Edge>> block2Edges = new HashMap<>();
    
  3. 缓存与脏位. 对于所有会被查询到的数据,都可以进行缓存,并设置脏位维护. 比如,Person会被查询所在最小生成树的权值,那么如果他所在的连通分支没有加入边,脏位会始终为false, 这样下次查询的时候,会直接返回上一次缓存的leastConnection的值.

    #class MypERSON
    private int leastConnection;
    private boolean lcDirty;
    

2.2 图的构建策略

  1. 加边/加人的缓冲

    • 提升程序的局部性,不要每次加人或加边都去更新并查集等数据结构。

    • 不查询就不更新图

  2. 尽可能减少查找的复杂度

    • 大量的使用HashMap构建映射,避免使用ArrayList的查找. 因为一旦查找的复杂度达到了O(n)其在被调用函数中的复杂度很容易达到O(n^2)
    • 利用堆优化,便于在使用迪杰斯特拉算法时,查找权值最小的边

2.3 图的维护策略:非必要不计算,应缓存尽缓存,应更新尽更新

  • 非必要不计算:指如果一个值被计算过,且这次查询时,该值并没有发生改变,那么就不要在进行重复的计算.
  • 应缓存尽缓存:作业的测试数据中,必然含有大量的查询指令, 把之前计算过的值缓存起来,可以便于查询,减小时间消耗.
  • 应更新尽更新:比如,在计算最小生成树权值时,连通分支内部每一个节点的leastConnection值都应该被更新为最新计算的结果. 因为同一个联通分支内,最小生成树的权值只有一个.

3. 性能问题与修复情况

  • 因为提前听取了建议,知道本单元不好好写会TLE,因此设计之初就十分重视性能.因此本单元三次作业中均没有出现BUG.
  • 唯一的性能问题是:在第三次作业查询最短路径的指令中, 只缓存并更新了A->B的路径值, 而没有缓存B->A的路径权值, 导致程序可能变慢. 而且最短路径需要缓存的数据过多,在对拍小伙伴的电脑上出现过不可复现的堆溢出BUG(当然此时的数据规模其实远大于互测和公测的限制), 个人感觉这里的优化容易适得其反.
    • 解决办法:取消了对最短路径的数据缓存.

4. 模型拓展

  • 对于不同Person的拓展总体来说类似于Message的拓展. 大概需要实现下列类.

  • 顾客会偏爱某种Type的产品,每种不同Type的产品对应许多具体的Product,其具有独立的id

    class Product{
        private int id; // 每一个产品的唯一标识
        private int price; // 产品的价格
        private int type;// 表明是何种类型的产品
        private Producer producer;// 产品的生产者
        private int status; //产品的状态
    }
    class Person{
        private int money;
    }
    class Producer extends Person{
        private Product[] produced;
        public Product makeProduct(){}
    }
    class Advertiser extends Person{
        public void sendAdvertisement(){}
    }
    class Customer extends Person{
        private int typePreference; //偏爱某种类型的产品,受到对应的广告后会购买
        public void recvAdvertisement(){}
    }
    class purchaseMsg extends Message{
        
    }
    class advertiseMsg extends Message{
        
    }
    class produceMsg extends Message{
        
    }
    
  • 查询某种类型产品的销售额:位于Network接口

    /*@ public normal_behavior
      @ requires (\exists int i; 0 <= i && i < products.length; groups[i].getType() == productType);
      @ ensures \result == (\sum int i; 0 <= i && i < products.length && 
      @ 		products[i].getType ==  productType && products[i].getStatus == Status.SOLD; 
      @ 		products[i].getPrice());
      @ also
      @ public exceptional_behavior
      @ signals (ProductTypeNotFoundException e) !(\exists int i; 0 <= i && i < products.length;
      @                                     		products[i].getType ==  productType);
    */
    public /*@ pure @*/ int queryProductSales(int productType) throws ProductTypeNotFoundException;
    
  • 生产某个产品: 位于Network接口

/*@ public normal_behavior
  @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == personId && 
  @							people[i] instanceof Producer);
  @ requires !(\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
  @ assignable products;
  @ ensures newProduct.getType()==type && newProduct.getId()==id && newProduct.getProducerId==personId
  @ ensures products.length == \old(products.length) + 1;
  @ ensures (\forall int i; 0 <= i && i < \old(products.length);
  @          (\exists int j; 0 <= j && j < products.length; products[j].equals(\old(products[i]))));
  @ ensures (\exists int i; 0 <= i && i < products.length; products[i].equals(newProduct));
  @ also
  @ public exceptional_behavior
  @ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length; 
  @										 	products[i].getId() == id)
  @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; 
  @						people[i].getId() == personId && people[i].getType == personType.PRODUCER);
  */
public void makeProduct(int type, int id, int personId) throws EqualProductIdException, PersonIdNotFoundException;
  • 查询销售路径:
/*@ public normal_behavior
  @ requires (\exists int i; 0 <= i && i < products.length; groups[i].getId() == productId);
  @	ensures (Person[] array; array.length == 3; 
  @ 		array[0].equals(getPerson(getProduct(productId.getProducer()))) &&
  @         array[1] instancef Advertiser && array[2] instanceof Customer &&
  @         (\forall int i; 0 <= i && i < array.length - 1; array[i].isLinked(array[i + 1]) == true)));
  @ ensures \results == array;
  @ also
  @ public exceptional_behavior
  @ signals (ProductIdNotFoundException e) !(\exists int i; 0 <= i && i < products.length;
  @                                     		products[i].getId ==  productId);
*/
public /*@ pure @*/ int querySalePath(int productId) throws ProductIdNotFoundException;

5. 感受

​ 第三单元应该是最简单的一个单元了. 难度比第一第二单元低, 题目也不想第四单元谜语人. JML清晰明了, 只需要做好优化就能轻松过关. 唯一有点难的就是实验写JML, 这玩意写的真的眼晕, 而且其实语法之类的也不是绝对规范, 可以有不同表述.

posted @ 2022-06-01 19:50  IIlIllIIlII  阅读(21)  评论(2编辑  收藏  举报