架构设计
本次作业没有做太多的架构设计,其中的类的继承关系只有三个message对MyMessage
类的继承。而从具体实现角度而言,其实我的社交网络几乎所有的任务都承担在了NetWork
类中,其中实现了大量的算法(如并查集,Dijistra
图模型的架构与维护策略
第一次作业
图模型:第一次作业是一个图的遍历问题。我使用了并查集+状态压缩的方法。
维护策略:维护了一些Hashset。
第二次作业
图模型:第二次作业是一个最小生成树模型,我使用了Kruskal算法加并查集,同时使用了优先队列。
维护策略:主要是维护了几个id2**的Hashmap和为了优化qgps|qgvs|qgav
的全局变量。
第三次作业
图模型:第三次作业是一个最短路问题,我使用了堆优化的 dijsktra
。
维护策略:发现了之前使用Hashset的问题,于是维护了一个全局递增的ID编号用于equals方法判断。其余同上。
性能优化
三次作业都要注意的是,尽量用Hashmap或者Hashset等容器替换list,否则任意一次查找,哪怕是计算size,也会多出O(n)的复杂度。
第一次作业
第一次作业的复杂度优化主要在qci
和qbs
,涉及到图的遍历查找问题。基础的思路是使用dfs
或bfs
,但这样在强测与互测中,时间复杂度会被卡。所以我并查集加状态压缩的优化。具体实现如下:
@Override
public boolean isCircle(int id1, int id2) throws PersonIdNotFoundException {
//System.out.println("circle start");
if (!contains(id1)) {
throw new MyPersonIdNotFoundException(id1);
} else if (!contains(id2)) {
throw new MyPersonIdNotFoundException(id2);
}
int father1 = find(id1);
int father2 = find(id2);
return father1 == father2;
}
public int find(int id) {
if (id == getPerson(id).getFather()) {
return id;
}
int temp = find(getPerson(id).getFather());
getPerson(id).setFather(temp);
return temp;
}
@Override
public int queryBlockSum() {
HashSet<Integer> fathers = new HashSet<>();
for (Person person : people) {
fathers.add(find(person.getId()));
}
return fathers.size();
}
第二次作业
第二次作业的优化点主要集中在qlc
和qgps|qgvs|qgav
。
其中qlc
的实现采用了Kruskal算法加并查集,同时使用了优先队列。在那个经典的qgvs
测试点中幸存。具体实现如下:
@Override
public int queryLeastConnection(int id) throws PersonIdNotFoundException {
if (!contains(id)) {
throw new MyPersonIdNotFoundException(id);
}
int minCost = 0;
HashSet<Person> connections = new HashSet<>();
//find all relevant people
connections.add(getPerson(id));
for (Person person : people) {
if (isCircle(person.getId(), id) && id != person.getId()) {
connections.add(person);
}
}
PriorityQueue<Edge> q = new PriorityQueue<>(Comparator.comparing(Edge::getValue));
HashSet<Edge> edges = new HashSet<>();
for (Person person : connections) {
MyPerson one = (MyPerson) person;
HashSet<MyPerson> myGraph = one.getAcquaintance();
if (myGraph.size() == 0) {
continue;
}
for (MyPerson person1 : myGraph) {
int value = ((MyPerson) person).getIdToValue().get(person1.getId());
if (connections.contains(person1)) {
Edge edge = new Edge(person.getId(), person1.getId(), value);
Edge edge2 = new Edge(person1.getId(), person.getId(), value);
if (edges.contains(edge) || edges.contains(edge2)) {
continue;
}
edges.add(edge);
edges.add(edge2);
q.add(edge);
}
}
}
if (q.size() == 0) {
return 0;
}
for (Person person : connections) {
MyPerson one = (MyPerson) person;
one.setDst(person.getId());
}
int count = 0;
while (count != connections.size() - 1) {
Edge temp = q.poll();
assert temp != null;
int id1 = temp.getId1();
int id2 = temp.getId2();
if (findDst(id1) != findDst(id2)) {
getPerson(findDst(id1)).setDst(findDst(id2));
count++;
minCost += temp.getValue();
}
}
for (Person person : connections) {
MyPerson one = (MyPerson) person;
one.setDst(person.getId());
}
return minCost;
}
qgps|qgvs|qgav
几个方法,主要是采用了全局变量的存储方法。在每次新增到group的时候,对全局变量进行更新,这样可以避免遍历带来的时间开销。
第三次作业
第三次作业的优化点集中在sim
,我才用了堆优化的 dijsktra
算法,具体实现如下:
@Override
public int sendIndirectMessage(int id) throws MessageIdNotFoundException {
if (!containsMessage(id) || getMessage(id).getType() == 1) {
throw new MyMessageIdNotFoundException(id);
}
try {
if (!isCircle(getMessage(id).getPerson1().getId(),
getMessage(id).getPerson2().getId())) {
return -1;
}
} catch (PersonIdNotFoundException e) {
System.out.println("can't happen here");
}
Person myPerson1 = getMessage(id).getPerson1();
Person myPerson2 = getMessage(id).getPerson2();
Message message = getMessage(id);
PriorityQueue<Pair> idToDist = new PriorityQueue<>(Comparator.comparing(Pair::getDist));
HashSet<MyPerson> certainPerson = new HashSet<>();
MyPerson current = (MyPerson) myPerson1;
HashMap<Integer, Integer> dis = new HashMap<>();
dis.put(myPerson1.getId(), 0);
while (current.getId() != myPerson2.getId()) {
certainPerson.add(current);
for (Person person : current.getAcquaintance()) {
if (certainPerson.contains((MyPerson) person)) {
continue;
}
if (dis.containsKey(person.getId()) &&
(dis.get(current.getId()) + person.queryValue(current) <
dis.get(person.getId()))) {
dis.put(person.getId(), dis.get(current.getId()) + person.queryValue(current));
idToDist.add(new Pair(person.getId(), dis.get(person.getId())));
} else if (!dis.containsKey(person.getId())) {
dis.put(person.getId(), dis.get(current.getId()) + person.queryValue(current));
idToDist.add(new Pair(person.getId(), dis.get(person.getId())));
}
}
while (certainPerson.contains(current)) {
current = getPerson((idToDist.poll().getId()));
}
}
message.getPerson1().addSocialValue(message.getSocialValue());
message.getPerson2().addSocialValue(message.getSocialValue());
if (message instanceof RedEnvelopeMessage) {
((MyPerson) myPerson1)
.setMoney(myPerson1.getMoney() - ((RedEnvelopeMessage) message).getMoney());
((MyPerson) myPerson2)
.setMoney(myPerson2.getMoney() + ((RedEnvelopeMessage) message).getMoney());
}
if (message instanceof EmojiMessage) {
int emojiId = ((EmojiMessage) message).getEmojiId();
idToHeat.put(emojiId, idToHeat.get(emojiId) + 1);
}
((MyPerson) message.getPerson2()).addMessage(message);
idToMessage.remove(id);
return dis.get(message.getPerson2().getId());
}
测试
自己对于本单元的测试采用了对拍与随机测试为主,JUnit 为辅的测试策略。
采用 JUnit 对自己感到不太把握的方法进行了较为基础的功能性弱测,测试了 qci, qbs, qgvs, sim 等方法,但并未对整个程序进行全面的单元测试。本地的随机测试是我这单元做的不太好的地方,只是通过控制指令条数和增加一些重要卡tle的指令条数来增加测试强度,没有特别对各种类型的数据针对性构造,总体而言测试强度不高。(好在有小伙伴分享的比较强的数据带我)除此以外就是对拍,将自己程序的输出与其他人程序的输出进行比对以确保自己没有出现较严重的实现偏差甚至对规格理解的偏差。
随机测试
三次作业都写了简易的评测机,因为第三单元基本只依靠对拍,输出完全一致即可。所以三次作业可以完全复用。
值得一提的是,对于伙伴们输出时间的校验非常重要。如果时间差别太大,意味着可能有较大的优化空间,甚至可能会tle。下附时间测试代码。
with open(f"input/testPoint{k}.txt", 'r') as inputfile:
with open(f"outputzhy/result{k}.txt", 'w') as outputfile:
t=time.time()
subprocess.Popen(["java", "-jar", "Homework11.jar"], stdin=inputfile, stdout=outputfile).wait()
print(f"zhy time is: {time.time()-t}")
JUnit单元测试
JUNIT是常见的单元测试框架,用于对类中的每一个方法进行单元测试,正好适用于本作业每个方法提供的JML规格,校验每一条规格的实现情况。主要设计的思路是,使用JUNIT中的@before方法准备数据,再用@Test方法进行测试。测试的过程中将JML提供的规格进行翻译,针对边界情况、压力数据、普通情况进行测试。测试中使用assert语句判断条件是否得到满足,以及对应的异常是否被正常抛出。
bug分析与修复
我的作业
在本单元的强测与互测中,均为发现bug(主要原因是在提交之前de了太多bug)
自测过程中发现的bug实在太多了,就列举几个典型的吧:
-
在第三次作业的cm操作中,由于删除操作没有使用Iterator,而是直接ForEach。remove操作中遇到了若某个 person 存有 id 相同的 message1 与 message2,其中1是普通消息,2是notice消息。由于重写后的 equals 的判断条件仅仅是 id 相等,所以在直接对实例对象进行 remove 时,可能明明想移除 message2 却误移除了 message1。
解决的方法是,
(哎但我为什么没有想到直接用Iterator),我新增了一个依次递增的全局变量,并修改了remove中匹配的equals方法——只有两个变量永远这个相同的全局变量(序号),才能认为相同。 -
使用Hashset中的一些问题:由于对指导书的理解不清晰以及构造测试样例的疏漏,我忽略了有的id在第一次进入Network又被移出后,同样id可能会再次进入Network。这个疏漏的主要问题出在了sendMessage,当A把信息发给了B后,B会在内部维护的Hashset里增加一条message,但是如果这个id第二次被加入后,由于Hashset的特性,两条message不会共存,于是可能出现bug。
解决的方法是,将Person内部维护的Hashset换成LinkedList。
-
维护用的容器出现增删不同步的问题。为了避免遍历list或者for循环的时间消耗,我设置了多个id2**的Hashmap,但是我在增加/删除元素的时候,有的时候忘记维护相应的Hashmap,出现了不少问题。
互测中发现的bug
由于我采用自保性政策,第一次和第三次作业中,房里很平安,所以我也没怎么hack别人。第二次作业中,我早上起来一看,房里非常惨烈,一位xd一刀五,虽然我还幸存,但我决定开启自卫反击战,最后共hack了其他同学7次。
其中一次也实现了一刀五,是经典的连续几千个qgvs的数据点,房里五个人时间复杂度都爆了貌似。还有一个是1111的组内限制的问题,提醒我们要好好读JML,不能有遗漏。最后一个是qci的实现问题。
功能扩展
Advertiser发送广告
/*@ public normal_behavior
@ requires containsMessage(id) && getMessage(id).getType() == 3;
@ assignable people[].products,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); \old(p.products.length) == p.products.length-1;
@ ensures (\forall Person p; !\old(getMessage(id)).getGroup().hasPerson(p); \old(p.products.length) == p.products.length;
@ ensures (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p); (\exists int i; 0 <= i && i < p.products.length; products[i].equals(\old(((AdvertiseMessage)getMessage(id))).getProduct()));
@ ensures (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p); (\forall int i; 0 <= i && i < \old(p.products.length);(\exists int j; 0 <= j && j <= p.products.length;\old(p.products[i]).equals(p.products[j])));
@ ensures (\forall Person p; !\old(getMessage(id)).getGroup().hasPerson(p); (\forall int i; 0 <= i && i < \old(p.products.length);(\exists int j; 0 <= j && j <= p.products.length;\old(p.products[i]).equals(p.products[j])));
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundException e) !containsMessage(id);
@ signals (NotAdvertisementException e) containsMessage(id) && getMessage(id).getType() == 3;
@*/
public void sendAdvertiseMent(int id) throws MessageIdNotFoundException,NotAdvertisementException;
生产产品
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length;
@ people[i].getId() == id && people[i] instanceof Producer);
@ assignable products[];
@ ensures products.length == \old(products.length) + 1;
@ ensures (\exists int i; 0 <= i && i < products.length; products[i] == product);
@ ensures (\forall int i; 0 <= i && i < \old(products.length);
@ (\exists int j; 0 <= j && j < products.length; products[j] == \old(products[i]))));
@ also
@ public exceptional_behavior
@ signals (EqualProductIdException e) (\exists int i; 0 <= i && i < products.length; products[i].equals(product));
@ signals (ProducerIdNotFoundException e) !(\exists int i; 0 <= i && i < products.length;
products[i].equals(product)) && (\forall i; 0 <= i && i < people.size; !(people[i].getId() == id && people[i] instanceof Producer))
@*/
public void addProduct(Product product, int id) throws EqualProductIdException, ProducerIdNotFoundException;
购买
/*public normal_behavior
@ requires product.contains(product) && contains(coustomerId) && getPerson(customerId) instanceof Customer;
@ assignable products[], getPerson(customerId).money, getPerson(customerId).products;
@ ensures products.length == \old(products.length) - 1;
@ ensures (\forall int i; i>=0 && i<=\old(products.length)) && product[i]!=product; (\exist int j; j>=0 && j<=products.length; product[j].equals(product[i]))
@ ensures getPerson(customerId).getmoney == \old(getPerson(customerId)) - product.getValue();
@ ensures getPerson(customerId).getProduct().length == \old(getPerson(customerId).getProduct().length)+1;
@ ensures (\forall int i; i>=0&&i<=\old(getPerson(customerId).getProduct().length)&&\old(getPerson(customerId).getProduct().get(i));(\exist int j; j>=0 && j<=getPerson(customerId).getProduct().length; getPerson(customerId).getProduct().get(j).equals(\old(getPerson(customerId).getProduct().get(i)))
@ ensures (\exist int j; j>=0 && j<=getPerson(customerId).getProduct().length; getPerson(customerId).getProduct().get(i).equals(product);
@public exceptional_behavior
@ signals (ProductNotFoundException e) !product.contains(product);
@ signals (CustomerIdNotException e) !contains(coustomerId) || !(getPerson(producerId) instanceof Producer);
*/
public void purchase(int customerId, Product product) throws ProductNotFoundException, CustomerIdNotException;
学习体会
JML阅读与代码编写流程
在本单元JML的学习中,我先阅读了JML指导书,知道了它的一些基本用法。在具体作业的实践中,我一般会先把JML规格描述比较短的方法和类先写完,到最后剩下几个比较复杂的方法。这些方法又分为两类,一个是虽然描述的长,但是细看意思逻辑还是非常清晰的,我会在写完简单方法后写它们。
最后剩下的一些是比如每次作业的核心方法,涉及到一些图算法和优化方法,我一般会先大致宏观了解这个方法到底要实现什么,具体有哪些实现方法和基本且必须的优化方法(因为第三单元没有性能分了,所以我也没有特别做很多优化,只是希望不要各种tle就成,但事实上,从第一次作业我一开始写dfs感觉可能会tle然后优化开始,感觉太摆可能也不行)然后再着手实践相关算法,最后再一条条check具体的JML规格要求不要有偏差。
体会与感谢
这一单元与小伙伴的对拍&测试规模比前两单元大了不少,可能是这一单元不像前两单元作业有确定的可以用程序得到的答案,这一单元主要依靠与小伙伴对拍。虽然第三单元的作业,和预想中的一样还算愉快,但三次作业的debug过程中却出乎意料的艰辛,而且还遇到了很多几个人一起都错的一样,形成两派答案的情况。过程中发现了很多很多bug,好在在最后提交之前也都修复了,强测和互测并没有出锅。