OO 第三单元博客作业

第三单元博客作业

架构设计

异常
private static int count = 0;   
private static HashMap<Integer, Integer> id2Count = new HashMap<>();
private final String msg;
  • count 统计总发生次数

  • id2Count 统计相应 id 发生该异常的次数

  • msg 存需要 print 的信息

3-1

/*@ public instance model non_null Person[] people;
 @ public instance model non_null Group[] groups;
 @*/
  • 这种写法只是代表这一对象中需要存储那些实例,并不代表实现时完全采用数组存储

  • 第一次作业对JML理解还不到位,所以均采用了 ArrayList。其实为了方便查询方法的实现时可以采用 HashMap

MyPerson
// Attribute
private final int id;
private final String name;
private final int age;
private final ArrayList<Person> acquaintance;
private final ArrayList<Integer> value;
private int fa;
// Operation
public int getFa();
public int SetFa();
...
  • MyPerson 增加属性 Fa,用来存 Person 的父节点

  • getFaSetFa 用于维护 Fa

MyGroup
// Attribute
private int id;
private ArrayList<Person> people;
private int valueSum;
// Operation
public void addPerson(Person person);
public int getValueSum();
public void delPerson(Person person);
...
  • 考虑到 qgvs 单纯按 JML 来实现,有 O(n^2) 的时间复杂度

    • 增加 valueSum ,在 addPersondelPerson 时,实现 O(n) 维护;在 getValueSum 时实现 O(1) 查找

MyNetWork
// Attribute
private final ArrayList<Person> people;
private final ArrayList<Group> groups;
private int blockSum;
// Operation
private int find(int id);
public void addRelation(int id1, int id2, int value);
public boolean isCircle(int id1, int id2);
public int queryBlockSum();
...
  • people 与 groups 采用 ArrayList

  • ar, qbsisCircle 最初并未采用并查集实现,其中 qbs 直接使用 BFS

3-2

Message 相关类省略

MyPerson
// Attribute
private final HashMap<Person, Integer> acquaintance;  
private int money;
private int socialValue;
private final ArrayList<Message> messages;
...
// Operation
public void getEdges(ArrayList<Edge> edges);
public List<Message> getReceivedMessages();
...
  • getEdges 用于为 qlc 提供边集合;

  • getReceiveMessages 的简便写法:messages.subList(0, Math.min(messages.size(), 4))

MyNetWork
// Attribute
private final HashMap<Integer, Person> people;
private final HashMap<Integer, Group> groups;
private final HashMap<Integer, Message> messages;
private final ArrayList<Edge> edges;
private int blockSum;
// Operation
public int queryLeastConnection(int id);
...
  • qlc 本质为找到 id 所在连通分量的最小生成树,返回边权值总和;采用 Kruskal 算法不会超时。

  • qlc 实现:

    1. 找到与 id 位于同一个连通分量的所有 Person

    edges.clear();
    int fa = find(id);
    ArrayList<Integer> set = new ArrayList<>();
    for (Integer pid : people.keySet()) {
    if (find(pid) == fa) {
    set.add(pid);
    ((MyPerson) getPerson(pid)).getEdges(edges);
    }
    }
    1. 重置这些 Person 的 Fa,并把边排序:此处 Kruskal 采用并查集实现更容易

    for (Integer pid : set) {
    ((MyPerson) getPerson(pid)).setFa(pid);
    }
    edges.sort(Comparator.comparingInt(Edge::getValue));
    1. 遍历 edges 集合,当满足 num_Vertex == num_Edge + 1 时结束遍历

    int numE = 0;
    int numV = set.size();
    int res = 0;
    for (Edge edge : edges) {
    if (numV - 1 == numE) {
    break;
    }
    int id1 = edge.getPerson1().getId();
    int id2 = edge.getPerson2().getId();
    int fa1 = find(id1);
    int fa2 = find(id2);
    if (fa1 != fa2) {
    ((MyPerson) getPerson(fa1)).setFa(fa2);
    res += edge.getValue();
    numE++;
    }
    }
    return res;

3-3

Message 相关类省略

