标签:OO 结点 get int rootOfp rootOfq 2022 public 单元
2022 OO 第三单元总结
一、利用 JML 规格准备测试数据
首先是对于 JML 规格的理解。通过阅读 model 可以初步了解需要维护的对象,针对每个方法按照 normal_behavior 和 exceptional_behavior 进行划分,先实现异常行为判断,再按照 requires 条件分别实现正常行为。对于比较长的 JML 规格,通常有很大一部分是在对 assignable 做解释说明;对于连通性、最小生成树和最短路径方法等,JML 规格比较抽象,最重要的是理解它代表的含义,将其转化为自己的设计。
其次是根据 JML 规格构造数据。可以使用 Junit 进行单元测试,编写数据生成器,利用随机数据进行基础功能测试;也可以生成针对特定方法的性能测试数据。采用黑箱测试模式的同时对拍,有利于及时发现问题,发现对于 JML 规格的理解偏差。
二、架构设计与图维护策略
由于本单元三次作业均给定了接口,且迭代层次明显,在此过程中没有重构,故展示第三次作业的 UML 类图,为表示简洁忽略了异常类。
对于图的维护,没有使用额外的类来集成表示社交关系,而是在提供的接口外实现了 MyPerson 的 getAcquaintance 方法。新建了 Edge 类来表示边,只包含一个结点的信息和边权值。
采用并查集维护连通性问题,采用了基于 rank 的路径压缩优化方式,在单次查询中的时间复杂度为 \(O(\log^{*}(n))\).
对于连通块个数的计算,由于维护的图具有非连通单权值无向无环的性质,故采用在增加有效结点和增加割边时维护的方式,分别在 addPerson 和 addRelation 方法中,后者需要保证操作前两结点不连通;分别在维护的连通块总数上增减 1.
对于以下代码中的 number 数组,主要是维护结点所在连通块中除自身以外的结点个数,其中 key 为并查集中所维护的父结点,作为最短路径算法和最小生成树算法的边界条件。尤其是对于后者,由于采用非动态维护的方式,加之边只保留了一个结点的信息,在最后加入的结点存在多条割边时可能出现会在存边的优先队列中弹出空指针的现象,而增加边界条件可以保证在访问连通块中所有结点后及时退出循环。此外,单源最短路径算法 Dijkstra 中有在结点被标记访问后其最短路径被确定的性质,因此可以在该类被创建时即传入起点和终点的参数,在达到终点时及时返回结果。
虽然上述看起来有点合理,且 Prim 和 Dijkstra 具有高度的一致性,但我却在实际使用中忽略了两者的区别。两者都可以使用堆优化,在优先队列中存放边,但两者的优先规则不一样。前者是每次弹出当前权值最小的割边,而后者是当前更新中的具有最小距离的结点所在边。由于这不是什么好事,这里不再赘述,参见后文。
在图维护策略中不足之处是对算法的理解还不够深刻,且并没有采用动态维护的方式,但在性能上由于指令条数的限制也没有体现出绝对的劣势。较好之处在于在同时维护图的多种性质中找到了关联,在现有的架构中一定程度上降低了编程难度。
public void addToNumber(int pn, int po) {
int numAdd = getNumber(po) + 1;
if (!number.containsKey(pn)) {
number.put(pn, numAdd);
} else {
number.replace(pn, number.get(pn) + numAdd);
}
number.remove(po);
}
public int getNumber(int p) {
return number.getOrDefault(p, 0);
}
public int find(int p) {
int pt = p;
while (pt != parents.get(pt)) {
parents.replace(pt, parents.get(parents.get(pt)));
pt = parents.get(pt);
}
return pt;
}
public void union(int p, int q) {
int rootOfp = find(p);
int rootOfq = find(q);
if (rootOfp == rootOfq) {
return;
}
// determine the merge direction by comparing the rank of roots
if (rank.get(rootOfp) < rank.get(rootOfq)) {
parents.replace(rootOfp, rootOfq);
addToNumber(rootOfq, rootOfp);
} else if (rank.get(rootOfq) < rank.get(rootOfp)) {
parents.replace(rootOfq, rootOfp);
addToNumber(rootOfp, rootOfq);
} else {
parents.replace(rootOfp, rootOfq);
rank.replace(rootOfq, rank.get(rootOfq) + 1);
addToNumber(rootOfq, rootOfp);
}
}
三、性能问题和修复情况
对于修复最难顶的一个问题是在第三次作业的修复中由于各种奇怪的原因搞错了 ddl,具体原因不在博客里说明了,最后没能成功提交修复,坦白(摊牌)了。
其实由于强测和互测中对于指令条数的限制,在对于连通性、最小生成树、最短路径上的查询中 CTLE,也不是一件容易的事情。这就导致我在一定程度上忽略了普通查询方法中可能出现的性能问题。
比如对于 qgvs 方法,如果直接翻译 JML 规格,时间复杂度是 \(O(n^2)\) ,针对其构造数据的策略是:新建一个组,往组里疯狂加人;如果对其稍加改进,改成对组内遍历查询,如果组内每个人所认识的人都是常数,时间复杂度是 \(O(n)\),虽然看起来没什么问题,但针对其构造数据的策略是:新建一个组,往组里疯狂加人,之后按照特定规则增加关系,让组内的每个人都和相邻的人相识。如果是用 ArrayList 而不是 HashMap 存储 people,单次查询的时间复杂度是 \(O(n)\),这是前述计算中所忽略的。
最好的优化方式是保证 \(O(1)\) 的查询时间复杂度,维护每个人所在的所有组,在将人加入组中时,将组内所有与该人旧识的社交值的二倍加在该组的总社交值上;在添加关系时先判断新交是否与自己有共同的组,如果有则将两者社交值的二倍加在该组的总社交值上。通过牺牲维护成本来提高查询效率的思想在本单元作业中体现得比较明显,因此并不能只是简单地实现了接口方法。
四、功能扩展
在下述情境下讨论 NetWork 的功能扩展问题,假设出现了几种不同的 Person:
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过 Advertiser 来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过 Advertiser 给相应 Producer 发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
相关接口方法
public interface ExtendedNetWork extends Network {
/*@ public instance model non_null Product[] products;
@ public instance model non_null int[] productHeatList;
@*/
/**
* 查询是否包含产品
* @param id 产品编号
* @return 是否包含产品
*/
public /*pure*/ boolean containsProduct(int id);
/**
* 获取产品
* @param id 产品编号
* @return 产品
*/
public /*pure*/ Product getProduct(int id);
/**
* 增加产品
* @param product 产品
* @throws EqualProductIdException 产品编号重复
*/
public void addProduct(Product product) throws EqualProductIdException;
/**
* 获取产品的价格
* @param id 产品编号
* @throws ProductIdNotFoundException 没有对应产品
* @return 该产品的价格
*/
public /*pure*/ int getProductValue(int id) throws ProductIdNotFoundException;
/**
* 查询产品销售路径
* @param productId 产品编号
* @param personId 购买者编号
* @throws ProductIdNotFoundException 没有对应产品
* @throws PersonIdNotFoundException 没有对应购买者
* @return 最小销售代价
*/
public /*pure*/ int sellProductPrice(int productId, int personId) throws
ProductIdNotFoundException, PersonIdNotFoundException;
/**
* 获取产品
* @param id 产品编号
* @return 产品的受欢迎程度
*/
public /*pure*/ int getProductPopularity(int id);
/**
* 下架不受欢迎的产品
* @param limit 容忍限度
* @return 产品的总数
*/
public int deleteColdProduct(int limit);
}
核心业务 JML 规格
1. 获取产品价值
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
@ ensures (\exists int i; 0 <= i && i < products.length; products[i].getId() == id &&
@ \result == products[i].getValue());
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) (\forall int i; 0 <= i && i < products.length;
products[i].getId() != id);
@*/
public /*pure*/ int getProductValue(int id) throws ProductIdNotFoundException;
2. 下架冷门产品
/*@ public normal_behavior
@ assignable productHeatList, messages;
@ ensures (\forall int i; 0 <= i && i < \old(products.length);
@ (\old(productHeatList[i] >= limit) ==>
@ (\exists int j; 0 <= j && j < products.length; products[j] == \old(products[i]))));
@ ensures (\forall int i; 0 <= i && i < products.length;
@ (\exists int j; 0 <= j && j < \old(products.length);
@ products[i] == \old(products[j]) && productHeatList[i] == \old(productHeatList[j])));
@ ensures products.length ==
@ (\num_of int i; 0 <= i && i < \old(products.length); productHeatList[i] >= limit);
@ ensures products.length == productHeatList.length;
@ ensures (\forall int i; 0 <= i && i < \old(messages.length);
@ (\old(messages[i]) instanceof AdvertisementMessage &&
@ (containsProduct(\old(((AdvertisementMessage)messages[i]).getProduct())) ==>
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));
@ ensures (\forall int i; 0 <= i && i < \old(messages.length);
@ (!(\old(messages[i]) instanceof AdvertisementMessage) ==>
@ (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));
@ ensures messages.length == (\num_of int i; 0 <= i && i <= \old(messages.length);
@ (\old(messages[i]) instanceof AdvertisementMessage) ==>
@ (containsProduct(\old(((AdvertisementMessage)messages[i]).getProductId()))));
@ ensures \result == products.length;
@*/
public int deleteColdProduct(int limit);
3. 产品销售路径
/*@ public normal_behavior
@ requires containsProduct(productId) && contains(personId) &&
@ !(\exists int i; 0 <= i && i < people.length && people[i] instance of Advertiser;
@ (Advertiser)people[i].hasProduct(productId) && isCircle(personId, people[i].getId()));
@ ensures \result == -1;
@ also
@ public normal_behavior
@ requires containsProduct(productId) && contains(personId) &&
@ (\exists int i; 0 <= i && i < people.length && people[i] instance of Advertiser;
@ (Advertiser)people[i].hasProduct(productId) && isCircle(personId, people[i].getId()));
@ ensures (\exists Person[] pathM;
@ pathM.length >= 2 &&
@ (\exists int i; 0 <= i && i < people.length && people[i] instance of Advertiser;
@ (Advertiser)people[i].hasProduct(productId) && isCircle(personId, people[i].getId()) &&
@ pathM[0].equals(people[i])) &&
@ pathM[pathM.length - 1].equals(getPerson(personId)) &&
@ (\forall int i; 1 <= i && i < pathM.length; pathM[i - 1].reachable(pathM[i]));
@ (\forall Person[] path;
@ pathM.length >= 2 &&
@ (\exists int i; 0 <= i && i < people.length && people[i] instance of Advertiser;
@ (Advertiser)people[i].hasProduct(productId) && isCircle(personId, people[i].getId()) &&
@ pathM[0].equals(people[i])) &&
@ pathM[pathM.length - 1].equals(getPerson(personId)) &&
@ (\forall int i; 1 <= i && i < path.length; path[i - 1].reachable(path[i]));
@ (\sum int i; 1 <= i && i < path.length; path[i - 1].queryValue(path[i])) >=
@ (\sum int i; 1 <= i && i < pathM.length; pathM[i - 1].queryValue(pathM[i]))) &&
@ \result==(\sum int i; 1 <= i && i < pathM.length; pathM[i - 1].queryValue(pathM[i])));
@ also
@ public exceptional_behavior
@ signals (ProductIdNotFoundException e) !containsProduct(productId);
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) containsProduct(productId) && !contains(personId);
@*/
public /*pure*/ int sellProductPrice(int productId, int personId) throws
ProductIdNotFoundException, PersonIdNotFoundException;
如果考虑到市场营销的实际情况,不仅 MyNetWork 类需要被扩展,其它的类也需要扩展相应的功能。比如在上述规格撰写中所提及的 reachable 方法,在销售相关问题时替代原有的 isLinked 方法,因为广告商不仅可以给自己认识的人发广告,也可以到微信群里转发广告(doge)...
考虑架构的扩展性时出现了一个严重的问题,是之前在完成现有作业功能时没有考虑到的。由于支持扩展功能的架构要支持构建多种类型的图,可能需要在查询销售路径时采用多源最短路径算法,因此最好新建一个图类。
五、心得体会
接上,如果需要考虑架构设计的扩展性问题,就是说原来偷的懒最后都还回来了(这次作业又何尝不说明了这个道理呢)。本单元对于 JML 规格的学习,一方面让我更加了解测试和异常处理等在实际开发中非常重要的问题,另一方面我也没有避免被它迷惑(?),过于依赖它的清晰严谨而直接翻译,没有在架构上做更好的设计。
构造数据的过程也体现了对于性能的一种理解,在互测中我也发现了很会构造数据的同学,属于是把这单元的各种指令和制约关系都玩明白了。我最开始一直是按照 average 的思想分析时间复杂度和写代码,虽然可能在数据量比较小时体现出一些优势,但在 pressure 下表现非常不好, 这可能也是我一直以来存在的一种误区,感谢 OO 给了我一个反省的机会。
标签:OO,结点,get,int,rootOfp,rootOfq,2022,public,单元 来源: https://www.cnblogs.com/RacerK/p/16343275.html
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。