BUAA_OO Unit3 单元总结

BUAA-OO Unit3单元总结

JML简介

JML(Java Modeling Language) 是用于对 Java 程序进行规格化设计的一种表示语言。

JML规格框架:

  1. requires 子句定义该方法的前置条件(pre-condition);
  2. 副作用范围限定, assignable 列出这个方法能够修改的类成员属性, \nothing 是个关键词,表示这个方法不对任何成员属性进行修改。
  3. ensures 子句定义了后置条件。

原子表达式:

  1. \result 表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
  2. \old(expr) 表达式:用来表示一个表达式 expr 在相应方法执行前的取值。
  3. \not_assigned(x,y,...) 表达式:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为 true ,否则返回 false 。实际上,该表达式主要用于后置条件的约束表示上,即限制一个方法的实现不能对列表中的变量进行赋值。

量化表达式:

  1. \forall 表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
  2. \exists 表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
  3. \sum 表达式:返回给定范围内的表达式的和。
  4. \product 表达式:返回给定范围内的表达式的连乘结果。
  5. \max 表达式:返回给定范围内的表达式的最大值。
  6. \min 表达式:返回给定范围内的表达式的最小值。
  7. \num_of 表达式:返回指定变量中满足相应条件的取值个数。

集合表达式

集合构造表达式。

操作符

  1. 子类型关系操作符: E1<:E2 ,如果类型 E1 是类型 E2 的子类型 (sub type),则该表达式的结果为真,否则为假。如果 E1 和 E2 是相同的类型,该表达式的结果也为真;
  2. 等价关系操作符: b_expr1<==>b_expr2 或者 b_expr1<=!=>b_expr2 ,其中 b_expr1 和b_expr2 都是布尔表达式;
  3. 推理操作符: b_expr1= =>b_expr2 或者 b_expr2<= =b_expr1 。

第一次作业

作业简介

实现 person 类和简单社交关系的模拟和查询,学习目标为 JML 规格入门级的理解和代码实现

容器选择

Person:

观察JML规格,发现accquaintanceidvalue是一一对应的,因此可以用HahsMap来存储键值对。

Network:

考虑到在之后的方法中需要对Person进行频繁查找,因此采用<PersonId,Person>的键值对来存储。

实现思路

第一次作业较为简单,直接按照JML规格写出对应方法即可。

度量分析

在原始版本中,如果直接按照JML规格实现,那么isCirclequeryBlockSum方法占用了较高的时间复杂度(我记不清了但是大概有O(n2)或者O(n3)的样子);其他方法的复杂度正常。

bug分析

