OO第三单元总结
OO第三单元总结
作业架构及分析
hw9
基本要求
根据JML规格实现自己的几个类,构建一个社交网络查询和模拟系统,完成\(ap\)、\(ar\)、\(qv\)、\(qps\)、\(qci\)、\(qbs\)、\(ag\)、\(atg\)、\(dfg\)指令。
基本设计(架构&图模型构建与维护)
整体架构方面,各种指令大体按照JML规格按部就班实现即可,而为优化算法时间复杂度避免\(TLE\),我们主要需要选择合适的容器存储信息。尤其是各种依照id的查找提取方法及各种异常的抛出上,我们可以通过\(HashMap\)哈希表容器将\(O(n)\)的暴力遍历优化为近似\(O(1)\)。
作业中需要特别注意的指令有qci和qbs;而JML规格中阅读有一定难度也是这两条指令。我们需要选择算法优化时间复杂度,通过阅读JML规格,qci指令即查询图中两点是否处于同一最大连通块中,而qbs即查询实例中的最大连通块个数。故根据讨论区教程,我们选择并查集将\(O(n^2)\)的qci指令优化为\(O(1)\),并依据并查集全局维护\(blockSum\)变量将qbs指令优化为\(O(1)\)。关于并查集的具体实现上,经过学习和自我测试后我选择了路径压缩的非递归版写法。路径压缩可以避免原始并查集算法最差情况下形成长链的\(O(n)\),而通过\(ArrayList\)记录遍历的各个节点主要是避免递归次数过多引起的爆栈问题。(虽然在指令条数限制下似乎不可能发生爆栈问题)。相关源代码如下:
//private HashMap<Integer, Integer> links; 记录节点id对应父节点id的并查集数据结构
//private int blockSum; 记录当前最大连通块个数的变量
private void merge(int id1, int id2) {
int rootID1 = find(id1);
int rootID2 = find(id2);
links.put(rootID1, rootID2);
if (rootID1 != rootID2) {
blockSum -= 1; //发生合并则维护blockSum变量减少1
}
}
private int find(int id) {
ArrayList<Integer> stackList = new ArrayList<>(); //非递归避免爆栈
int var = id;
while (links.get(var) != var) {
stackList.add(var);
var = links.get(var);
}
for (int i = 0; i < stackList.size(); i++) { //路径压缩到一个点
links.put(stackList.get(i), var);
}
return var;
}
如此,我们只需要在\(ap\)加入人时在并查集结构中新增一个父节点为自己的元素、在\(ar\)时调用\(merge\)函数合并两个节点的父节点即可。在\(qci\)指令中返回\(find(id1) == find(id2)\)而在\(qbs\)指令中返回维护好的\(blockSum\)变量即可。
hw10
基本要求
继续按照JML规格实现\(qgps\)、\(qgvs\)、\(qgav\)、\(am\)、\(sm\)、\(qsv\)、\(qrm\)、\(qlc\)指令
基本设计(架构&图模型构建与维护)
需要注意的指令有\(qgvs\)、\(qgav\)、\(qlc\)。其余指令按照JML的描述进行实现即可。
\(qgvs\)的JML规格是一个\(O(n^2)\)的指令,按照JML规格实现很可能有\(TLE\)(尤其是互测中)的风险,这里我有两种思路,一种是以在\(ag、ar、dfg\)等指令中维护群组的\(valueSum\)属性,以提升提升部分复杂度\(O(n)\)为代价削减\(qgvs\)为\(O(1)\);另一种是只在群组中遍历每个人的熟人进行判断,即将\(O(n^2)\)优化为\(O(V+E)\),可以在强测互测的数据条数背景下避免问题。最后由于实现的复杂程度就是懒我选择了后者。
\(qgav\)指令的JML规格同样是\(O(n^2)\),需要优化,这里我们可以通过维护群组中年龄的总和实现维护平均值变量的\(O(1)\)从而降低方差为\(O(n)\)达到可接受水平。另外我们可以通过简单的数学展开实现维护群组的年龄总和及年龄平方总和实现动态维护方差为\(O(1)\)。不过需要注意的是,下取整在遇到负数时的行为和正数“不太一样”,故直接消去分子和分母的相同项可能导致最终结果差1。
最后是\(qlc\)指令,阅读JML规格即实现最小生成树。这里可以选用的有普里姆算法和克鲁斯卡尔算法,但朴素算法的复杂度均为\(O(n^2)\),需要进行优化。思路有堆优化的普里姆算法和利用并查集优化的的克鲁斯卡尔算法,由于高工数据结构啥也没学会根本不知道堆优化咋写,并查集的函数在上一次作业中已经实现过了,我选择了克鲁斯卡尔算法。又因为需要对“边”这一属性进行操作,故结合\(Network\)类在测试时唯一的特点,我直接在\(Network\)中构建静态内部类\(Edge\),并构建\(ArrayList\)容器存储边。每次进入\(qlc\)函数首先对边进行排序,再遍历全局并查集遍历所有人取出目标点建立内部临时并查集,并维护一个计数器变量,初始值等于目标点数,接着开始有序遍历边,并不断更新并查集,每合并一个点直到计数器减到1为止,说明构建完成,返回结果退出函数。整体而言,根据理论复杂度在\(O(nlogn)\)量级。
\(qlc\)的核心代码如下:
public int queryLeastConnection(int id) throws PersonIdNotFoundException {
Collections.sort(edges); //对边排序
int result = 0;
if (people.containsKey(id)) {
HashMap<Integer, Integer> nodes = new HashMap<>(); //新建临时并查集
int root = find(id);
for (int var: people.keySet()) { //遍历全局并查集找到全部目标点
if (find(var) == root) {
nodes.put(var, var); } }
int blocks = nodes.size(); //计数器,尽量减少遍历长度
for (Edge edge: edges) {
if (nodes.containsKey(edge.getNode1()) && nodes.containsKey(edge.getNode2())) {
if (innerMerge(edge.getNode1(), edge.getNode2(), nodes)) {
result += edge.getWeight();
blocks -= 1;
if (blocks == 1) {
return result; } } } }
return result;
} else {
throw new MyPersonIdNotFoundException(id); }
}
hw11
基本要求
继续按照JML规格实现\(arem\)、\(anm\)、\(cn\)、\(aem\)、\(sei\)、\(sei\)、\(qp\)、\(dce\)、 \(qm\)、 \(sim\)指令,并对之前的\(sm\)、\(am\)指令稍作修改。
基本设计(架构&图模型构建与维护)
本次的实现中有非常多的细节,需要注意细节,仔细阅读JML规格。例如dce指令的清除对象、cn指令的指针操作等等。
算法上需要特别注意的是\(sim\)指令,按照JML规格即为实现两点间的最短路径计算,并进行一个\(sm\)的操作。我选择了堆优化的迪杰特斯拉算法。具体到实现上,即为通过\(priorityQueQue\)实现一个小根堆优化朴素算法中的查询选择新增扩展边的操作。首先需要遍历所有人取出最短路上可能经过的人,另外利用两个哈希表容器存储各个节点的扩展情况和已知可达距离。当扩展到目标点时,即退出最小路算法块进行消息的发送部分。根据理论,整体的复杂度为\(O(nlogn)\)量级。不过根据自我对拍和强测第6个点结果,同样的堆优化,我要慢很多,虽然看了下不会T就压根没管。核心代码部分如下(确实是附近认识所有人里写的最丑的):
HashMap<Integer, Integer> recordDistance = new HashMap<>(); //记录已知最短距离
HashMap<Integer, Integer> recordVisited = new HashMap<>(); //记录访问情况,即未访问过、未扩展、已扩展
PriorityQueue<Relation> heap = new PriorityQueue<>();
int root = find(p1.getId());
for (int i :people.keySet()) { //取出所有可能经历的点
if (find(people.get(i).getId()) == root) {
recordVisited.put(people.get(i).getId(), null); //null为未访问、本身为已扩展、其它为为扩展
recordDistance.put(people.get(i).getId(), -1); } }
recordVisited.put(p1.getId(), p1.getId());
recordDistance.put(p1.getId(), 0);
MyPerson nowNode = (MyPerson) p1;
HashMap<Person, Integer> edges = nowNode.getAcquaintanceAndValue();
for (Person p: edges.keySet()) { //第一轮初始化(这段代码其实可以合并到底下)
int tmpId = p.getId();
if (recordVisited.get(tmpId) == null) {
heap.add(new Relation(tmpId, edges.get(p)));
recordDistance.put(tmpId, edges.get(p));
recordVisited.put(tmpId, nowNode.getId()); } }
while (true) { //循环扩展
while (! heap.isEmpty() && (recordVisited.get(heap.peek().getNode()) != null)
&& recordVisited.get(heap.peek().getNode()) == heap.peek().getNode()) {
heap.poll(); }
Relation newRe = heap.poll(); //最小队列弹出最小边
nowNode = (MyPerson) people.get(newRe.getNode());
if (nowNode == p2) { //退出条件为扩展到目标点
result = newRe.distance;
break; } else {
recordVisited.put(nowNode.getId(), nowNode.getId());
edges = nowNode.getAcquaintanceAndValue(); //遍历新边,入堆
for (Person p: edges.keySet()) {
if (recordVisited.get(p.getId()) == null) {
recordVisited.put(p.getId(), nowNode.getId());
recordDistance.put(p.getId(), edges.get(p) +
recordDistance.get(nowNode.getId()));
heap.add(new Relation(p.getId(), edges.get(p)
+ recordDistance.get(nowNode.getId()))); } else {
if (newRe.getDistance() + edges.get(p) < recordDistance.get(p.getId())) {
recordDistance.put(p.getId(), newRe.getDistance() + edges.get(p));
heap.add(new Relation(p.getId(),
newRe.getDistance() + edges.get(p))); } } } } }
自测策略&Hack分析
Hack分析
三次作业中强测及互测未出现正确性或\(TLE\)问题。
互测情况:hw9通过\(qbs\)指令hack到2人;hw10通过\(qgvs\)指令hack到2人;hw11通过\(qbs\)指令hack到1人。(这次没hack全,因为忙着跑路)另外,身边大多数人的问题都是算法的复杂度问题,按照指令数数据限制和CPU时间限制,所有指令的复杂度只要不超过\(O(n^2)\)就好。
互测策略:由于自己搭建了评测机,只需要打好jar包扔进去,再准备一些针对复杂度可能大于等于\(O(n^2)\)的边界压力测试数据即可。事实上能hack到人的大都是精心设计的边界压力测试数据,因为互测数据的限制比较严格。
评测机构建思路
这三次作业由于网课终于有时间了由于输入指令简单,我便首次构建了简单的评测机(其实是对拍器),采用python语言编写,大体逻辑如下:commands.py负责生成单条指令,get_data.py负责调用commands.py生成不同的测试样例数据,而judge.py负责循环生成测试点并调用jar包进行对拍。相较于同班dl的纯随机数据,我分为了network稀疏图、稠密图等固定测试点,整体更有针对性但覆盖可能不是很全面。这里取第二次作业的部分代码举例如下:
首先是get_data.py,调用的gen_*函数为根据概率随机生成异常或正常数据,而gen_all_*则为遍历性地不发生异常的生成覆盖当前network实例下的所有相关指令测试,一方面避免有正确性错误,另一方面也方便测试时间复杂度。
import commands as s # 调用生成单条指令的commands.py
def get_line(function, f, data_input): # 集成性通过函数指针调用各种单条生成逻辑
line = function()
f.write(line)
data_input = data_input + line
return data_input
def gen_data_test_network_dense(): # 稠密图相关指令测试点
s.init()
f = open('stdin.txt', 'w')
data_input = ''
for _ in range(100): # 初始化person
data_input = get_line(s.gen_ap, f, data_input)
for _ in range(10000): # 大量添加关系
data_input = get_line(s.gen_ar, f, data_input)
for _ in range(3): # network方法测试
data_input = get_line(s.gen_all_qv_qlc_qci, f, data_input)
data_input = get_line(s.gen_qps, f, data_input)
data_input = get_line(s.gen_qbs, f, data_input)
for __ in range(10):
data_input = get_line(s.gen_ap, f, data_input)
for __ in range(1000):
data_input = get_line(s.gen_ar, f, data_input)
for __ in range(5):
data_input = get_line(s.gen_qv, f, data_input)
data_input = get_line(s.gen_qlc, f, data_input)
data_input = get_line(s.gen_qci, f, data_input)
data_input = get_line(s.gen_all_qv_qlc_qci, f, data_input) # 生成当前实例下测试可能的所有qv、qlc、qci指令集
data_input = get_line(s.gen_qps, f, data_input)
data_input = get_line(s.gen_qbs, f, data_input)
f.close()
return data_input
接着是commands.py,取部分函数如下:
# 各种全局变量
alphabet = [chr(_) for _ in range(65, 127)]
people_list = []
people_linked_list = {}
group_list = []
group_people_list = {}
message_list = {}
# 每次新生成测试点时要进行的初始化操作
def init():
people_list.clear()
people_linked_list.clear()
group_list.clear()
group_people_list.clear()
message_list.clear()
# 单条指令生成操作
def gen_qgvs():
p = random.random()
if p < 0.98 and len(group_list) != 0:
id = random.choice(people_list)
else:
id = random.randint(1, 500)
return 'qgvs ' + str(id) + '\n'
# 一个全覆盖性测试指令集生成
def gen_all_qv_qlc_qci():
result = ''
for id in people_list:
result += 'qlc ' + str(id) + '\n'
for i in range(len(people_list)):
for j in range(i + 1, len(people_list)):
result += 'qci ' + str(people_list[i]) + ' ' + str(people_list[j]) + ' ' + '\n'
result += 'qv ' + str(people_list[i]) + ' ' + str(people_list[j]) + ' ' + '\n'
return result
最后最顶层的测试方面,照抄了网络上的popen方法,每次建立临时的各个jar包的输出文件,从第一个到最后一个逐行比较,有不同之处直接另存输出文件、输入文件并注明异常行数即可。核心代码如下:
from os import popen
import get_data
import time
jars = ['jbt', 'Archer', 'Assassin', 'Breserker', 'Caster', 'Rider'] # 要对拍的jar包
txts = [i + '.txt' for i in jars]
tle_flags = [False for _ in jars] # 超时标准(单线程直接用time库了)
test_types = ['Group_Var', 'Group_qvs', 'Test_Weak', 'Test_Mid', 'Group&Message', 'G&S(parse)',
'Net(dense)', 'Net(sparse)', 'Net(normal)'] # 测试点
times = 0
err_times = 1
while True: # 无限制轮回测试
for i in range(len(tle_flags)):
tle_flags[i] = False
if times == 0:
data_input = get_data.gen_limit_group_var()
elif times == 1:
data_input = get_data.gen_limit_group_sum_value()
elif times % 7 == 2:
........ # 选择测试点进行数据生成
# 向标准输出打印测试次数与类型
print('============time' + str(times) + ' ' + test_types[times if times == 0 or times == 1 else (times - 2) % 7 + 2] + ' ============')
for i in range(len(jars)): # 核心导入测试数据并保存临时输出的逻辑
start_time = time.time()
sub = popen('java -jar ' + jars[i] + '.jar > ' + jars[i] + '.txt', 'w')
sub.write(data_input)
sub.flush()
sub.close()
end_time = time.time()
print(jars[i] + ' => ' + str(round(end_time - start_time, 2)))
........ # 省略对拍逻辑
三次作业中,前两次我的程序较为完备,第三次数据则较弱 (ddl又糊脸了不能拖延.jpg)。由于初写评测机经验也不是十分丰富,最底层的单条指令生成函数实际上可扩展性不强,且迭代开发中指令间的关系改变时也不能很好地做出调整。因此有dl同学要走我写的屎山后着手重写,评价"单条指令构建写的好烂“@zxy
意外的是,第二次作业由于我构建了一个二十多万条数据的庞大测试点,发现别人10s左右运行完的程序我需要30s甚至更多,但其它数据量相对更小的测试点则没有这个问题。经过从头到尾检查逻辑后锁定问题在System.out.printf和Sytem.out.println的性能差距上。不过在替换后(气死我了)我并没有再去深究这个问题。或许猜想与String的不可变有关?
Network扩展
首先,需要添加一些对应的类,由于买卖商品需要流通“产品",发广告和购买产品需要信息。故我们要新建Advertiser、Producer、Customer、AdvertisementMessage、purchaseMessage、product这几个类(当然为了简单我们可以直接将广告和商品本身也抽象为信息)。在Network中对应的方法如下,有JML规格的方法为选取撰写的方法。(新增类暂且不表)
//只写出新增部分
public interface Network {
/*@ public instance model non_null Advertisement[] advertisements;
*@ public instance model non_null Product[] products;
@*/
//发送广告
/*@ public normal_behavior
@ requires AdcontainsMessage(id) && getMessage(id) instance of AdvertisementMessage &&
@ getMessage(id).getPerson1() instancedof Advertiser && getMessage(id).getMessage.getType() == 0
@ assignable 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 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) == \old(getMessage(id));
@ also
@ public normal_behavior
@ requires AdcontainsMessage(id) && getMessage(id) instance of AdvertisementMessage &&
@ getMessage(id).getPerson1() instancedof Advertiser && getMessage(id).getMessage.getType() == 1 &&
@ getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1());
@ 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 < getMessage(id).getGroup().getPeople().size(); (
@ (\forall int j; 0 <= j && j < \old(getMessage(id).getGroup().getPeople()
@ [i].getMessages().size());
@ \old(getMessage(id).getGroup().getPeople()[i].getMessages().get(j+1)) ==
@ \old(getMessage(id).getGroup().getPeople()[i].getMessages().get(j)))));
@ ensures \old(getMessage(id)).getGroup().getPeople()[i].getMessages().get(0) == \old(getMessage(id));
@ also
@ public exceptional_behavior
@ assignable messages;
@ signals (MessageIdNotFoundException e) ! AdcontainsMessage(id);
@ signals (PersonIdNotFoundException e) AdcontainsMessage(id) &&
@ !getMessage(id).getPerson1() instance of Advertiser);
@ signals (RelationNotFoundException e) AdcontainsMessage(id)
@ && getMessage(id).getType() == 0 &&
@ !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
@ signals (PersonIdNotFoundException e) AdcontainsMessage(id) && getMessage(id).getType() == 1 &&
@ !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
@*/
public void sendAdvertisement(int id) throws MessageIdNotFoundException,PersonIdNotFoundException, RelationNotFoundException;
//购买商品(实现中采用的JML是广告消息里记录了生产者的id)
/*@ public normal_behavior
@ requires contains(id1) && getMessages(id1).contains(id2) &&
@ ProducerContains(getMessages(id1).get(id2).getProduct().getProducerId()) &&
@ isCircle(getPerson(id1), getPerson(getMessages(id1).get(id2).getProduct().getProducerId()));
@ assignable getPerson(id1).money,
@ getPerson(getMessages(id1).get(id2).getProduct().
@ getProducerId()).money,getMessages(id1).products,
@ getPerson(getMessages(id1).get(id2).getProduct().getProducerId()).products();
@ ensures getPerson(id1).getMoney() = \old(getPerson(id1).getMoney()) -
@ getMessages(id1).get(id2).getProduct().getValue();
@ ensures getPerson(getMessages(id1).get(id2).getProduct().getProducerId()).getMoney() =
@ \old(getPerson(getMessages(id1).get(id2).getProduct().getProducerId()).getMoney()) -
@ getMessages(id1).get(id2).getProduct().getValue();
@ ensures getPerson(id1).getProducts.size() == \old(getPerson(id1).getProducts.size()) + 1;
@ ensures \forall(int i; 0 <= i < \old(getPerson(id1).getProducts.size());
@ getPerson(id1).getProducts[i + 1] == \old( getPerson(id1).getProducts[i]))
@ ensures getPerson(id1).getProducts[0] == getMessages(id1).get(id2).getProduct();
@ ensures getPerson(getMessages(id1).get(id2).getProduct().getProducerId()).getProducts.size() =
@ \old(getPerson(getMessages(id1).get(id2).getProduct().getProducerId())).getProducts.size() - 1;
@ ensures \forall(int i; 0 <= i <
@ \old(getPerson(getMessages(id1).get(id2).getProduct().getProducerId()).getProducts.size() - 1;
@ (\exists int j; 0 <= j <
@ \old(getPerson(getMessages(id1).get(id2).getProduct().getProducerId()).getProducts.size());
@ getPerson(getMessages(id1).get(id2).getProduct().getProducerId()).getProducts[i] == \old
@ (getPerson(getMessages(id1).get(id2).getProduct().getProducerId()).getProducts[j])));
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !contains(id1)
@ signals (MessageIdNotFoundException e) contains(id1) && !getMessages(id1).contains(id2)
@ singals (ProductIdNotFoundException e) contains(id1) && getMessages(id1).contains(id2) &&
@ ! ProducerContains(getMessages(id1).get(id2).getProduct().getProducerId())
@ signals (RealtionNotFoundExcption e) contains(id1) && getMessages(id1).contains(id2) &&
@ ProducerContains(getMessages(id1).get(id2).getProduct().getProducerId()) &&
@ ! isCircle(getPerson(id1), getPerson(getMessages(id1).get(id2).getProduct().getProducerId()));
@*/
public void purchaseProduct(int id1, int id2) throws PersonIdNotFoundException, MessageIdNotFoundException, ProductIdNotFoundException, RealtionNotFoundException;
//查询商品价格
/*@ public normal_behavior
@ requires contains(id1) && getPerson(id1).Productcontains(id2);
@ assignable \nothing;
@ ensures \result == getPerson(id1).getProduct(id2).getValue();
@ also
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) ! contians(id1);
@ signals (ProductIdNotFoundException) contians(id1) && ! getPerson(id1).Productcontains(id2);
@*/
public int queryProductValue(int id1, int id2) throws PersonIdNotFoundException,ProductIdNotFoundException;
}
心得体会
首先是契约式编程思想和JML规格方面,JML规格十分严谨,用来传达程序要做什么这一点上无可比拟。但他也只是告诉我们“目标是什么”而非“我们要怎么做”,因此我们还要根据自己的理解选择自己的数据结构并选择合适的算法。但这JML以后还是要花大半页讲一个用自然语言不难说清楚的事情的话,似乎确实有点折磨人......
但这单元给我印象最深的其实是三次作业的不同图算法。对于数据结构课程上没有得到很好训练的我来说,这一单元真真正正让我对时间复杂度有了直观认识。也直观认识到了各种图算法的应用场景。事实上在本学期的数学建模课程和离散数学二课程中,我也了解了一些更高级的图算法,但真正在实际背景而非抽象背景下实现还是第一次。尤其是堆优化的迪杰特斯拉算法中,我花了很久才明白由于是小根堆,直接将更新后的距离数据扔进堆里即可。了解了的算法和实现过的算法还是有许多差距的,这也体现了我在数据结构和算法方面受训练的严重不足,在未来还需要多下功夫。