LCXKevin

面向对象设计与构造第三单元博客作业

一、测试与JML

JML规格的前置条件本就保证不重不漏,所以顺着JML规格的思路就可以构造覆盖性较好的数据,测试时赋予正常和异常以不同的权重,以正常运行为主,异常只要各分支覆盖到即可。

对拍的话由于本单元不像前两个单元,大家的输出结果在正确的情况下一定是相同的,找同学对拍而不是去自己写程序是最简单有效的方式了。

由于有了规格的限制,大家的结构与实现基本类似,大大降低了阅读代码的难度,所以这一单元的互测直接阅读代码不失为一种很好的方法。

二、架构设计与性能问题

基于规格的代码实现,只要正确理解规格,功能正确性上基本不会出问题,且整体架构跳脱不开课程组给的框架,至于说图模型构建和维护策略,则主要都是出于性能考虑,二者密不可分,所以本单元的主要问题在于性能,需要保证每一条指令的时间复杂度低于O(n^2)才能通过强测和互测。要实现这一目的主要有三种方法,选择合适的容器,维护变量保存计算结果,算法优化。

选择合适的容器

除第一次作业错误理解规格的实现,完全按照规格使用ArrayList外,后面的实现涉及到查询的均采用HashMap以id为key进行储存,将诸多查找的操作由O(n)降为O(1)。此外,还要根据特殊用途选择特殊的容器,比如使用PriorityQueue优化最小生成树和最短路径算法,也能降低操作的时间复杂度。

维护变量保存计算结果

维护变量可以使原本查询为O(n^2)的操作变为O(1),难点在于全面考虑什么时候需要更新变量。如第二次作业互测时qgvs由于复杂度过高出现了CTLE,采用的优化是在MyGroup中维护valueSum变量,除MyGroup中的ap和dp时需要更新外,MyNetwork中的ar时也需要进行更新。

算法优化

并查集

第一次作业中的qci和qbs,若不采用并查集,前者的时间复杂度为O(n),后者更是由于调用前者,时间复杂度可以达到O(n^3);而采用并查集后,则可以将前者降为接近O(1),后者则为O(n),效果显著。具体来说,在MyPerson中新增变量fa保存父节点,同时设置setter和getter,最后在MyNetwork中实现合并与查找即可。

private void merge(int id1, int id2) {
int f1 = getF(id1);
int f2 = getF(id2);
if (f1 != f2) {
((MyPerson) getPerson(f2)).setF(f1);
}
}

private int getF(int id) {
if (((MyPerson) getPerson(id)).getF() == id) {
return id;
}
int ans = getF(((MyPerson) getPerson(id)).getF());
((MyPerson) getPerson(id)).setF(ans);
return ans;
}

更进一步的话,可以实现路径压缩,让小的子树的根节点指向大的子树,降低查找的复杂度。

同时,使用并查集也可以优化Kruskal算法,方便判断是否成环。

Kruskal和Dijkstra的堆优化

两者实现过程中均采用PriorityQueue容器,方便寻找权值最小的边和与起点距离最小的节点。

PriorityQueue<Edge> pq = new PriorityQueue<>((o1, o2) -> {
if (o1.getValue() == o2.getValue()) {
return o1.getDes() - o2.getDes();
}
return o1.getValue() - o2.getValue();
});

PriorityQueue<Node> pq = new PriorityQueue<>((o1, o2) -> {
if (o1.getValue() == o2.getValue()) {
return o1.getId() - o2.getId();
}
return o1.getValue() - o2.getValue();
});

三、扩展任务

