OO第三单元总结
图模型架构
基础元素
-
顶点的维护:使用
hashmap<Integer, Person>
,以id为key实现O(1)查询 -
边的维护:使用
HashMap<Edge, Integer>
,其中Edge
是边,知道两个顶点时可以实现O(1)查询 -
并查集维护:
private void union1(int id1, int id2) {
int root1 = find1(flag.get(id1));
int root2 = find1(flag.get(id2));
if (root1 == root2) {
return;
}
if (troots[root1] < troots[root2]) {
troots[root2] = root1; //按高度求并
} else if (troots[root1] > troots[root2]) {
troots[root1] = root2;
} else {
troots[root1] = root2;
troots[root2]--;
}
}
复杂指令
-
qci:查询两个顶点是否在同一集合中,并查集维护正确即可,时间复杂度为O(1)。
-
qlc: 查询包含特定顶点的最小生成树。最小生成树主要有两个算法,Prim算法和Kruskal算法。其中Prim算法主要采用堆优化找到最短的割边,优化后的时间复杂度为O(ElogV),适合于稠密图。Kruskal算法可以采用并查集优化查找两个顶点是否同时加入了最小生成树,堆优化可以快速找到当前最小的边,优化后的时间复杂度为O(|Elog|E|)。在课下测试时,发现以强/互测的数据规模,Kruskal的表现更优,所以我选择的是Kruskal算法。
-
sim:查询最小加权路径主要采用的是使用堆优化的Dijkstra算法,时间复杂度为O(ElogV )。
动态维护
本次作业互测主要的hack点就在于hack没有动态维护的O(n2)的算法,所以实现时这是一个很需要注意的问题。
-
qbs:添加person时集合的数量加1,合并时集合的数量减1,即可实现O(1)查询
-
qgvs:在atg和ar时增加边值,在dfg时减少边值,避免使用jml规格上的方法
-
qgav:需要注意的是平均年龄只需要查询一次,否则该算法会编程O(n2)
数据构造与测试
本单元由于实现比较简单,只要读懂了jml利用一些基础的图算法就能完成,所以大量时间都放在了测试上。
数据构造思路
第三单元的数据大体上可以分为增加信息类型的数据(例如ap、ar、am等),以及发送、查询类型的数据(qv、qbs等),为了能够正确的查询信息,避免异常,所以需要将已经增加的信息存在数据生成器内部的容器当中,然后针对性的生成查询,发送消息的数据。
添加类型
public String getMessage() {
String name;
int id;
int age;
name = getName();
id = random.nextInt(500000);
while (idSet.contains(id)) {
id = random.nextInt(500000);
}
idSet.add(id);
age = random.nextInt(201);
return String.format("ap %d %s %d", id, name, age);
}
以ap指令为例,利用HashSet保存已经增加的personId信息,避免生成相同id的ap指令,同理,ag、am等指令也将groupId等信息存储。
查询类型
查询以ar为例,因为该指令既要使用已经存入的id(本质上也是一种查询),也会存入新的信息(person的关系),封装了一个类来判断,本质上是一个无序的二元组,避免重复添加边。
public Link(int id1, int id2) {
this.id1 = id1;
this.id2 = id2;
}
异常类型
由于已经存了所有的信息,所以产生异常的信息也比较简单,例如PersonIdNotFoundException
异常只要查询一个idSet中没有的id即可。需要注意的点在于,不能简单的全部产生异常指令,需要一些梯度非异常指令,再逐步产生异常指令,才能更好的保证测试的有效性,和异常的覆盖性。
覆盖率保证
覆盖率的保证主要是理清楚各条指令的关系,然后将可能相互产生影响的指令交互,例如message有关的指令(am、arem、anm、aem、sim、sm)可以放在一起测试,ar、qv等指令也可以放在一起测试。
本次作业比较遗憾的一个点在于,由于时间关系,没有做很细致的覆盖率分析,和系统化的分类标准,主要是评感觉保证覆盖性。
自动化脚本
os.system("java -jar DataGen-uml.jar")
os.system('javac -encoding UTF-8 -cp {0} $(find . -name "*.java")'.format(lib))
for j in range(first, last):
filein = open("{1}data{0}.txt".format(j, back), "r")
fileout = open("A{0}.txt".format(j), "w")
time2 = time.time()
p = subprocess.Popen(
'java {0}.java'.format(main),
shell=True,
stdin=filein,
stdout=fileout,
encoding="utf-8"
)
p.wait(10)
time2 = time.time() - time2
本次测试以对拍为主,在写对拍之前,观看了其他大佬的自动化测试脚本,得出了一些经验。
-
java编译:直接使用java编译产生可执行文件,省去打包的过程。
-
log:采用日志方式,既可以避免短路,也可以准确定位出错的位置。
-
subprocess:该模块的
Popen()
函数用起来非常方便,可以指定输入输出管道,可以控制运行时间等等。
性能分析和Hack策略
个人分析
本次作业的性能尚可,大部分可以优化的点都优化了,所以性能上没有出现太大问题。唯一一个在互测中被hack的点是一个懒删除的问题,在实现dce时使用了懒删除,但是没有考虑到一个消息删除之后可能会以同样的id重新添加进来,所以出了问题。其他地方均没有发现bug。
他人bug
但是在互测中发现,本单元虽然比较容易实现正确,但是在性能上很容易出现问题。三次作业中均有hack成功,去掉同质bug共计6刀。
-
qbs没有实现动态查询:出现这个问题的同学均是按照jml实现,而没有动态维护集合的数量。
-
**qgvs没有实现动态查询:**这个问题在前问分析qgvs已经提到过,主要是出现了O(n2)的算法
-
qgvs使用缓存:这个实在阅读代码过程当中发现的,该同学使用了缓存来实现qgvs查询,本质上还是一个复杂度为O(n2)的查询,只需要在每次查询之后更新信息再次查询即可。另外在这个点中还发现了疑似java编译时优化,如果只在group中添加person而完全没有ar,即使是O(n2)的算法cpu时间也非常短,而只要稍微增加少量arcpu时间就会大幅度增加,以达成hack的目的。猜想是由于java虚拟机进行了一些分支预测优化。
-
qgav:该指令也提到过了,本质上还是一个复杂度为O(n2)的查询。
Network扩展
新增三个Advertiser
、Customer
继承Person
、Producer
继承了Customer
其中Producer内部维护了一个商品信息(以String表示),以及生成该商品所需要的其他商品,所以可以查询某个商品的生产链。
新增AdvertiseMessage
和TrdingMessage
来表示广告和销售,并且继承Message
完成业务逻辑的核心方法jml如下
/* @ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 0 &&
@ &&@(getMessage(id) instanceof RedEnvelopeMessage) getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
@ getMessage(id).getPerson1() != getMessage(id).getPerson2();
@ assignable messages;
@ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
@ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue;
@ 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 \old(getMessage(id)).getPerson1().getSocialValue() ==
@ \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
@ \old(getMessage(id)).getPerson2().getSocialValue() ==
@ \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
@ ensures \old(getMessage(id).getPerson2().canTrading(getMessage(id).getProduct()))==true;
@ 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 normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 1 &&
@ (getMessage(id)) instanceof RedEnvelopeMessage && getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1());
@ assignable people[*].socialValue, messages;
@ 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 Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==
@ \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue()) && p.getMessage(id).getProduct() == true;
@ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
@ \old(people[i].getSocialValue()) == people[i].getSocialValue());
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
@ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
@ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
@ !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
@*/
void sendAdvertise(int id) throws
RelationNotFoundException, AdvertiseMessageIdNotFoundException, PersonIdNotFoundException;
/*@ public normal_behavior
@ requires contains(id) && getPerson(id) instanceof Producer;
@ ensures \result == getPerson(id).getProductPath();
@ also
@ public exceptional_behavior
@ signals (producerIdNotFoundException e) !contains(id);
@*/
List<String> queryProductPath(int producerId);
als (producerIdNotFoundException e) !contains(id);
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < produtIdList.length; produtIdList[i] == id);
@ assignable produtIdList;
@ ensures (\exists int i; 0 <= i && i < productIdList.length; productIdList[i] == id);
@ ensures productIdList.length == \old(productIdList.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
@ (\exists int j; 0 <= j && j < productIdList.length; productIdList[j] == \old(productIdList[i]);
@ also
@ public exceptional_behavior
@ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < emojiIdList.length;
@ productIdList[i] == id);
@*/
public void storeProductId(String id) throws EqualProductIdException;
单元学习体会
不足之处
-
数据构造有所疏漏。由于从上学期计组课程开始大量写数据生成器,写到本单元的时候已经有所疲惫,所以这个单元构造数据出现了没有考虑到的情况,导致在互测中被hack了一次。之后希望通过构建数据分析工具来全面完善的分析覆盖率。
-
数据自动分析能力不足。之前构造数据出现问题之后都是人为分析错误,之后可以根据对数据的理解以及可能出现的bug尽量在对拍之后有一个完善的分析环节。
能力提升
-
阅读jml的能力得到提升,设计更具规范化。
-
算法能力提升,由于长时间没有写纯算法的题,所以导致许多算法及数据结构的知识有所忘记,本单元正好使我重新回顾了很多算法知识。
-