MyNetWork
// Attribute
private final HashMap<Integer, Integer> emojiId2Heat;
// Operation
public int sendIndirectMessage(int id);
...
  • sim 本质是求 id 对应的 Message 中,Person1 到 Person2 的最短路

  • sim 实现:

            Person person1 = getMessage(id).getPerson1();
           Person person2 = getMessage(id).getPerson2();
           if (find(person1.getId()) != find(person2.getId())) {
               return -1;
          }
           HashMap<Integer, Integer> dis = new HashMap<>(); // id->dis
           PriorityQueue<Pair<Integer, Integer>> pq = // dis->id, 按距离权值排序
                   new PriorityQueue<>(Comparator.comparingInt(Pair::getKey));
           dis.put(person1.getId(), 0);           // 把原点的dis置0, 刚进循环时从原点开始
           pq.add(new Pair<>(0, person1.getId())); // 加入优先队列
           while (!pq.isEmpty()) {
               Pair<Integer, Integer> u = pq.poll(); // 队头一定是已经找到最小距离的点
               HashMap<Person, Integer> acquaintance = // 遍历所有邻接点
                      ((MyPerson) getPerson(u.getValue())).getAcquaintance();
               for (Map.Entry<Person, Integer> p : acquaintance.entrySet()) {
                   if (!dis.containsKey(p.getKey().getId()) || // 距离小时修改
                       u.getKey() + p.getValue() < dis.get(p.getKey().getId())) {
                       dis.put(p.getKey().getId(), u.getKey() + p.getValue());
                       pq.add(new Pair<>(u.getKey() + p.getValue(), p.getKey().getId()));
                  }
              }
               if (u.getValue() == person2.getId()) { // 找到直接返回
                   break;
              }
          }
           sendPpmRemain(person1, person2, id); // remain
           return dis.get(person2.getId());
    • 为什么优先队列队头一定是满足最小距离的点?

      •  

         

      • 若 A 作为源点,与其邻接的只有 B,C,D 三点,其dist[]最小时顶点为 C,即就可以确定A→C为 A 到 C 的最短路。但是我们存在疑问的是:是否还存在另一条路径使 A 到 C 的距离更小? 用反证法证明。

        假设存在如上图的红色虚线路径,使A→D→C的距离更小,那么A→D作为A→D→C的子路径,其距离也比A→C小,这与前面所述“dist[]最小时顶点为 C”矛盾,故假设不成立。因此这个疑问不存在。

        根据上面的证明,我们可以推断出,Dijkstra 每次循环都可以确定一个顶点的最短路径,故程序需要循环 n-1 次。

         

性能问题和修复