假设出现了几种不同的Person

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

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

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

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

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

  • advertise:

        /*@ public normal_behavior
        @ requires contains(id1) && getPerson(id1) instanceof Advertiser && containsMessage(id2);
        @ assignable people[*].messages;
        @ ensures (\forall int i; 0 <= i && i <= people.length &&
        @         people[i].isLinked(getPerson(id1)) && people[i].getId() != id1;
        @         \old(people[i].messages.length) == people[i].messages.length - 1);
        @ ensures (\forall int i; 0 <= i && i <= people.length &&
        @         !people[i].isLinked(getPerson(id1));
        @         \old(people[i].messages.length) == people[i].messages.length);
        @ ensures (\forall int i; 0 <= i && i <= people.length &&
        @         people[i].isLinked(getPerson(id1)) && people[i].getId() != id1;
        @         (\forall int j; 0 <= j && j <= \old(people[i].messages.length);
        @         (\exists int k; 0 <= k && k <= people[i].messages.length;
        @         \old(people[i].messages[j]) == people[i].messages[k])));
        @ ensures (\forall int i; 0 <= i && i <= people.length &&
        @         people[i].isLinked(getPerson(id1)) && people[i].getId() != id1;
        @         (\exists int k; 0 <= j && j <= people[i].messages.length;
        @         getPerson(id1).messages[j] == getMessage(id2)));
        @ also
        @ public exceptional_behavior
        @ signals (PersonIdNotFoundException e) !contains(id1) ||
        @         contains(id1) && !(getPerson(id1) instanceof Advertiser);
        @ signals (MessageIdNotFoundException e) contains(id1) && getPerson(id1) instanceof Advertiser &&
        @         !containsMessage(id2);
        @*/
      public void advertise(int id1, int id2) throws PersonIdNotFoundException, MessageIdNotFoundException;

    除了最基础的功能(id为id1的Advertiser将id为id2的ProductMessage发送给所有与之关联的人),还可以设置商品热度等属性的变化,还可以向Group发送,或者整合进sendMessage函数中。

  • produce:

        /*@ public normal_behavior
        @ requires contains(id1) && getPerson(id1) instanceof Producer;
        @ requires getPerson(id1).containsProduct(id2);
    @ ensures getPerson(id1).productList.length == \old(getPerson(id1).productList.length);
    @ ensures getPerson(id1).getProduct(id2).amount =
        @         \old(getPerson(id1).getProduct(id2).amount) + 1;
        @ also
        @ public normal_behavior  
    @ requires contains(id1) && getPerson(id1) instanceof Producer;
        @ requires !getPerson(id1).containsProduct(id2);
    @ ensures getPerson(id1).productList.length == \old(getPerson(id1).productList.length) + 1;
    @ ensures getPerson(id1).getProduct(id2).amount = 1;
        @*/
    public void produce(int id1, int id2);
  • purchase:

        /*@ public normal_behavior
        @ requires contains(id1) && getPerson(id1) instanceof Customer;
        @ requires containsProduct(productId);
        @ requires contains(id2) && getPerson(id2) instanceof Producer;
        @ requires getPerson(id2).getProduct(productId).amount > 0;
        @ ensures getPerson(id1).money = \old(getPerson(id1).money) - getProduct(productId).getValue;
        @ ensures getPerson(id2).money = \old(getPerson(id2).money) + getProduct(productId).getValue;
        @ ensures getPerson(id2).getProduct(productId).amount =
        @         \old(getPerson(id2).getProduct(productId).amount) - 1;
        @*/
      public void purchase(int id1, int id2, int productId);

     

四、心得体会

这一单元给人的感觉就是一个图论单元,重点在于实现规格的同时进行性能上的优化,尽管是基于规格去写代码,但相较于前两个单元,无非是将题目描述从自然语言换成了JML语言,理想中的样子是实验那样,不过考虑到作业要落脚到代码上,似乎也没有更好的方案了。

在JML规格的描述下,代码的需求变得十分明晰,不会像自然语言那样有二义性。这是JML让我感受到的最大的好处。除此之外,JML的书写和阅读都略显困难,对我而言,写JML比写代码难多了,这个难不在于设计,而是如何用规格表示想法。在正确的JML规格指示下,确实可以很好地实现代码,但是一旦规格错误,代码需求就不得而知了。考虑到JML的特点,写错JML反倒不是一件难事(bushi)。

当然,JML毕竟只是一种语言,我觉得更重要的不是精通JML,而是掌握基于规格实现代码的思想,写代码前根据需求生成自己的规格,写起来就会事半功倍。

posted on 2022-06-02 23:46  LCXKevin  阅读(36)  评论(0编辑  收藏  举报