1. 程式人生 > >NOIP複賽複習(九)如何設計測試資料?

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個程式的選手對於演算法的理解可能是旗鼓相當的,但是程式的正確率絕對會有天壤之別。冰凍三尺,非一日之寒,勤加練習才是確保在比賽中取得好成績的根本。