第三单元总结
北京航空航天大学计算机学院 2019春季
17373468 赵子敬
单元主题:规格的理解、撰写和使用(图和地铁运行系统)
第一次作业:实现两个容器类Path
和PathContainer
,JML
规格入门级的理解和代码实现。
第二次作业:实现容器类Path
和数据结构类Graph
,JML
规格进阶级的理解和代码实现、设计模式的简单实战,以及单元测试的初步使用尝试。
第三次作业:实现容器类Path
,地铁系统类RailwaySystem
,规格进阶级的理解和代码实现、设计模式和单元测试的进阶级实战。
一、JML
语言的理论基础、应用工具链情况
(本段较多引用网络教程,链接:)
面向对象分析和设计的原则之一就是应当尽可能地把过程设想往后推。我们大多数人只在实现方法之前遵守这一规则。一旦确定了类及其接口并该开始实现方法时,我们就转向了过程设想。那么到底有没有别的选择?和大多数语言一样,编写 Java 代码时,我们需要为计算每个方法的结果一步一步地提供过程。
就其本身而言,过程化表示法只是说 如何做某事,却不曾说过我们正在设法做 什么。在动手之前了解我们想要取得的结果,这可能会有用,但是 Java 语言没有提供显式地将这种信息合并到代码中的方法。
Java 建模语言(JML
)将注释添加到 Java 代码中,这样我们就可以确定方法所执行的内容,而不必说明它们如何做到这一点。有了JML
,我们就可以描述方法预期的功能,无需考虑实现。通过这种方法,JML
将延迟过程设想的面向对象原则扩展到了方法设计阶段。
JML
编译器可以生成类文件,这些文件可以在运行时自动检查 JML
规范。如果程序没有完成其 JML
注释中规定要做的事,则 JML
会抛出一个未经检查的异常,指出违反了哪部分规范。这在捕获错误或帮助使文档(JML
注释格式)与代码保持同步方面都非常有用。
JML
运行时断言检查编译器是数目不断增多的使用 JML
的工具中的第一个。其它工具包括 jmldoc
和 jmlunit
。 jmldoc
类似于 javadoc
,不过它还在生成的 HTML 文档中包含了 JML
规范。 jmlunit
生成测试案例类的框架,它使 JML
能方便地与 JUnit
结合使用。
二、【选做】部署 SMTSolver
,至少选择3个主要方法来尝试进行验证,报告结果
三、部署 JMLUnitNG
/JMLUnit
,针对 Graph
接口的实现自动生成测试用例, 并结合规格对生成的测试用例和数据进行简要分析
本次作业我没有使用 JMLUnit
进行测试,只是使用了 JUnit
,针对规格实现了单元测试,在此简单梳理随机用例测试代码。
private static final int maxPathSize = 80; private static final int maxDistinctNode = 120; private RailwaySystem system = new MyRailwaySystem(); private ArrayListnodeSet = new ArrayList<>(); private ArrayList pathSet = new ArrayList<>();
上面这段定义测试类全局变量,maxPathSize
和 maxDistinctNode
两个常量用来便于修改测试过程中数据范围边界;system
实例化一个 RailwaySystem
类,作为测试的对象; nodeSet
和 pathSet
分别用来存储测试类生成的路径和节点。
private Path generateRadomPath() { int pathSize = (int)(2 + Math.random() * (maxPathSize - 1)); ArrayListpathNodeList = new ArrayList<>(); for (int i = 0; i < pathSize; i++) { int flag = (int)(Math.random() * 2); if ((nodeSet.size() == maxDistinctNode || flag == 0) && nodeSet.size() != 0) { int index = (int)(Math.random() * nodeSet.size()); Integer node = nodeSet.get(index); pathNodeList.add(node); } else { Integer newNode = (int)(Integer.MIN_VALUE + Math.random() * ((long)Integer.MAX_VALUE - (long)Integer.MIN_VALUE + 1)); pathNodeList.add(newNode); nodeSet.add(newNode); } } int[] pathList = new int[pathNodeList.size()]; for (int i = 0; i < pathNodeList.size(); i++) { pathList[i] = pathNodeList.get(i); } System.out.println("adding path, path size: " + pathSize); System.out.println(pathNodeList); return new MyPath(pathList); }
上面这段为随机生成路径。首先路径长度是在最大范围内随机的,其次循环生成在int范围内随机的节点。为了保证整个系统中的节点数不超过限制,在生成路径时即存储所有节点,若超过限制则在已有的节点里寻找节点构建新路径。最后打印生成路径信息。
private Path removeRandomPath() { int pathIndex = (int)(Math.random() * pathSet.size()); Path toBeRemoved = pathSet.get(pathIndex); System.out.println("the following path has been removed."); System.out.println(toBeRemoved); pathSet.remove(pathIndex); nodeSet.clear(); int i; int j; for (i = 0; i < pathSet.size(); i++) { for (Integer node : pathSet.get(i)) { if (!nodeSet.contains(node)) { nodeSet.add(node); } } } return toBeRemoved; }
上面这段为随机删除路径。在已有的路径随机选择一个下标将对应的路径删除,随即更新节点集信息。
@Before public void setUp() throws Exception { int addNum = (int)(1 + Math.random() * 50); for (int i = 0; i < addNum; i++) { Path newPath = generateRadomPath(); pathSet.add(newPath); system.addPath(newPath); } int removeNum = (int)(Math.random() * addNum); for (int i = 0; i < removeNum; i++) { Path toBeRemoved = removeRandomPath(); system.removePath(toBeRemoved); } System.out.println("system has been built."); System.out.println("total paths: " + pathSet.size()); System.out.println("distinct node count: " + nodeSet.size()); }
上面这段是 JUnit
框架的 @Before
模块,在单元测试之前初始化测试类。在此仍然采用随机的方式,在增删指令数限定范围内随机增加路径,随后在不超过已有路径数量的范围内随机删除路径,最终打印现有地铁系统路径数量和节点数量信息,在测试之前建立好地铁系统。
@Test public void containsNode() { int index = (int)(Math.random() * nodeSet.size()); assert system.containsNode(nodeSet.get(index)); System.out.println("correct: " + nodeSet.get(index) + " is in the system."); int node = (int)(Math.random() * Integer.MAX_VALUE); while (nodeSet.contains(node)) { node = (int)(Math.random() * Integer.MAX_VALUE); } assert system.containsNode(nodeSet.get(index)); System.out.println("correct: " + node + " is not in the system."); }
上面这段是 JUnit
单元测试针对 containsNode
接口的测试。此处由于我们已经在测试类存储了所有节点,因此只是从测试类取出了已有节点或随机生成新节点,无需按照规格遍历所有路径即可找到测试节点。上面这段代码从正反分别验证,在系统中的节点和不在系统中的节点均有测试。
@Test public void containsEdge() { int index = (int)(Math.random() * pathSet.size()); Path testPath = pathSet.get(index); index = (int)(Math.random() * (testPath.size() - 1)); int fromNode = testPath.getNode(index); int toNode = testPath.getNode(index + 1); assert system.containsEdge(fromNode, toNode); System.out.println("correct: edge (" + fromNode + ", " + toNode + ") is in the system"); index = (int)(Math.random() * nodeSet.size()); fromNode = nodeSet.get(index); index = (int)(Math.random() * nodeSet.size()); toNode = nodeSet.get(index); boolean flag = true; while (flag) { flag = false; for (Path path : pathSet) { for (int i = 0; i < path.size() - 1; i++) { if ((path.getNode(i) == fromNode && path.getNode(i + 1) == toNode) || (path.getNode(i) == toNode && path.getNode(i + 1) == fromNode) || fromNode == toNode) { flag = true; break; } } if (flag) { break; } } if (flag) { index = (int)(Math.random() * nodeSet.size()); toNode = nodeSet.get(index); } } assert !system.containsEdge(fromNode, toNode); System.out.println("correct: edge (" + fromNode + ", " + toNode + ") is not in the system"); }
上面这段是 JUnit
单元测试针对 containsEdge
接口的测试。这段测试严格遵循了规格,按照规格的定义对边进行了测试,同样是从正反两方面验证,首先在已有路径随机选择一条路,再从中随机选择一条边取出进行测试,确保这条边是属于系统的;而另一方面寻找不属于系统的边则比较麻烦,需要首先随机寻找节点,再遍历系统确保所有路径都不包含这条边。
还有更多接口的单元测试,代码过于冗长在此不再赘述。以 containsEdge
为例,测试的输出信息如下:
... junit4 MyRailwaySystemTest,containsEdge adding path, path size: 22 [1232241898, 1232241898, 378328260, 1232241898, 1232241898, 378328260, 1059498109, 1059498109, 1059498109, 1232241898, 513409004, -1001154432, -1001154432, -1001154432, 1059498109, -1151735537, 1478286012, -2011719206, -1151735537, -338002509, -1719290353, -1077194302] adding path, path size: 8 [-1009325207, -1009325207, 280639520, 378328260, 1640517423, -1009325207, -358986601, 1059498109] adding path, path size: 42 [966545447, 378328260, 1215082132, -1922842082, -1922842082, -1009325207, 378328260, -1151735537, -1009325207, -358986601, -1061266839, -1719290353, 410078478, 2085504756, 1232241898, 280639520, -2060925210, -369734078, 1223397009, -426274874, 1223397009, 933549319, -761667280, -271824825, 467283959, -1365251337, 1032049891, -1009325207, 1190892105, -1151735537, 1478286012, 1232241898, -1077194302, 1288726621, 1313963050, -1612728892, -73540680, -1719290353, 1640517423, -1873436263, -426274874, -426274874] adding path, path size: 54 [1690346573, 402468808, -1873436263, -1873436263, 1232241898, -1061266839, 1640517423, 1475715686, 1977518312, 1575320703, -413736847, -1505480721, 1288726621, -1723345819, -73540680, 723556111, 1886070244, 467283959, 2015089536, -1719290353, -1061266839, 1478286012, 1762562422, -1719290353, -1001154432, 2015089536, 11719880, 1919254022, -1922842082, -87287690, -1503898901, -1489990093, 1882643122, 1288726621, 1139289371, 2015089536, -1365251337, 2101859962, -269155122, -966483414, 1575320703, 723556111, 410078478, -910520886, -271824825, -1422471783, 1575320703, -1422471783, 1139289371, 257175990, -1503898901, 2052736417, 1690346573, -1723345819] adding path, path size: 58 [-583849437, 1190892105, -1196197506, -910520886, -1756866206, 273438400, -1814841704, 725680019, -73540680, -506344367, -73540680, -1365251337, -309934114, 465396124, 1223397009, -860023481, -369734078, 1204976612, 1762562422, -25286283, 467283959, 1886070244, 1736431410, -1968909372, 1478286012, 2052736417, 755188845, -2060925210, 1690922899, -1756866206, 1762562422, 816076988, -910520886, 723556111, 151760211, -2011719206, -1505480721, 1357764646, -301487943, 2085504756, -354252823, 1554967211, -1008226810, 933549319, 1221881081, 1191566630, -87287690, 755188845, -641269296, -1513703690, -830510707, -90020217, 437843332, -271824825, -768990883, 228240350, -1505480721, 1223397009] adding path, path size: 9 [-70177856, 1278044153, -269155122, 816076988, -1263627592, 180011778, 11719880, -271824825, -34994370] adding path, path size: 28 [1762562422, -420234991, 755188845, 1167139935, -377147417, 549200588, 281646107, 1118305039, -1429609969, -1632813850, -1873436263, -562522988, -1922842082, -510676862, -761667280, 1357764646, -1151735537, 876397354, 419176848, 941885397, 1518270263, -309934114, 1091165502, 1221881081, 1993619079, 1810909193, -625828750, 1052359578] adding path, path size: 55 [1690346573, -73540680, -1758621909, 1575320703, 1052359578, -301487943, -1873436263, 2015089536, 1223397009, 1478286012, -1723345819, 1167139935, 1191566630, -641269296, -1009325207, 816076988, 1118305039, 1091165502, 1690346573, -1489990093, -269155122, -1077194302, 1690346573, 11719880, -73540680, -966483414, -506344367, -1756866206, 1640517423, 281646107, 1059498109, -1873436263, -70177856, 437843332, 1288726621, -1196197506, 1810909193, 437843332, 1191566630, -1612728892, -761667280, 2101859962, -1489990093, -1968909372, 419176848, 1288726621, 1052359578, 281646107, -1719290353, -1196197506, -562522988, -1756866206, 228240350, -1263627592, 1882643122] adding path, path size: 35 [1221881081, -506344367, -338002509, -1503898901, -761667280, -1489990093, 410078478, 1690922899, -1719290353, 180011778, 1919254022, 513409004, 1052359578, -860023481, -910520886, 1091165502, 402468808, -271824825, 257175990, -358986601, 1478286012, 876397354, 1215082132, -1151735537, -377147417, 1139289371, -625828750, 410078478, -301487943, 755188845, -562522988, -25286283, 437843332, 2052736417, -1719290353] adding path, path size: 19 [-1719290353, 1518270263, 1993619079, -358986601, 257175990, -583849437, 257175990, 378328260, 1882643122, 1810909193, 281646107, -358986601, 1575320703, -1814841704, 151760211, 11719880, -269155122, -338002509, -761667280] adding path, path size: 48 [1215082132, 467283959, 1690922899, 1215082132, 933549319, 280639520, -354252823, 1554967211, 1690346573, 273438400, -25286283, 1223397009, 941885397, 1059498109, 280639520, -761667280, -426274874, 378328260, -1719290353, -413736847, 1554967211, -1968909372, -1365251337, 1993619079, -966483414, -1196197506, -1503898901, 273438400, 1690922899, 1313963050, 1288726621, -1196197506, -338002509, 1139289371, -1758621909, 1091165502, 1690346573, -1632813850, -269155122, 1690922899, 1278044153, -301487943, -301487943, -641269296, -641269296, -426274874, 410078478, 2052736417] adding path, path size: 76 [-1489990093, -269155122, 1313963050, 281646107, 1204976612, 1139289371, 281646107, -1077194302, 437843332, -73540680, 1221881081, -269155122, -830510707, 941885397, 1475715686, 1518270263, -338002509, 1032049891, 1190892105, 1139289371, 1278044153, 1640517423, -1365251337, -1489990093, 402468808, -358986601, 1575320703, 1204976612, 1993619079, 966545447, 11719880, -1196197506, -1429609969, 941885397, -1365251337, 1690346573, 1191566630, 1554967211, 933549319, 1313963050, -1429609969, -1719290353, 1223397009, -1814841704, -87287690, 1640517423, -1009325207, -1632813850, -2011719206, 1475715686, -73540680, -1922842082, -506344367, -309934114, -562522988, -1365251337, 1191566630, 755188845, -966483414, -1968909372, 257175990, -910520886, 1221881081, 723556111, 933549319, 419176848, -1365251337, -583849437, -70177856, 281646107, -910520886, -1503898901, -1723345819, -1489990093, 1762562422, 280639520] adding path, path size: 27 [-768990883, 549200588, 1762562422, -271824825, 1032049891, -1196197506, -562522988, 1575320703, -1263627592, -1001154432, -73540680, -1873436263, -301487943, -1922842082, -1612728892, 1221881081, 273438400, 1232241898, -354252823, -2011719206, 437843332, 1357764646, -420234991, 1736431410, -1422471783, 1575320703, -506344367] adding path, path size: 72 [1977518312, -87287690, -1503898901, 1052359578, -420234991, 933549319, -1632813850, -1009325207, 151760211, 966545447, 2085504756, -1758621909, 1575320703, 1190892105, -1263627592, -369734078, -1513703690, -301487943, 755188845, -271824825, -1503898901, 1288726621, 1882643122, -420234991, -34994370, 1690922899, -625828750, -761667280, 1554967211, 933549319, 1091165502, 273438400, 1518270263, -354252823, 1478286012, 1762562422, -1196197506, -1263627592, 378328260, -1503898901, 410078478, 1810909193, 1690346573, 513409004, -562522988, -1077194302, 410078478, -1873436263, 1091165502, 1091165502, -301487943, 228240350, -413736847, -1008226810, -1422471783, -1632813850, -338002509, -1489990093, -369734078, -1968909372, 1518270263, -830510707, 1288726621, -1422471783, 2101859962, 1215082132, 1357764646, 1977518312, 1288726621, 933549319, -562522988, -506344367] the following path has been removed. [1215082132, 467283959, 1690922899, 1215082132, 933549319, 280639520, -354252823, 1554967211, 1690346573, 273438400, -25286283, 1223397009, 941885397, 1059498109, 280639520, -761667280, -426274874, 378328260, -1719290353, -413736847, 1554967211, -1968909372, -1365251337, 1993619079, -966483414, -1196197506, -1503898901, 273438400, 1690922899, 1313963050, 1288726621, -1196197506, -338002509, 1139289371, -1758621909, 1091165502, 1690346573, -1632813850, -269155122, 1690922899, 1278044153, -301487943, -301487943, -641269296, -641269296, -426274874, 410078478, 2052736417] the following path has been removed. [1690346573, -73540680, -1758621909, 1575320703, 1052359578, -301487943, -1873436263, 2015089536, 1223397009, 1478286012, -1723345819, 1167139935, 1191566630, -641269296, -1009325207, 816076988, 1118305039, 1091165502, 1690346573, -1489990093, -269155122, -1077194302, 1690346573, 11719880, -73540680, -966483414, -506344367, -1756866206, 1640517423, 281646107, 1059498109, -1873436263, -70177856, 437843332, 1288726621, -1196197506, 1810909193, 437843332, 1191566630, -1612728892, -761667280, 2101859962, -1489990093, -1968909372, 419176848, 1288726621, 1052359578, 281646107, -1719290353, -1196197506, -562522988, -1756866206, 228240350, -1263627592, 1882643122] the following path has been removed. [1690346573, 402468808, -1873436263, -1873436263, 1232241898, -1061266839, 1640517423, 1475715686, 1977518312, 1575320703, -413736847, -1505480721, 1288726621, -1723345819, -73540680, 723556111, 1886070244, 467283959, 2015089536, -1719290353, -1061266839, 1478286012, 1762562422, -1719290353, -1001154432, 2015089536, 11719880, 1919254022, -1922842082, -87287690, -1503898901, -1489990093, 1882643122, 1288726621, 1139289371, 2015089536, -1365251337, 2101859962, -269155122, -966483414, 1575320703, 723556111, 410078478, -910520886, -271824825, -1422471783, 1575320703, -1422471783, 1139289371, 257175990, -1503898901, 2052736417, 1690346573, -1723345819] the following path has been removed. [-70177856, 1278044153, -269155122, 816076988, -1263627592, 180011778, 11719880, -271824825, -34994370] the following path has been removed. [966545447, 378328260, 1215082132, -1922842082, -1922842082, -1009325207, 378328260, -1151735537, -1009325207, -358986601, -1061266839, -1719290353, 410078478, 2085504756, 1232241898, 280639520, -2060925210, -369734078, 1223397009, -426274874, 1223397009, 933549319, -761667280, -271824825, 467283959, -1365251337, 1032049891, -1009325207, 1190892105, -1151735537, 1478286012, 1232241898, -1077194302, 1288726621, 1313963050, -1612728892, -73540680, -1719290353, 1640517423, -1873436263, -426274874, -426274874] the following path has been removed. [-583849437, 1190892105, -1196197506, -910520886, -1756866206, 273438400, -1814841704, 725680019, -73540680, -506344367, -73540680, -1365251337, -309934114, 465396124, 1223397009, -860023481, -369734078, 1204976612, 1762562422, -25286283, 467283959, 1886070244, 1736431410, -1968909372, 1478286012, 2052736417, 755188845, -2060925210, 1690922899, -1756866206, 1762562422, 816076988, -910520886, 723556111, 151760211, -2011719206, -1505480721, 1357764646, -301487943, 2085504756, -354252823, 1554967211, -1008226810, 933549319, 1221881081, 1191566630, -87287690, 755188845, -641269296, -1513703690, -830510707, -90020217, 437843332, -271824825, -768990883, 228240350, -1505480721, 1223397009] system has been built. total paths: 8 distinct node count: 107 correct: edge (-1922842082, -510676862) is in the system correct: edge (1690922899, 1736431410) is not in the system Process finished with exit code 0
生成的测试数据是完全随机的,这个特性有优势也有劣势。优势在于可以全面地并且随即地测试接口实现,并可以灵活调整测试规模,方便地进行多次不同数据测试。当然,明显的劣势在于随即数据难以很好地兼顾边界和特殊情况,只能通过大量多次测试来提升语句覆盖率,而很难达到语句覆盖100%。
四、按照作业梳理自己的架构设计,并特别分析迭代中对架构的重构
第一次作业:
第一次作业要求比较简单,只需要实现路径( path
)的构造和路径容器( pathContainer
)的构造。pathContainer
需要实现路径的增添、删除、编号,以及一个相对特殊的方法:统计节点数。由于是第一次作业,原本的思路就是按照规格中规中矩地实现了,但是在性能要求的影响下我们发现不可以按照规格一模一样地实现代码,因为采用静态的容器不能满足增删需求,用动态数组统一进行每一个方法的计算查询又会耗费大量时间,这也是我们第一次体会到,规格仅仅用来规范输入输出,而具体的实现是自由的。只要保证与规格的输入输出要求一致,应该尽可能构思性能更好的实现方法。
因此在两个接口实现的过程中,为了提升时间性能我都适当牺牲了空间性能。在 Path
接口的实现过程中我才用了这样的存储:
private ArrayListnodeList = new ArrayList<>(); private HashSet nodeSet = new HashSet<>(); public MyPath(int... nodeList) { for (int node : nodeList) { this.nodeList.add(node); this.nodeSet.add(node); } }
同时使用动态数组和集合,把每一个节点存两遍,这样既保留路径原有节点和顺序信息,也能够方便地查询节点和统计不相同节点数。
在 pathContainer
接口实现中我同样采取了存储多次的方法,将不同类型信息多次存储,已达到快速查询的目的:
private HashMappathMap1 = new HashMap<>(); private HashMap pathMap2 = new HashMap<>(); private static int idCount = 0; private ArrayList nodeList = new ArrayList<>(); private HashSet distinctNodeSet = new HashSet<>(); public MyPathContainer() { }
其中 pathMap1
和 pathMap2
将路径和其编号一一对应地存储,为了使查询更快使用哈希表,而为了能实现路径-编号双向查询我使用了两个哈希表,并为路径重写了 hashCode
和 equals
方法。而为了查询不相同节点,我将所有路径的节点再次存储,这样一来所有查询的复杂度都被降到常数。
第二次作业:
第二次作业的需求比第一次多了一些,主要挑战在于要求查询节点的连通性以及计算最短路径。这两个新增需求看似可以通过少量对第一次代码的扩展来实现,但实际上如果这样做,时间性能将不能够支撑代码通过测试。但看计算最短路径,不论是迪杰斯特拉还是弗洛伊德,时间复杂度都在三次方量级。按照作业需求,高达数千次的查询操作会耗费大量时间。并且如果在动态容器基础上操作,不论是查询计算还是增删,都会有很多Java包调用的开销。因此我采用了多重存储+静态二维数组的方式,并单独封装一类维护图结构。
private static final int SIZE = 400; private HashMapidexMap = new HashMap<>(); private HashMap nodeMap = new HashMap<>(); private int[][] adjMatrix = new int[SIZE][SIZE]; private int[][] dstMatrix = new int[SIZE][SIZE];
这是我单独封装的 Matrix
类全局变量,我使用了静态的二维数组作为增删和查询路径操作的主要对象,而为了让动态变化的节点可以反映在静态的二维数组上,我才用了两个哈希表将节点映射为数组下标,并实现双向索引。而二维数组我也采用了两重存储,dstMatrix
用来存储计算好的两点之间最短距离,而 adjMatrix
则用来存储点的邻接情况。这样一来,只需要在每次增删操作时改变矩阵和映射关系,重新计算更新所有矩阵的信息,将繁杂的计算工作分摊给需求较少的增删操作,而每次的查询无非是访问了一个静态二维数组。
private void setDstMatrix() { int i; int j; int k; for (i = 0; i < SIZE; i++) { for (j = 0; j < SIZE; j++) { if (i == j) { dstMatrix[i][j] = 0; } else if (adjMatrix[i][j] > 0) { dstMatrix[i][j] = 1; } else { dstMatrix[i][j] = -1; } } } for (k = 0; k < SIZE; k++) { for (i = 0; i < SIZE; i++) { for (j = 0; j < SIZE; j++) { int temp = -1; if (dstMatrix[i][k] >= 0 && dstMatrix[k][j] >= 0) { temp = dstMatrix[i][k] + dstMatrix[k][j]; } if (temp >= 0 && (temp < dstMatrix[i][j] || dstMatrix[i][j] < 0)) { dstMatrix[i][j] = temp; } } } } }
上面这段代码是采用弗洛伊德算法更新距离矩阵。我们基于邻接矩阵来更新距离矩阵,初始化过程为,相同节点间距离为0,若两节点邻接则距离为1,其余节点间距离均为无穷(在这里使用了-1)。而弗洛伊德算法,遍历每个节点(变量k表示),对于每个节点,遍历整张图的其他所有节点对(变量i和j表示),查看如果k节点作为中间节点,i和j之间的路径如果通过k到达会不会更短,如果更短则更新i和j之间的距离。弗洛伊德算法的时间复杂度是n^3,理论上比迪杰斯特拉算法要慢,但是在本次作业中实测优于迪杰斯特拉,这可能是由于节点数目多但图较为稀疏的情况下迪杰斯特拉算法时间复杂度的常数项很大,而弗洛伊德能够始终保持稳定的三次方。
第三次作业:
第三次作业的需求比第二次再升一级,需要实现地铁线路中很现实的:最低票价查询、最少换乘查询和最少不满意度查询,以及相对独立的连通块个数查询。题目给出的模型我认为非常符合实际,地铁系统给出两站之间最少换乘是很有必要的功能;票价的模型是两站间距离加上最少换乘数的二倍,这与北京地铁实际票价计价方式一致;不满意度也很符合人们的实际心理,不满意度和路途远近、线路途径站点的平均人流量和换成次数有关,以我个人的经验,从沙河到学院路,在早晚高峰我宁愿选择站数更多的八号线,也不愿意选择在魔鬼西二旗换乘的十三号线。
仔细分析过后我们会发现,前三种查询都涉及到了换乘站的记录,这是我们前两次作业始料未及的。我们在第二次作业计算最短路的时候,不论是弗洛伊德算法还是迪杰斯特拉算法,都无法确定中途哪一站在换乘。而这次三个查询都需要确定,我们所选的这条路有哪些站在换乘。
而这个棘手的问题被王嘉仪同学提出的算法完美解决,即升级邻接矩阵。如果将每条路径也看成图,每条路径内部的联通情况和距离情况将被提前计算好,而原有 Matrix
类的邻接矩阵将被升级为每个单独路径的图的简单叠加。这样一来,不同路径之间的连通性就不会被直接显示在邻接矩阵中。接下来,在计算和换乘有关的最短路时,只需要按照需求分别存储其他的矩阵,基于升级后的邻接矩阵增加边权值即可。
例如在计算最低票价时,对每条Path, 任意两个直接连边,边权修改为距离+2,即在边权中直接加入换乘的钱,然后进行弗洛伊德算法的计算。这样做求出的结果多最后一次换乘的2,减去即可。这个做法能够保证正确性。因为任意一条求出的路径长度都是真实的路径长度+2,和中间的换乘次数无关。且不存在求出的最短路在一条路线上多次换乘的问题,因为每条路径上的任意两点都直接连有边。最少换乘问题,可以将每条边的边权设为1+很大的一个数(大于最多换乘次数,这个数被我设置为700),这样根据最短路的特性,求出最短路后/120即为换乘的次数。最少不满意度可以参考最低票价解决。
private static final int SIZE = 130; private static final int INF = Integer.MAX_VALUE; private static final int MODE = 700; private HashMapidexMap = new HashMap<>(); private HashMap nodeMap = new HashMap<>(); private HashSet pathSet = new HashSet<>(); private int[][] adjMatrix = new int[SIZE][SIZE]; private int[][] dstMatrix = new int[SIZE][SIZE]; private int[][] cntMatrix = new int[SIZE][SIZE]; private int[][] priMatrix = new int[SIZE][SIZE]; private int[][] excMatrix = new int[SIZE][SIZE]; private int[][] upvMatrix = new int[SIZE][SIZE]; private int[] pre = new int[SIZE]; private int cntCount;
以上代码即第三次作业多存储的诸多矩阵。同样,为了查找方便,我们适当地采用空间换时间。
private void setAdjMatrix(Integer lastNode, Integer curnNode) { int last = nodeMap.get(lastNode); int curn = nodeMap.get(curnNode); adjMatrix[last][curn] += 1; adjMatrix[curn][last] += 1;}private void setCntMatrix() { int i; int j; for (i = 0; i < SIZE; i++) { for (j = 0; j < SIZE; j++) { if (i == j) { cntMatrix[i][j] = upvMatrix[i][j] = 0; } else { cntMatrix[i][j] = upvMatrix[i][j] = INF; } } } for (Path path : pathSet) { for (Integer inode : path) { for (Integer jnode : path) { i = nodeMap.get(inode); j = nodeMap.get(jnode); int temp = ((MyPath)path).getUpv(inode, jnode) + 32; if (((MyPath)path).getLength(inode, jnode) < cntMatrix[i][j]) { cntMatrix[i][j] = ((MyPath)path).getLength(inode, jnode); } if (temp < upvMatrix[i][j]) { upvMatrix[i][j] = temp; } } } }}private void setMatrixes() { int i; int j; int k; for (i = 0; i < SIZE; i++) { for (j = 0; j < SIZE; j++) { if (adjMatrix[i][j] > 0) { dstMatrix[i][j] = 1; } else { dstMatrix[i][j] = INF; } if (cntMatrix[i][j] != INF) { priMatrix[i][j] = cntMatrix[i][j] + 2; excMatrix[i][j] = 1 + MODE; } else { priMatrix[i][j] = INF; excMatrix[i][j] = INF; } if (i == j) { dstMatrix[i][j] = 0; priMatrix[i][j] = 0; excMatrix[i][j] = 0; } } } for (k = 0; k < SIZE; k++) { for (i = 0; i < SIZE; i++) { for (j = 0; j < SIZE; j++) { int temp1 = INF; int temp2 = INF; int temp3 = INF; int temp4 = INF; if (dstMatrix[i][k] != INF && dstMatrix[k][j] != INF) { temp1 = dstMatrix[i][k] + dstMatrix[k][j]; } if (priMatrix[i][k] != INF && priMatrix[k][j] != INF) { temp2 = priMatrix[i][k] + priMatrix[k][j]; } if (excMatrix[i][k] != INF && excMatrix[k][j] != INF) { temp3 = excMatrix[i][k] + excMatrix[k][j]; } if (upvMatrix[i][k] < INF && upvMatrix[k][j] < INF) { temp4 = upvMatrix[i][k] + upvMatrix[k][j]; } if (temp1 < dstMatrix[i][j]) { dstMatrix[i][j] = temp1; } if (temp2 < priMatrix[i][j]) { priMatrix[i][j] = temp2; } if (temp3 < excMatrix[i][j]) { excMatrix[i][j] = temp3; } if (temp4 <= upvMatrix[i][j]) { upvMatrix[i][j] = temp4; } } } }}public void addPath(Path path) { pathSet.add(path); Integer lastNode = null; Integer curnNode = null; for (Integer node : path) { curnNode = node; mapId(curnNode); if (lastNode != null && curnNode != null) { setAdjMatrix(lastNode, curnNode); } lastNode = node; } setCntMatrix(); setMatrixes(); setCntCount();}
以上代码即增添路径的过程中对所存储矩阵的操作。
最终是连通快数目的计算,这一部分采用并查集的算法,不再赘述。
三次作业中,重构的方式无非是增添存储来适应更多的需求。不增添存储也是可以的,只是为了满足时间性能需求,我们不能够直接采用规格直接规定的算法,而是要思考更好的实现方式。
五、按照作业分析代码实现的bug和修复情况
说来惭愧,三次作业我一共只有一个愚蠢的bug,而这个bug导致我前两次作业强测都炸到六十多分,它就是:
@Override public int compareTo(Path path) { int len1 = this.size(); int len2 = path.size(); int lin = Math.min(len1, len2); for (int k = 0; k < lin; k++) { Integer i1 = this.getNode(k); int i2 = path.getNode(k); if (!i1.equals(i2)) { return i1 - i2; } } return len1 - len2; }
没错,就是int类型的减法溢出。第一次作业我没有考虑到这一点,修复bug之后第二次作业重用第一次的代码竟然又忘记改这个地方,所以死两次。
这个故事告诉我们,千里之堤溃于蚁穴。
六、阐述对规格撰写和理解上的心得体会
说来惭愧,在这么多次训练过后,我所收获的有关规格的理解仅仅停留在写规格。当然,我理解为什么要产生规格,为什么要产生Java建模语言,为什么要进行基于规格的单元测试,这显然是这个单元给我的一大收获,我慢慢理解在大型工程开发中团队分工实现代码的过程。在企业研讨课上专家也向我们讲述了,架构设计不是一个过程,而是一个模块,不是设计完成后交付实现就完事了,而是作为一个模块随时调用随时调整的。
另一方面,这单元作业也让我真正实操了诸如继承、重写、接口实现和 JUnit