本文作者是全国信息学竞赛金牌教练曹文老师,本文发表在《NOI专刊》总第13期。

NOIP复赛复习(九)如何设计测试数据?-少儿编程教育网

有些同学参加一次信息学比赛之后,自我感觉非常不错,但是测评结果成绩却并不理想。造成这种情况的原因有多方面,但是我认为其中不可忽视的一大原因就是在写完程序之后,他们并不知道如何保证程序的正确性。在这里,我就这个问题提出一点自己的看法。

先从考试的时间分配问题讲起。很多同学觉得考试时间很充分,NOIP有三个小时,写完4个程序绰绰有余(编者注:目前普及组复赛为一场3.5小时4题,提高组复赛为两场,各3.5小时3题)。显然这个想法是错误的。考试有三个小时,可这三个小时不是全都用来写程序的。一个合理的规划方案应该是:

1.半个小时读题,想算法;
2.一个半小时写程序;
3.一个小时制作测试数据测试程序。

当然,参加不同的比赛,题目质量不同,身体状态不同,心理压力也不同,这个规划方案不可能按部就班。但我认为它是有一定的指导意义的。下面让我们一起来仔细地分析一下整个方案。

读题和想算法,是比赛的重中之重,想必所有的同学都已经铭记于心,不必再多说了。而写程序的话,也许一个半小时仓促了一点,但是这其实只是一个编程熟练程度的问题。如果我们勤加练习,对算法和各种常用过程(快速排序、堆排序等)了如指掌,这个目标并不难达到。事实上,我校一位在NOIP2007中取得满分的同学读题加写程序一共只用了1小时15分钟。

最后一步,也是本文的中心,就是制作测试数据了。整个规划方案中安排了一个小时用来测试题目,也许有些同学会觉得没有必要。但是我认为,每一道题都应该设计测试数据,有些把握不大的题应该设计多种测试数据以保证测试的全面。因此,在这一个小时里,为4个程序设计数据,平均15分钟就要设计一个,这还不包括如果你发现了程序中的问题,再修改程序用的时间。这么一算,一个小时就有点吃紧了,似乎还不怎么够,不过其实还是够的,因为做数据比做题要简单得多。

测试数据分很多种。一般而言可以把它们分成小数据、大数据和极限数据三种。

一、小数据

样例无疑是所有OIer的最爱。大家学编程的时候就知道写完程序第一件事就是过样例,样例就是一个典型的小数据。小数据有三大优点:

1.易于调试

很多同学对pascal的调试模式有很大的依赖性,静态查错的能力很弱。严格地说这不能算是一个坏习惯,因为调试模式确实给我们带来了高效率,帮助我们在极短的时间找到程序的问题所在。但是调试的弊端就是不能处理大数据量的数据。因此,对于静态查错能力弱而调试能力相对较强的同学而言,小数据很重要,绝大部分的错误都是靠小数据的跟踪调试找到的。

2.易于设计

这一点不用多说。由于数据量小,我们往往可以手工设计质量更高的数据,同时对于数据本身也有直观的了解。与此同时,很多的题都会有所谓的“变态数据”,这和极限数据有着一些不同,它虽然数据量不大,但是剑走偏锋,比如某矩阵题给你一个全都是1的矩阵之类的。这种狡猾的数据在评测的时候往往并不罕见,由于这样那样的原因,我们就栽了跟头。为了使得自己的程序更加强壮,我们需要预先测试自己的程序是否能够通过这样的数据。这种变态数据只能够由我们手工设计,因此一般都是小数据。

3.覆盖面广

对于很多题目而言,测试数据理论上存在无穷多组;但是如果有n<5并且所有数都小于10的限制,那么数据的个数就变得有限了,不妨设是1000组。我们可以通过写一个程序,直接把这1000组小数据全部都制作出来,然后逐个儿测试。虽然这些数据的数据量小,但是由于它们把小数据的所有可能的情况都包括在其中了,因此你的程序的大部分问题都能够在这1000组数据中有所体现。同时,因为是小数据,程序可以在很短的时间内运行出解,例如是0.05秒,这样,1000组数据,也不过只要50秒,完全可以接受。但是要注意,生成所有数据的同时,我们还要写一个效率差,确保正确的程序来验证结果的正确性。因此这种想法至少需要2个程序。(具体操作流程参考“大数据”部分)。

二、大数据

大数据是属于那种数据量比小数据大,同时可以使用较弱的替代算法得到结果的数据。一般的操作流程是这样的:先写一个随机化的制作大数据的程序;然后写一个针对题目的效率较差但是正确性能够保证的使用替代算法的程序;最后使用一个批处理文件,进行多次对比测试,即生成一个数据,然后再比较两个程序的结果。一定要注意这三个程序的文件输入输出和批处理的实现,这些地方很容易出错。

大数据的使用方法和前面讲过的小数据的穷举方法差不多,但是相比之下有些许不同:

1.数据量不同

数据量变大之后,对程序是一个新的挑战,一些更加难以发现的问题可能会显露出来。