本次作业中中测和强测中没有出现Bug,互测中由于上述时间复杂度问题被hack了三刀(%……&%……*%¥#,我是真没想到第一次作业都要Hack算法淦,只能说要保持警惕啊)。

所以后来为了降低复杂度,用并查集重新写了一遍这俩方法,具体实现为:

在Network中,实现HashMap<id,father_id>存储每个用户和其祖先的id。在addPerson方法中,新增id对应的键值对,将father_id设为自己。在addRelation方法中,合并两个人的father_id。另实现getRoot方法,路径压缩+返回祖先。在isCircle方法中,对两个id分别find出祖先,以判断二人是否连通。

getRoot方法实现如下:

public int getRoot(int id) {
    if (tree.get(id) == id) {
        return id;
    } else {
        tree.put(id, getRoot(getRoot(tree.get(id))));
        return tree.get(id);
    }
}

第二次作业

作业简介

本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。

容器选择

Person:

新增socialValue和messages,前者直接纳入,后者选择使用ArrayList存储。

Network:

新增的group和messages都用HashMap存储。

Gruop:

未变。

Message:

不需要容器。

实现思路

本次作业新增加了Group类和Message类,Message类的方法较为简单,直接按照JML规格即可,而Group类中的getAgeMean,getAgeVargetValueSum几个方法需要注意,(吸取了上次翻车的经验发现)直接按照JML又会出现O(n^2)以上的负责度,因此需要更改,具体方法为:

//在类中增加属性便于全局计数
private int valueSum;
private int ageSum;

//在addPerson时,更新valueSum和ageSum
@Override
public void addPerson(Person person) {
    people.put(person.getId(), (MyPerson) person);
    ageSum += person.getAge();
    for (MyPerson p : people.values()) {
        // renew valueSum
        if (p.isLinked(person)) {
            valueSum += (p.queryValue(person)) * 2;
        }
    }
}

//同理,deletePerson时也需要更新
@Override
public void delPerson(Person person) {
    people.remove(person.getId());
    ageSum -= person.getAge();
    for (MyPerson p : people.values()) {
        // renew valueSum
        if (p.isLinked(person)) {
            valueSum -= (p.queryValue(person)) * 2;
        }
    }
}

//有前述内容铺垫的基础上,这三个方法的复杂度即可被压缩
@Override
public int getValueSum() {
    return valueSum;
}

@Override
public int getAgeMean() {
    if (getSize() == 0) {
        return 0;
    }
    return ageSum / getSize();
}

@Override
public int getAgeVar() {
    int ageVar = 0;
    int size = getSize();
    if (size == 0) {
        return 0;
    }
    for (MyPerson p : people.values()) {
        ageVar += (p.getAge() - getAgeMean()) * (p.getAge() - getAgeMean());
    }
    return ageVar / size;
}

记得addPerson和deletePerson两个方法中都要更新这两个属性。

度量分析

由于把valueSum和ageSum在全局中更新而不是每次调用时遍历,因此方法整体的时间复杂度较为合理。

bug分析

本次作业强测大翻车,问题在于看错了JML的规格:

//JML规格如下:
/*@ ensures \result == (people.length == 0? 0 :
@          ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length));
@*/
public /*@pure@*/ int getAgeMean();

可以发现,按照括号的层次,应该是先对people中的age进行求和,再除以people的长度;而如果先除以people的长度再求和,则可能因为int/int造成的数据精度缺失。

阅读JML一定要仔细www

第三次作业

作业简介

本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。

容器选择

Network:

由于emojiId和emojHeat可以组成键值对,因此只需要一个HashMap即可存放这两个值。

实现思路

在Message的基础上,延展出了三个子类:EmojiMessageRedenvelopMessageNoticeMessage,较为简单,直接按照JML规格实现即可。

本次作业的难点和重点在于Network的sengIndirectMessage方法,本质即是求两点间最短路径。查阅资料后,发现有弗洛伊德算法和迪杰斯特拉算法可考虑。前者可以求从任一结点到另一结点的路径;而后者只能求node[0]到其他所有结点的路径。但考虑到前者是O(n3)的时间复杂度而后者仅为O(n2),因此最终还是选择迪杰斯特拉算法,实现如下:

首先为便于比较,新建一个Pair类,存储了id和value,进而实现一个新的comparator。

public class Pair {
    private int id;
    private int dis;

    public Pair(int id, int dis) {
        this.id = id;
        this.dis = dis;
    }

    public int getId() {
        return this.id;
    }

    public int getDis() {
        return this.dis;
    }

    public void setDis(int dis) {
        this.dis = dis;
    }

}

首先要判断两个结点是否连通:

  1. 如果两点不连通(即!isCircle(id1,id2)),直接返回-1
  2. 反之,如果两点相同,返回0
  3. 以上两者都不符合,使用迪杰斯特拉算法寻找最小值。
@Override
public int sendIndirectMessage(int id) throws MessageIdNotFoundException {
    //求最短路径且返回路径长度。
    if (messages.containsKey(id) && getMessage(id).getType() == 0) {
        Message m = getMessage(id);
        int value = m.getSocialValue();
        MyPerson p1 = (MyPerson) m.getPerson1();
        MyPerson p2 = (MyPerson) m.getPerson2();
        if (!checkIsCircle(p1.getId(), p2.getId())) {
            return -1;
        } else if (p1.equals(p2)) {
            return 0;
        }
        p1.addSocialValue(value);
        p2.addSocialValue(value);
        if (m instanceof RedEnvelopeMessage) {
            int money = ((RedEnvelopeMessage) m).getMoney();
            p1.subMoney(money);
            p2.addMoney(money);
        } else if (m instanceof EmojiMessage) {
            int heat = emojiHeatList.get(((EmojiMessage) m).getEmojiId()) + 1;
            emojiHeatList.replace(((EmojiMessage) m).getEmojiId(), heat);
        }
        p2.getMessages().add(0, m);
        messages.remove(id);
        return dijkstra(p1.getId(), p2.getId());
    } else {
        throw new MyMessageIdNotFoundException(id);
    }
}

由于我采用的是HashMap来存储Messages,不方便直接建立图结构,因此还是将其转化为ArrayList后使用算法,实现如下:

private static Comparator<Pair> pairComparator = Comparator.comparingInt(Pair::getDis);

private int dijkstra(int id1, int id2) {
    HashMap<Integer, Pair> paths = new HashMap<>(5000);//记录每个点到id1的长度
    ArrayList<Integer> used = new ArrayList<>(5000);//记录用过的点
    used.add(id1);
    for (Integer i : people.keySet()) {
        paths.put(i, new Pair(i, Integer.MAX_VALUE));
    }
    paths.get(id1).setDis(0);
    Queue<Pair> remains = new PriorityQueue<>(5000, pairComparator);
    remains.add(paths.get(id1));
    Pair vertex = remains.poll();
    while (true) {
        Person currentPerson = getPerson(vertex.getId());
        for (Map.Entry<Integer, Integer> entry :
             ((MyPerson) currentPerson).getAcquaintance().entrySet()) {
            int id = entry.getKey();
            if (!used.contains(id)) {
                Person person = getPerson(id);
                int dis = currentPerson.queryValue(person) + paths.get(vertex.getId()).getDis();
                if (dis < paths.get(id).getDis()) {
                    paths.get(id).setDis(dis);
                    remains.remove(paths.get(id));
                    remains.add(paths.get(id));
                }
            }
        }
        vertex = remains.poll();
        if (vertex.getId() == id2) {
            return paths.get(vertex.getId()).getDis();
        }
        used.add(vertex.getId());
    }

度量分析

使用迪杰斯特拉算法后,时间复杂度最高的方法即为上述方法,O(n^2),但由于数据范围限制,最终也被控制在较为可观的范围里。

bug分析

本次大胜利,强测互测都没有被hack到。反思原因,首先方法的时间复杂度还算过得去很重要,其次和同学对拍多次,(也是在对拍的过程中发现了许多bug)。

关于测试方法,只有第一次作业使用了Junit方法以检查规范性,后面两次还是觉得对拍效率更高。

感想

相较于前两单元的作业,本次作业真的可以说是送分。

JML不难,在简单阅读并理解范例后可以很快入门,但是随着代码量的增多和复杂度的增加,JML规格变得越来越复杂,其中包含的信息也越来越多,稍有不慎就可能看漏或者看错其中的一些细节(第二次作业大翻车就是很好的体现),因此更为仔细的阅读是JML学习中必不可少的。

此外,通过第一次被hack的教训我也意识到,JML更多体现的是最终需要达到的“结果”,中间的过程、如何实现需要我们自己思考。以这一单元的几个方法为例,如果单纯按照JML给出的思路去写,很有可能会TLE,对其进行优化,也就更显得必要。

总之,本单元虽然简单却仍有其精妙之处,学到了很多东西,值得!

posted @ 2021-05-27 10:58  blurrrr  阅读(191)  评论(0)    收藏  举报