一、JML语言理论基础
JML语言是一种形式化的,面向JAVA语言行为的规格语言,可以描述JAVA程序的数据,方法,类的规格,对其行为进行抽象与限制,体现了契约式编程的思想。主要包含了对于数据规格抽象和方法规格抽象以及综合的类规格抽象。在此我简单介绍一下JML语言的理论基础,总结JML语言中常用的表达。
1、方法规格:
通过JML语言提供的一些关键词,实现对方法规格的描述,保证方法执行结果没有二义性。其中比较重要的关键词有以下几种:
(1)requires:这个关键词后面跟随的是执行这个方法前所需要满足的前置条件,一般是对数据的一些要求,保证输入的数据在方法中都有合理的执行结果,不会意外报错。
(2)assignable:这个关键词后面跟随的是执行这个方法的过程中,发生改变的变量,即方法对数据的影响。
(3)ensures:这个关键词后面跟随的是对方法执行结果的描述,通过逻辑化的表示方式,对方法执行结果进行准确的描述,保证方法执行的正确性。
(4)normal_behavior:这个关键词代表后续一部分的JML描述是属于正常情况,不会抛出异常,可以正常得到结果。
(5)exceptional_behavior:这个关键词代表后续一部分的JML描述属于异常情况,会抛出不同类型的异常,具体抛出逻辑需要进行规定。
2、数据规格:
同样是利用JML语言提供的一些关键词,实现的是对程序中的数据进行约束,保证数据正常合理。主要的关键词有以下几种:
(1)invariant:这个关键词表示数据在可见状态下需要满足的特性。
(2)constraint:这个关键词表示数据在某些操作之前与之后数据需要满足的特性。
3、类规格:
类似于方法规格,JML语言可以使用类似的一些关键词对类的数据结构进行一个基本的约束,但是对于具体的实现方式并没有限制。主要的关键词有以下几种:
(1)requires:这个关键词减弱了对数据结构的要求,但是要求在构造这个类的时候,满足相应的条件。
(2)ensures:这个关键词可以对类中涉及到的数据结构需要满足的逻辑条件进行限制,确保数据结构具有某种特点。
4、JML逻辑描述语法:
为了保证类、数据、方法在实现上没有二义性,JML语言使用了一些列的逻辑描述语法,保证不同的人对同样的一个方法有相同的理解,保证程序运行的正确性。
(1)\nothing, \everything:一般用于描述方法的作用范围,\nothing表示对任何数据都没有影响,\everything则表示对所有的数据都会产生影响。
(2)\result:用于描述方法返回的结果。
(3)\forall, \exists:对比谓词逻辑中的存在量词以及全称量词,\forall代表着所有在一定范围内的数据都需要满足的条件,\exists代表着只要在一定范围内存在一个满足要求的数据就可以。
(4)\old:这个关键词代表方法执行前数据的情况,以此用于区别方法执行后数据的变化。
(5)\sum, \max, \min:这几个关键词可以根据单词的含义进行理解,分别表示在一定范围内的数据进行求和、求最大值、求最小值的结果。
二、JML工具链
JML语言可以为原本混乱的JAVA程序提供一个形式化的描述性语言,那么是否可以实现根据JML语言的描述实现对JAVA程序的验证,实现自动化的验证代码的可靠性呢?针对这个问题,JML工具链会成为我们的一大帮手,实现对JML语言的静态以及动态的验证。然而,由于目前JML工具链并不完善,实现起来并不是那么的顺手。
1、openJML:
OpenJML是可用于Java程序的程序验证工具,它可以检查使用JML(Java Modeling Language)语言进行注释的程序的正确性。它支持静态的检查,也支持运行时检查。
但是,OpenJML中依旧存在对一些JML语法支持不好的现象,在很多情况下对JML以及具体的代码实现有较高的要求,应用起来可以说是十分别扭。
2、JMLUnitNG:
JMLUnitNG是JMLUnit的进阶版本,全称为JMLUnit Next Generation。JMLUnitNG是用于带有JML注释的Java代码的自动化单元测试生成工具,包括使用Java 1.5+特性(例如泛型,枚举类型和增强的for循环)的代码。 像原始的JMLUnit一样,它使用JML断言作为测试。 它通过允许对要测试的类的每个方法参数轻松地自定义数据,以及使用Java反射自动生成非原始类型的测试数据,对原始JMLUnit进行了改进。
在具体的使用过程中,由于生成的测试样例只包含极端的数据,对于大量的非边缘数据没有进行有效的测试,对于本单元的作业测试效果一般。而且这一工具的更新时间是2014年,年代久远,部分功能上难以与现有的JAVA环境相匹配。
三、OpenJML与JMLUnitNG联合测试
顺应课程组的要求,我对自己写的Group类进行了自动化的测试。首先,使用OpenJML对JML代码进行静态的测试,之后对JML代码进行动态测试,使用的JML工具链是JMLUnitNG,接下来我会将我修改后的Person类,Group类的代码给出,避免后来者因为某些格式或者语法的原因,而导致工具链的无法使用。
Person类:
package test; import java.math.BigInteger; import java.util.ArrayList; public class Person { public int id; public String name; public BigInteger character; public int age; public ArrayList<Person> acquaintance; public ArrayList<Integer> value; public Person(int id, String name, BigInteger character, int age) { this.id = id; this.name = name; this.character = character; this.age = age; acquaintance = new ArrayList<Person>(); value = new ArrayList<Integer>(); } public int getId(){ return id; } public String getName(){ return name; } public BigInteger getCharacter(){ return character; } public int getAge(){ return age; } public boolean equals(Object obj){ if (obj != null && obj instanceof Person) { return (((Person) obj).getId() == id); } else { return false; } } public boolean isLinked(Person person){ if (person.getId() == id) { return true; } for (int i = 0; i < acquaintance.size(); i++) { if (acquaintance.get(i).getId() == person.getId() || person.getId() == id) { return true; } } return false; } public int queryValue(Person person){ for (int i = 0; i < acquaintance.size(); i++) { if (acquaintance.get(i).getId() == person.getId()) { return value.get(i); } } return 0; } public int getAcquaintanceSum(){ return acquaintance.size(); } public int compareTo(Person person){ return name.compareTo(person.getName()); } }
Group类:
package test; import java.math.BigInteger; import java.util.ArrayList; public class Group { public int id; public Person[] people; public int peopleSum; public int relationSum; public int valueSum; public BigInteger conflictSum; public int ageSum; public int ageSquareSum; public Group(int id) { this.id = id; people = new Person[1024]; peopleSum = 0; relationSum = 0; valueSum = 0; conflictSum = BigInteger.ZERO; ageSum = 0; ageSquareSum = 0; } //@ ensures \result == id; public /*@pure@*/ int getId(){ return id; } /*@ also @ public normal_behavior @ requires obj != null && obj instanceof Group; @ assignable \nothing; @ ensures \result == (((Group) obj).getId() == id); @ also @ public normal_behavior @ requires obj == null || !(obj instanceof Group); @ assignable \nothing; @ ensures \result == false; @*/ public /*@pure@*/ boolean equals(Object obj){ if (this == obj) { return true; } if (!(obj instanceof Group)) { return false; } Group group = (Group) obj; return getId() == group.getId(); } public void addPerson(Person newPerson){ people[peopleSum] = newPerson; peopleSum += 1; for (int i = 0; i < peopleSum; i++) { Person person = people[i]; if (newPerson.isLinked(person)) { if (newPerson.equals(person)) { relationSum += 1; } else { relationSum += 2; valueSum += newPerson.queryValue(person) + person.queryValue(newPerson); } } } conflictSum = conflictSum.xor(newPerson.getCharacter()); int age = newPerson.getAge(); ageSum += age; ageSquareSum += age * age; } //@ ensures \result == (\exists int i; 0 <= i && i < people.length; people[i].equals(person)); public /*@pure@*/ boolean hasPerson(Person person){ for(int i = 0; i < peopleSum; i++) { if(people[i].equals(person)) { return true; } } return false; } /*@ ensures \result == (\sum int i; 0 <= i && i < people.length; @ (\sum int j; 0 <= j && j < people.length && people[i].isLinked(people[j]); 1)); @*/ public /*@pure@*/ int getRelationSum(){ return relationSum; } /*@ ensures \result == (\sum int i; 0 <= i && i < people.length; @ (\sum int j; 0 <= j && j < people.length && @ people[i].isLinked(people[j]); people[i].queryValue(people[j]))); @*/ public /*@pure@*/ int getValueSum(){ return valueSum; } /*@ public normal_behavior @ requires people.length > 0; @ ensures (\exists BigInteger[] temp; @ temp.length == people.length && temp[0] == people[0].getCharacter(); @ (\forall int i; 1 <= i && i < temp.length; @ temp[i] == temp[i-1].xor(people[i].getCharacter())) && @ \result == temp[temp.length - 1]); @ also @ public normal_behavior @ requires people.length == 0; @ ensures \result == BigInteger.ZERO; @*/ public /*@pure@*/ BigInteger getConflictSum(){ return conflictSum; } /*@ ensures \result == (people.length == 0? 0 : @ ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length)); @*/ public /*@pure@*/ int getAgeMean(){ int n = peopleSum; if (n == 0) { return 0; } else { return ageSum / n; } } }
同时在这里我们也给出我们的目录结构,避免因为目录结构又产生一些不必要的麻烦。
E:. │ jmlunitng.jar │ openjml.jar │ temp.txt │ ├─Solvers │ cvc4-1.6.exe │ libz3.dll │ libz3.lib │ libz3java.dll │ libz3java.lib │ Microsoft.Z3.dll │ Microsoft.Z3.xml │ msvcp100.dll │ msvcr100.dll │ vcomp100.dll │ z3-4.3.2.exe │ z3-4.7.1.exe │ └─test Group.java Person.java
在当前文件目录下,我们利用cmd运行一下几条指令:
java -jar jmlunitng.jar test/Group.java
javac -cp jmlunitng.jar test/*.java
java -jar openjml.jar -rac test/Group.java test/Person.java
java -cp jmlunitng.jar test.Group_JML_Test
如果运行成功的话可以得到以下结果(其中的警告就忽略吧,主要是没有错误就行了):
test\Group.java:86: 警告: A non-pure method is being called where it is not permitted: test.Person.isLinked(test.Person) @ (\sum int j; 0 <= j && j < people.length && people[i].isLinked(people[j]); 1)); ^ test\Group.java:94: 警告: A non-pure method is being called where it is not permitted: test.Person.isLinked(test.Person) @ people[i].isLinked(people[j]); people[i].queryValue(people[j]))); ^ test\Group.java:94: 警告: A non-pure method is being called where it is not permitted: test.Person.queryValue(test.Person) @ people[i].isLinked(people[j]); people[i].queryValue(people[j]))); ^ test\Group.java:103: 警告: A non-pure method is being called where it is not permitted: test.Person.getCharacter() @ temp.length == people.length && temp[0] == people[0].getCharacter(); ^ test\Group.java:105: 警告: A non-pure method is being called where it is not permitted: test.Person.getCharacter() @ temp[i] == temp[i-1].xor(people[i].getCharacter())) && ^ test\Group.java:117: 警告: A non-pure method is being called where it is not permitted: test.Person.getAge() @ ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length)); ^ test\Group.java:86: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression @ (\sum int j; 0 <= j && j < people.length && people[i].isLinked(people[j]); 1)); ^ test\Group.java:93: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression @ (\sum int j; 0 <= j && j < people.length && ^ test\Group.java:102: 注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression @ ensures (\exists BigInteger[] temp; ^ 6 个警告
运行JMLUnitNG后得到结果:
[TestNG] Running: Command line suite Passed: racEnabled() Passed: constructor Group(-2147483648) Passed: constructor Group(0) Passed: constructor Group(2147483647) Failed: <<test.Group@1761e840>>.addPerson(null) Failed: <<test.Group@6c629d6e>>.addPerson(null) Failed: <<test.Group@3f102e87>>.addPerson(null) Passed: <<test.Group@27abe2cd>>.equals(null) Passed: <<test.Group@5f5a92bb>>.equals(null) Passed: <<test.Group@6fdb1f78>>.equals(null) Passed: <<test.Group@1517365b>>.equals(java.lang.Object@4fccd51b) Passed: <<test.Group@44e81672>>.equals(java.lang.Object@60215eee) Passed: <<test.Group@4ca8195f>>.equals(java.lang.Object@65e579dc) Runtime exception while evaluating postconditions - postconditions are undefined in JML java.lang.NullPointerException at test.Group.getAgeMean(Group.java:117) at test.Group_JML_Test.test_getAgeMean__0(Group_JML_Test.java:246) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:80) at org.testng.internal.Invoker.invokeMethod(Invoker.java:694) at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:886) at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1248) at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127) at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111) at org.testng.TestRunner.privateRun(TestRunner.java:758) at org.testng.TestRunner.run(TestRunner.java:613) at org.testng.SuiteRunner.runTest(SuiteRunner.java:369) at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:363) at org.testng.SuiteRunner.privateRun(SuiteRunner.java:318) at org.testng.SuiteRunner.run(SuiteRunner.java:242) at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:53) at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:90) at org.testng.TestNG.runSuitesSequentially(TestNG.java:1144) at org.testng.TestNG.runSuitesLocally(TestNG.java:1069) at org.testng.TestNG.run(TestNG.java:981) at test.Group_JML_Test.main(Group_JML_Test.java:56) Passed: <<test.Group@b065c63>>.getAgeMean() Runtime exception while evaluating postconditions - postconditions are undefined in JML java.lang.NullPointerException at test.Group.getAgeMean(Group.java:117) at test.Group_JML_Test.test_getAgeMean__0(Group_JML_Test.java:246) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:80) at org.testng.internal.Invoker.invokeMethod(Invoker.java:694) at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:886) at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1248) at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127) at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111) at org.testng.TestRunner.privateRun(TestRunner.java:758) at org.testng.TestRunner.run(TestRunner.java:613) at org.testng.SuiteRunner.runTest(SuiteRunner.java:369) at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:363) at org.testng.SuiteRunner.privateRun(SuiteRunner.java:318) at org.testng.SuiteRunner.run(SuiteRunner.java:242) at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:53) at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:90) at org.testng.TestNG.runSuitesSequentially(TestNG.java:1144) at org.testng.TestNG.runSuitesLocally(TestNG.java:1069) at org.testng.TestNG.run(TestNG.java:981) at test.Group_JML_Test.main(Group_JML_Test.java:56) Passed: <<test.Group@490d6c15>>.getAgeMean() Runtime exception while evaluating postconditions - postconditions are undefined in JML java.lang.NullPointerException at test.Group.getAgeMean(Group.java:117) at test.Group_JML_Test.test_getAgeMean__0(Group_JML_Test.java:246) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:80) at org.testng.internal.Invoker.invokeMethod(Invoker.java:694) at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:886) at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1248) at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127) at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111) at org.testng.TestRunner.privateRun(TestRunner.java:758) at org.testng.TestRunner.run(TestRunner.java:613) at org.testng.SuiteRunner.runTest(SuiteRunner.java:369) at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:363) at org.testng.SuiteRunner.privateRun(SuiteRunner.java:318) at org.testng.SuiteRunner.run(SuiteRunner.java:242) at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:53) at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:90) at org.testng.TestNG.runSuitesSequentially(TestNG.java:1144) at org.testng.TestNG.runSuitesLocally(TestNG.java:1069) at org.testng.TestNG.run(TestNG.java:981) at test.Group_JML_Test.main(Group_JML_Test.java:56) Passed: <<test.Group@449b2d27>>.getAgeMean() Failed: <<test.Group@5479e3f>>.getConflictSum() Failed: <<test.Group@27082746>>.getConflictSum() Failed: <<test.Group@7bfcd12c>>.getConflictSum() Passed: <<test.Group@42f30e0a>>.getId() Passed: <<test.Group@24273305>>.getId() Passed: <<test.Group@5b1d2887>>.getId() Failed: <<test.Group@46f5f779>>.getRelationSum() Failed: <<test.Group@1c2c22f3>>.getRelationSum() Failed: <<test.Group@18e8568>>.getRelationSum() Passed: <<test.Group@33e5ccce>>.getValueSum() Passed: <<test.Group@5a42bbf4>>.getValueSum() Passed: <<test.Group@270421f5>>.getValueSum() Passed: <<test.Group@4f4a7090>>.hasPerson(null) Passed: <<test.Group@18ef96>>.hasPerson(null) Passed: <<test.Group@6956de9>>.hasPerson(null) =============================================== Command line suite Total tests run: 31, Failures: 9, Skips: 0 ===============================================
其中,部分测试点测试的是一些极端的数据情况,例如空指针,这种测试点在我们的强测中是不会出现的,可以忽略。
总体来说,使用JML工具链的感受不太好,由于界面比较不友善,而且对代码风格有较高而又不明确的要求,导致使用工程中充满艰辛险阻。而且通过JML工具链的动态检查,检查的内容大多是和极端数据相关的情况,对于我们作业中要求的效率,正确情等指标并不能起到一个很好的作用,因此这个结果只能作为一个参考,千万不要把所有的测试时间都投入到这上面,多使用JUnit以及自动生成数据与其他人的程序进行对拍可能是一个更好的选择(我的选择)。
四、代码架构
在这单元的作业中,我写的程序代码结构比较简单,没有自己新建其他的类,只实现了课程组要求的三个类,MyPerson,MyNetWork,MyGroup,这三个类之间的关系如下图所示。
五、代码实现方式
1、第一次作业:
在第一次作业中,课程组给出的JML规格相对简单,对程序效率的要求上也并不高,因此在实现方式上没有过多可以介绍的内容。这次任务中,我认为课程组将重点放在了对JML的初步认识以及准确理解上,考察的是同学们对JML规格的准确理解,其中的难点在于JML规定的前置条件,避免因为对前置条件的疏忽而导致重大的失误。
2、第二次作业:
在第二次作业中,课程组加大了JML规格的难度,加入了诸如求取平均值,方差以及关系数量统计等复杂操作。我认为,在这次任务中课程组将重点放在了强调JML的规格作用,但只是一个规格而已,对具体的实现方式并不做具体要求,需要编写代码的人员通过合理的数据结果,完成JML规格中的要求,绝不能完全照搬JML规格中的实现方法。
针对课程组的这一意图,在第二次任务中我提高了对算法层面的重视,在实现规定的输入输出要求的同时,也要考虑对数据结构的选择,考虑对实现方法的选择,提高程序的运行效率。具体来说,这次任务中主要的难点或者说是消耗时间的代码集中在Group类中,由于涉及到大量的循环遍历操作,如果不特殊处理,简单的在接受到查询请求后遍历数组实现统计,这会导致程序的效率大大降低,严重的可能导致测试tle。为了解决由于查询时遍历带来的时间成本,我选择了缓存的方法。通过在Group类中维护多个相关的临时变量,保存当前状态下可能需要查询的数值的结果,从而实现复杂度为O(1)的查询过程。但是为了实现这一设计,我们需要在addPersonToGroup方法中对这些变量进行维护,保证这些变量在Group类发生改变的时候可以及时更新。
在实现缓存的过程中,也存在着一点点的小技巧。对于查询Group类中成员年龄方差的请求,如果维护的临时变量是当前的方差结果,当我们再次向组中加入新的成员时,我们难以根据原来的方差计算新的方差。为了解决这一问题,我们可以选择维护两个中间变量,一个是组内成员的年龄和,另一个是组内成员的年龄平方和。利用这两个中间变量,我们可以根据方差的计算公式得到方差查询结果。(不过这里一定要处理好精度问题,可能会由于笨方法精度较高而出现以外的问题)。
3、第三次作业:
在第三次作业中,实现方法的选择就显得尤为重要,如果选择了错误的实现方法,可能就会直接暴毙。相比于第二次作业中的要求,第三单元的实现难度明显有了提升(这对这个问题我一直觉得可能课程组在这次作业中对算法的要求有些高),如果没有很好的算法基础或者说是对JAVA语言的深入理解,很有可能会自己都不知道自己是怎么死的。
在这次任务中,实现最为困难的有三个方法,BlockSum,MinPath,StrongLink。针对这三个方法,我将分别进行简单的实现方法分析。
(1)BlockSum
这个方法的作用是查询NetWork中以Person为节点形成的关系图中连通块的个数。为了解决这个问题我们有两条路可以走。
第一条路是一条十分朴素的路,即请求时计算。通过多次使用dfs对图进行遍历,每次遍历可以得到一个联通子图,将得到的联通子图中的节点从图中移出,重复dfs遍历的过程,直到所有节点全部从图中移出。在这个过程中,调用dfs的次数就是连通块的个数。下方给出了我写的样例代码。
这种方法思路简单,容易理解,实现方便,但是在时间效率上并不理想。通过设计3000条数量级的测试请求数据,我发现这种方法的运行时间达到了4.73秒,是无法接受的。
第二种实现方案应该是大多数人使用的方法,即通过缓存的技术实现查询时复杂度O(1)的方法。由于BlockSum的值仅会因为addPerson以及addRelation两个方法调用的影响,即调用这两个函数的时候可能会对BlockSum的值进行改变,因此我们可以在调用这两个方法的时候更新BlockSum的值,当调用BlockSum方法时直接返回缓存的数据。
addPerson方法没有发生异常的情况下,新加入的人与其他人没有任何联系,因此BlockSum++;addRelation方法没有发生异常的情况下,需要判断建立关系的两个人是否属于同一组,如果不是同一组,需要将两组进行合并,并且BlockSum--。下面是我给出的实现代码。
这种方法的实现难度不大,而且时间效率较为理想,经过同样的测试样例测试发现,这种方法的运行时间为1.14秒。
最后,我还想稍微提一下并查集的实现方法,这也是一些高端玩家使用的技术。为了实现高效的分组查找功能,将NetWork中的元素存储在并查集中,利用并查集的相关技术实现对分组数量的快速统计。简单来讲,并查集的原理就是对于属于同一连通块的元素,我们提取其中一个元素作为代表元素,如果说涉及到集合的合并,只需要将代表元素加入到新的连通块,实现快速的合并,同时查询连通块的个数也可以通过查找代表元素个数来实现。
(2)MinPath
这个方法的作用是查询关系图中的两个Person节点的最短路径。针对这个方法,我们可以选择的实现方法只有一个,那就是堆优化的迪杰特斯拉算法。
所谓堆优化的迪杰特斯拉算法就是利用小顶堆排序的特性,每次弹出对顶最小的元素,从而减少循环查找最小元素是所耗费的时间。在JAVA中我们可以使用PriorityQueue这一数据结构进行实现。这一数据结构可以实现小顶堆的功能,保证每次弹出的节点是目前所能取到的最短路径,提高算法效率。其余部分和迪杰特斯拉算法没有区别,数据结构正常学习过的同学一般都可以完成。
(3)StrongLink
这个方法的左右是查询关系图中的两个Person节点是否为双联通。针对这个方法,实现的方法较多,效率也不尽相同。
首先来说一下最朴素的实现方案,利用两次dfs实现对两条连通路径的查找。一开始有人提出,可以先进行一次dfs,找到一条路径,之后删除这条路径上的节点,进行第二次dfs。如果两次dfs都成功发现连通路径,那这个双联通就稳了。但是这个方法并不可靠,存在严重问题。对于下图所示的情况,查询1-4节点是否双联通,如果第一次dfs找到的路径为1-2-3-4,第二次dfs将无法找到正确的连通路径,但是1-4节点却是一个双联通的关系,进而产生错误。
为了解决这个问题,我们可以使用循环两次dfs的方法。每当dfs找到一条连通路径后,删除相关节点后尝试进行第二次dfs,如果跑通直接给出成功的返回值,否则继续尝试查找另一条连通路径。在完成对所有可能连通路径的便利后,如果依旧没有找到,则返回失败。然而这个算法虽然能保证正确性,但是时间复杂度极高,对于咱们的数据要求,这个算法会直接爆炸,以至于在我本地无法正常执行完成这个程序。
在尝试了前两个算法之后,我们发现,直接通过dfs寻找那两条连通的路径貌似是不太可行的,我们需要尝试用更加巧妙的方法。在助教的启发下,我们注意到,如果两个节点满足双联通关系,当且仅当在去除图中除起点与终点以外的任意一个其他节点后,起点与终点的连通性不变,保持连通。同时,为了进一步提高这一算法的效率,我们可以先进行一次dfs,得到一条连通路径。在删除节点的过程中,我们只需要考虑删除这条路径上的节点,而不需要对所有节点都进行删除尝试,毕竟删除了不再这一路径上的节点,依旧会存在这条路径作为连通的路径,不会影响连通性。基于这一定理,我修改了实现方法,完成了下图所示的代码,得到了理想的运行效率。
六、Bug修复
1、第一次作业:
在这一次作业中,喜提oo作业第一次D屋体验,心态发生了一点点微妙的变化,现在想起来,出现的错误真的是莫名其妙,毫无技术含量,修改过程不超过5行代码。
在这一次作业中,因为初次接触JML这种规格描述语言,可能是存在部分内容没有很好的理解(更有可能是眼睛不太好使),导致其中addRelation方法中的限制条件没有看清,进而出现了让整个程序爆炸的BUG。由于addRelation这个方法在多个测试点中都有大量的调用,因此这个方法一旦写错,直接全盘崩溃。
除了上面所说的情况以外,isCircle方法的实现也并不是十分到位。由于数据结构知识年久失修,dfs理解没有十分到位,导致判断连通路径的过程中,我写成了需要遍历查询所有连通路径的时间复杂度超高的dfs,进而引发了一些测试点出现了tle的问题。现在看来,这次的D屋实在是不值得,有点太不小心了。现在就是后悔,非常后悔。
2、第二次作业:
这次作业中整体上没有什么问题,只是因为一些细枝末节的问题,导致在强测过程中出现了一个测试点的错误,再次不做过多的分析。
3、第三次作业:
由于第三次作业难度提升较多,虽然没有沦落到D屋,但是也是被打入了C屋。由于完成代码后编写的测试程序出了一些问题,没能正确的对我的程序进行测试,因此导致我误以为我的程序十分完美,进而没有进一步的思考。具体来讲,我在MinPath以及StrongLIink两个方法的实现上效率不高,导致很多测试点出现了tle的情况。而相关的实现方法已在前一部分中进行了提及。
不过这次互测的过程中还是有一个小插曲的,由于一些原因,互测数据点的合法性判断出了一些问题,导致我自以为已经成功hack了52/54的超高战绩。为了避免无意义的纷争,我打算就此收手,立地成佛。然而,在互测结束的那天晚上,我本来是想最后再确认一下别人的情况(之前别人一直是0/0),突然发现自己提交的测试点全部被清楚了,回到了0/0。事后了解到这是由于助教那边事务繁忙,没有及时更些测试点合法性判断的程序,也没有及时删除不合法测试点,导致信息反馈的时间有些过长。因此,最后紧赶慢赶,一方面hack比例下降了很多个,另一方面,最后只成功hack了11个点,有点亏。
七、心得体会
最后,谈一下这一单元作业的心得体会。最大的感受当然是对JML这种形式化描述语言的好奇,了解了一种可以规范代码实现方式的语言,对以后在参与团队设计的过程中,肯定能为我提供不小的帮助,保证多人设计的代码效果的一致性,不会出现理解上的二义性。其次,让我感触最深的就是JML的工具链,这是真的“好用”。由于各种原因,与JML相关的自动化验证工具,想要正常运行需要进行大量的修改,甚至是对方法或者类的简化,最终的结果基本上就是聊胜于无,甚至是有些鸡肋。如果说JML这种形式化的规格描述语言可以流通的更加广泛的话,相信相关的自动化验证工具也会逐渐完善,希望以后能有一个更好的JML自动验证体验。