2.可以随机化

与小数据不同,由于大数据的数据个数过多,不能够穷举完成,因此推荐使用随机化。而随机化显然比穷举要容易编写得多,因此大数据的实现更加方便。而随机化的缺点是,变态数据未必能够随机到。

而与极限数据相比,大数据的优点是可以使用替代算法。极限数据往往不能使用替代算法,因为替代算法往往不能在几秒钟,几分钟甚至几个小时内得出解。

因此大数据是最值得提倡的,我认为如果条件允许,每一题都应该用大数据来测试一下,确保正确性。

三、极限数据

在很多人的观念里,极限数据是非常重要的一种数据。我发现很多同学在通过样例之后第一个测试的数据就是极限数据。我认为这是一种非常不好的习惯,因为极限数据并没有我们想象的那么重要。我们能够通过极限数据了解到什么呢?无非是我们的程序是否会超时,外加我们的程序是否会越界。事实上一些简单得随机化得到的极限数据或者手工出的带规律的极限数据甚至未必真的能够给我们翔实的信息,因为就算对于这些数据我们的程序能够快速得到结果,我们仍然不知道我们的程序是不是真的不会在处理别的数据时超时或者越界。所谓的极限数据,其实不过是用来测试你的数组有没有越界而已,这才是它的最大的用处。

此外,极限数据容易给你一些虚假的信息。例如你测试一个1000*1000的全是1的矩阵,结果程序在0.01秒内出解了,结果无疑是正确的。于是你得出你的程序不越界、不超时、不错误的结论。一些没有经验的同学甚至可能就不再检查这个程序而努力去完成下一道题。然而这么一个特殊的数据,其实什么问题都说明不了。

因此你又不得不使用特殊数据,因为如果你使用了一个随机的数据,你又不能够确定自己的程序结果是不是正确。

当然,我并没有宣扬以后大家不要再设计极限数据。极限数据仍然有它的重要性。但是它的设计应该是最后一步,在至少设计并测试大数据,确保了程序的正确性之后,再检查程序的极限情况下是不是会超时或越界。一般的测评数据中,极限数据的个数并不会很多,我们不应该因小失大。

虽然对测试数据的种类有了一定的了解,但是很多同学也许对如何测试仍有疑惑。测试程序的原始方法是全部手工的,大致可以概括为:手工运行测试数据生成程序、手工运行源程序、手工运行替代算法的程序,最后目测两个程序的结果是否一样。这个方法虽然直观,但是有很多不可忽视的缺点。比如效率太低,操作时容易有较大误差等。我在这里详细地介绍一下使用批处理的方法。假设我们需要测试一个程序sample.exe,它的输入输出文件分别是sample.in、sample.out,那么首先我们制作一个数据生成程序samplemaker.exe,然后写一个用替代算法的程序sample2.exe,注意这个程序的输入文件是sample.in,但是输出文件是sample2.out。最后写一个txt文件:

Samplemaker.exe
Sample.exe
Sample2.exe
Fc sample.out sample2.out
Pause

然后把这个txt文件的后缀改成bat,就变成了一个批处理文件了。双击运行这个批处理文件,就可以自动测试,得出两个程序的结果是否相同了。Fc命令是用来比较两个文件是否相同,pause表示暂停,以便让我们能够看清楚结果。

但是这个批处理只能够测试一次,我们需要运行它n次来进行n次的测试。这个效率仍然偏低。提高效率的方法是,在批处理文件中加入循环语句,如下:

:loop
Samplemaker.exe
Sample.exe
Sample2.exe
Fc sample.out sample2.out
Pause
Goto loop

这样的话,这个批处理就能够运行无数次,每次你只需要敲一下键盘就可以了。这样的效率已经很高了,你需要做的不过是盯着fc的结果,找到一个不一样的就关掉批处理,找到sample.in,然后调试。

不过,对于有些题目来说,这样的效率仍然不够,1秒可能只能测试几个数据。更好的方法应该是,把pause删除,然后把fc的结果输入进文件,然后写一个程序判断fc的结果,如果发现结果不一样,就把sample.in记录下来。如下:

:loop
Samplemaker.exe
Sample.exe
Sample2.exe
Fc sample.out sample2.out >result.txt
Judge.exe
Goto loop

其中,judge.exe需要我们编写。它应该从result.txt中读入数据,判断结果,如果不一样,就进入死循环(例如使用while true do;语句)。这样你就能够一眼看出有问题出现了,关掉批处理,然后检查sample.in。

下面我就NOIP2007的4道题,给大家讲一讲我校部分学生设计测试数据的体会。

一、count统计数字

输入一个数n(n≤200000)和n个自然数(每个数都不超过1.5*109),请统计出这些自然数各自出现的次数,按顺序从小到大输出。输入数据保证不相同的数不超过10000个。

本题的算法是很简单的,排序+统计。这里推荐使用快速排序,因为快速排序是众多排序中运行效率最高的一个。对于本题,可以设计三种数据:

1.小数据:小数据应该设计几个变态数据。例如10个0,或者9,8,7,6,5,4,3,2,1之类。主要是测试一些不容易随机得到的情况。

2.大数据:本题的代替算法有多个。比较简单的算法应该是冒泡排序或者使用数组下标排序之类。这里推荐使用数组下标排序,因为对于本题的要求(统计次数),这个排序有巨大的优势(编程复杂度很低)。

3.极限数据:由于本题的数据量很大,有必要测试一下程序是否会越界。

二、expand字符串的展开

我们可以用减号对连续字母或数字进行缩写,于是字符串a-dha3-68就可以展开为abcdha34568。

输入三个参数p1,p2,p3,再输入一个仅由数字、小写字母和减号组成的字符串(长度不超过100),请按参数展开此字符串。

各个参数的意义如下:

参数p1=1→所有填充的字母都写成小写;

参数p1=2→所有填充的字母都写成大写;

参数p1=3→所有填充的字母和数字都用星号代替;

参数p2=k→同一个填充字符连续写k遍;

参数p3=1→顺序填充;

参数p3=2→逆序填充。

另外,如果减号两边的字符一个是数字一个是字母,或者减号右边的ASCII码没左边的大,则该处不变。

本题是比较简单的模拟,关键是要看清楚题意。本题不适合出大数据或者极限数据,推荐使用静态查错的方法。当然,也可以设计一些有针对性的小数据。

三、game矩阵取数游戏

一个n行m列的矩阵,每次你需要按要求取出n个数,m次正好将所有数取完。每取出一个数你都会有一个得分,请求出最终的得分最大是多少。

每一次取数的要求:每一行中恰好取一个数,且只能取剩下的数中最左边或最右边位置上的数。

每取一个数的得分:所取数的数值乘以2i,i表示这是第i轮取数。

矩阵中的数为不超过100的自然数,1≤n,m≤80。

本题是比较简单的动态规划。因为是动态规划,一般很少有较好的替代算法。由于行和列之间没有必然的联系,因此我们完全可以考虑n=1的情况。这样的话,可能使用2m的穷举算法,这样m可以到20左右。因此对于本题,可以设计:

1.小数据:针对一些特殊情况,例如所有的数据都是0,或者所有的数都是1的情况;
2.大数据:使用穷举算法加随机化生成数据,可以测试程序的正确性;
3.极限数据:测试程序是否会超时。

四、core数网的核

树上的任两点间都有唯一路径。定义某一点到树上某一路径的距离为该点到路径上所有点的路径长度中的最小值。定义树中某条路径的“偏心距”为所有其他点到此路径的距离的最大值。定义树的直径为树的最大路径(可能不唯一)。给出一个有n个节点的无根树,请找出某个直径上的一段长度不超过s的路径(可能退化为一个点),使它的偏心距最小。请输出这个最小偏心距的值。

题目已经告诉你如下定理:树的所有直径的中点必然重合(这个中点可能在某条边上)。其实这个结论很明显嘛,因为如果中点不重合的话必然可以找到一条更长的路。

5≤n≤300,0≤s≤1000,边权是不超过1000的正整数。

相对于前面的三题而言,本题有一定难度。本题告诉了我们很多条件,因此正确的算法有很多,例如dfs或者bfs加穷举等,这里不作讨论。由于本题与树有关,因此并不容易手工出数据。对于本题,可以设计:

1.大数据:随机化可以出一些大数据,但是一个好的替代算法不容易想到。我觉得穷举还算不错的想法。可以使用n2的时间穷举长度不超过s的路径,然后再使用n的时间穷举别的点,再用n的时间算出距离。这样n可以大概到100左右,算是不错的算法了。

2.极限数据:出不出都可以,因为只要算法恰当,本题应该不会超时或者越界。

最后笔者还要强调一些问题:

1.不能够做错测试数据。测试数据生成程序时非常容易写的,手工出测试数据也很轻松,但是不能够轻视出数据这一个过程。数据的格式、范围以及是否合法都非常重要。如果你不慎出错了数据,你很可能认为不是数据的问题,而是程序的问题,然后花费大量的时间检查自己的程序。这无疑是非常不值得的。

2.测试数据虽然是非常优秀的检查程序的方法,但是不能够过于依赖,静态查错仍然非常重要。对于小数据,也许还可以轻松跟踪调试,但是大数据或者极限数据就需要花费大量时间了。因此,笔者着重推荐静态查错的方法。

3.认真看完本文就会发现,对于某一个程序,写三四个小程序来测试它其实一点都不过分或者夸张。因此,打字速度和编程熟练程度就变得非常重要。一个一小时能够写10个程序的选手和一个一小时只能写3个程序的选手对于算法的理解可能是旗鼓相当的,但是程序的正确率绝对会有天壤之别。冰冻三尺,非一日之寒,勤加练习才是确保在比赛中取得好成绩的根本。


作者简介:1989年复旦大学数学系毕业后,来到省常中。从BASIC语言走过了PASCAL语言,走过了C语言,他从一个普通计算机老师走到高级程序员,再走到今天的金牌教练。