综合三次作业,性能问题最容易出现在四个函数上:qbsqgvsqlcsim。总结一下可能的原因

  • qbs:没有使用并查集,如 DFS 和 BFS 可能会 t(别有侥幸心理

  • qgvs:没有动态维护,每次查询都是 O(n^2)

  • qlc:采用了 prim 算法,对于稀疏图时间复杂度较高,可能会 t;Kruskal 寻找连通分量时未采用并查集(?

  • sim:O(n^2) 暴力实现,未采用堆优化(优先队列);本人是采用优先队列但是多余的初始化操作导致 t

qbs

并查集实现如下(3-1):

// Find
private int find(int id) {
if (((MyPerson) getPerson(id)).getFa() == id) {
return id;
}
return find(((MyPerson) getPerson(id)).getFa());
}
// Merge:
private void merge(int id1, int id2) {
int fa1 = find(id1);
int fa2 = find(id2);
   if (fa1 != fa2) {
      ((MyPerson) getPerson(fa1)).setFa(fa2);
       blockSum--;
  }
}
  • Find:此处为递归写法,且并未路径压缩,也可以顺利通过强测

  • Merge:更改其中一个 PersonFa ,同时维护 blockSum ,因为 addPerson 时所有 Person 都对 blockSum 自增 1

    • blockSum 其实就是 NetWork 图中的连通分量个数

    •     /*@ ensures \result ==
           @         (\sum int i; 0 <= i && i < people.length &&
           @         (\forall int j; 0 <= j && j < i; !isCircle(people[i].getId(), people[j].getId()));
           @         1);
           @*/
         public /*@ pure @*/ int queryBlockSum();

      为什么规格中要求 0 <= j && j < i所有 Person 之间的连接均为双向边,可以理解为一个邻接矩阵只需取上半或下半,否则会导致计算出的结果为正确结果的二倍

注:Find 的非递归,路径压缩写法(3-2 中实现)

private int find(int id) {
int root = id;
while (((MyPerson) getPerson(root)).getFa() != root) {
root = ((MyPerson) getPerson(root)).getFa();
}
for (int i = id, j; i != root; i = j) {
      j = ((MyPerson) getPerson(i)).getFa();
      ((MyPerson) getPerson(i)).setFa(root);
  }
   return root;
}

 

qgvs

O(n^2) 查找 -> O(n) 维护,O(1) 查找

// in Class MyGroup
private int valueSum;

@Override
public void addPerson(Person person) {
if (!hasPerson(person)) {
people.add(person);
for (Person p : people) {
valueSum += p.queryValue(person);
      }
  }
}

@Override
public void delPerson(Person person) {
if (hasPerson(person)) {
people.remove(person);
       for (Person p : people) {
           valueSum -= p.queryValue(person);
      }
       people.remove(person);
  }
}

@Override
public int getValueSum() {
  return 2 * valueSum; // 注意乘2, 规格中并不需要对双向边去重
}

 

sim

第一版写法存在很多不必要的语句增加了时间复杂度,如下:

  1. HashMap<Integer, Boolean> vis = new HashMap<>() 用于存储 PersonId 对应的 Person 是否已经找到到源点的最短路:由之前分析知道,优先队列队首对应的 Person 一定是已经找到单源最短路的点。不需要 vis 判断。

  2. disvis 用 for 循环遍历初始化:由于采用 Map<K,V>,未遍历到的节点不需要初始化为 -infinity,因为无法查到

  3. 仍采用 Edge 存边:对 edges 的维护需要额外开销,不如直接 getAcquaintance

 

测试数据准备

  • 对自己程序的测试采用随机数据生成+管道+Shell命令对拍实现

  • hack数据手动构造极端样例,如针对每一次作业的互测指令数量限制和容易出现性能问题的几种指令,C++循环遍历即可得到,eg.

// limit: 5000
// ap * 1667
ap 1 1 100
...
ap 1667 1667 100
// ar * 1666
ar 1 2 1
...
ar 1666 1667 1
// qbs * 1667
qbs
...

 

NetWork 拓展

  • 假设出现了几种不同的Person

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

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

    • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买

      • 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息

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

  • 如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等

  • 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

扩展:新建 Advertiser, Producer, Customer 继承已经实现的 MyPerson(Person);其作用可以看做是一种 Message

public class Product;
// Advertiser
public class Advertiser extends MyPerson;
public class advertiseMessage implements Message {
   private Map<Integer, Product> products = new HashMap<>();
  ...
}
// Producer
public class Producer extends MyPerson;
public class produceMessage implements Message;
// Customer
public class Customer extends MyPerson;
public class buyMessage implements Message;

核心业务功能实现:

  1. Advertiser: sendAdvertisement

    /* @ public normal_behavior
  @ assignable messages;
  @ assignable getMessage(id).getPerson2().messages;
  @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
      @         getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
      @         getMessage(id).getPerson1() != getMessage(id).getPerson2();
      @ requires (message instanceof advertiseMessage) && (getMessage(id).getPerson1() instanceof Advertiser) && (getMessage(id).getPerson2() instanceof Customer)
      @ 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 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 exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id);
      @ signals (MessageReceiveException e) containsMessage(id) && getMessage(id).getType() == 1
      @ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
      @         !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
      @ signals (MessageTypeExcpetion e) containsMessage(id) && getMessage(id).getType() == 0 &&
      @         (getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2())) && !((message instanceof advertiseMessage) && (getMessage(id).getPerson1() instanceof Advertiser) && (getMessage(id).getPerson2() instanceof Customer))
   */
public void sendAdvertisement(int id) throws MessageIdNotFoundException, MessageReceiveException, RelationNotFoundException, MessageTypeException;
  1. Producer: addProduct

    /* @ public normal_behavior
      @ assignable productList
      @ requires !containsProduct(product.getId());
      @ ensures produtList.length == \old(productList.length) + 1;
      @ ensures containsProduct(product.getId());
      @ ensures (\forall int i; 0 <= i && i < \old(productList.length)
      (\exists int j; 0 <= j && j < productList.length; productList[j] == \old(productList[i])));
      @ also
      @ public exceptional_behavior
      @ signals (EqualProductIdException e) containsProduct(product.getId());
   */
public void addProduct(/*@ non_null @*/Product product) throws EqualProductIdException;
  1. Customer: buyProduct

    /* @ public normal_behavior
  @ assignable getPerson(customerId).products
  @ assignable productList
      @ requires containsProduct(id);
      @ requires (getPerson(customerId) instanceof Customer) && getPerson(customerId).getMoney >= getProduct(id).getPrice();
      @ ensures produtList.length == \old(productList.length) - 1;
      @ ensures !containsProduct(id);
      @ ensures (\forall int i; 0 <= i && i < \old(productList.length) && !(\old(productList[i]).equals(getProduct(id)))
      (\exists int j; 0 <= j && j < productList.length; productList[j] == \old(productList[i])));
      @ ensures (\forall int i; 0 <= i && i < \old(productList.length) && \old(productList[i]).equals(getProduct(id))
      !(\exists int j; 0 <= j && j < productList.length; productList[j] == \old(productList[i])));
      @ also
      @ public exceptional_behavior
      @ signals (ProductIdNotFoundException e) !containsProduct(product.getId());
      @ signals (PersonTypeException e) containsProduct(product.getId()) && !(getPerson(customerId) instanceof Customer);
      @ signals (MoneyNotEnoughException e) containsProduct(product.getId()) && (getPerson(customerId) instanceof Customer) && !(getPerson(customerId).getMoney >= getProduct(id).getPrice());
   */
public void buyProduct(int customerId, int id) throws ProductIdNotFoundException, PersonTypeException, MoneyNotEnoughException;

 

学习体会

  • 从单元主题上讲,本单元主要是在JML规格下进行自己的代码编写,虽然思维难度比前两个单元低很多,但是在细节理解,代码规范和算法时间复杂度上提出了更高的要求。

  • 从算法学习上讲,大一下的时候没有重点学习过数据结构课程中图论的有关算法,在本单元中也是认真学习了一遍如并查集、最小生成树、最短路等算法。真的在这些算法中收获了很多。

  • 从程序测试上讲,本单元的输入规则明确,很适合书写自动评测和随机数据生成脚本。在与他人交流学习后,也更提高了自己对于自动化测试的理解和认识。在同侪压力下也让自己头一次认真系统的学习了Python的有关知识,希望以后也可以自己实现一个完整的评测机。

posted @ 2022-06-05 03:03  WassuhJ  阅读(85)  评论(1编辑  收藏  举报