程序員編程藝術第三十八章:Hero在線編程判題、出題系統的演進與優化

  第三十八章:Hero在線編程判題、出題系統的演進與優化

編程藝術githubhttps://github.com/julycoding/The-Art-Of-Programming-By-July,邀你共同創作!


前言

    以前出門在外玩的時候,經常跑去網吧,去網吧也不幹啥事,看看博客,改改博客,但若想修改博客上的一段代碼,卻發覺網吧沒有裝編譯器這個東西,可一想到安裝它需要不少時間,所以每次想在網吧寫代碼都作罷。

    當時,便想,如果某一天打開瀏覽器,便能在網頁上直接敲代碼,那該有多好,隨時隨地,不受編譯器限制。好事多磨,今年3月終於來CSDN來做這樣一個在線編程網站Hero了:http://hero.csdn.net/,以項目負責人的身份總體負責它的產品和運營、包括出題。

    爲何要寫此文?本文不談Hero如何實現,也不談今年3月至今,它的PV漲了多少倍,不談每一道題的具體解法、思路、代碼是怎樣的(日後可能會寫),更不談它的界面是如何一步步優化的,只談談它的判題系統、出題系統是如何一步步演進和優化的,即它背後是怎樣的一種判題機制(用來判斷每天幾千個用戶提交的程序正確與否),以及如何做到讓每一個用戶都可以來Hero上出題的。

    順便對很多朋友詢問“Hero後臺到底是怎樣判題的,爲何我的程序提交出錯?”的一個集中回答,把判題機制開放出來,對每一個Hero的用戶做到公平公正。最後年終將至,也算是對自己近一年工作的部分回顧與總結。

    OK,本文有何問題,歡迎隨時指正,對Hero有任何改進或建議,歡迎隨時向我反饋,thanks。


第三十八章、Hero在線編程判題、出題系統的演進與優化

一、最初的人工肉眼判題

    Hero從頭至尾的實現沒有借用過任何開源工具,所以它的每一步探索都顯得進展緩慢、推動艱難。在今年3月份之前,在Hero上玩的人不多,所以我剛來公司時,是完全人工肉眼去看每一個用戶的程序思路是否正確,不確定的便得自己複製用戶的代碼粘貼到編譯器裏進行編譯,看結果是否正常。也就是說如果我出一道題:求N個字符的全排列。系統後臺只做一件事情,就是把用戶的代碼簡單保存起來。

    但打開許多用戶的答案後,才發覺他並沒有實現全排列,他只是寫了一個“hello world”:

public class HelloWorld 
{ 
	public static void main(String args[]) 
	{
		System.out.println("Hello World");
	} 
} 

    即用戶的程序是否正確,我得人工判斷。這樣的人工判題持續了整整一個月,後來發覺來Hero上玩的人越來越多,每天從之前只看幾份代碼到需要看幾百份代碼,我便立馬覺得不對勁了。

   是的,必須得讓機器實現自動判題。


二、寫測試代碼讓機器自動判題

2.1、簡單粗暴的一系列if else判斷

    怎麼讓機器實現自動判題呢?其實原理也挺簡單,可以在出題時寫一段測試代碼,然後用這段包含了很多組測試數據的測試代碼去驗證用戶的程序是否正確。

    比如現在有一道題是這樣子的:

  最長有效括號的長度:給定只包含括號字符'('和 ')''的字符串,請找出最長的有效括號內子括號的長度。

舉幾個例子如下:

  1. 例如對於"( ()",最長的有效的括號中的子字符串是"()" ,有效雙括號數1個,故它的長度爲 2。 
  2. 再比如對於字符串") () () )",其中最長的有效的括號中的子字符串是"() ()",有效雙括號數2個,故它的長度爲4。 
  3. 再比如對於"( () () )",它的長度爲6。     

    換言之,便是有效雙括號"()"數的兩倍。

給定函數原型int longestValidParentheses(string s),

#include <string>
#include <iostream>
using namespace std;

class Solution
{
public:
    int longestValidParentheses(string s) {
        // Start typing your C/C++ solution below
        // DO NOT write int main() function
       //....
        return length;
    }
};

//start 提示:自動閱卷起始唯一標識,請勿刪除或增加。
int main()
{    
    //write your code

    return 0;
}
//end //提示:自動閱卷結束唯一標識,請勿刪除或增加。        

請完成此函數,實現題目所要求的功能。”

    如此,我可能會寫這樣一段測試代碼來驗證每一個用戶的程序是否正確,如下:

//start 提示:自動閱卷起始唯一標識,請勿刪除或增加。
int main()
{    
    char* la ="";
    char* b =    "(";    
    char* c =    ")"    ;
    char* d =    ")(";    

    char* e =    "()";    
    char* f =     "(()";    
    char* g =    "())";    

    char* h    = "()()";    
    char* i    = "()(())";    
    char* j =    "(()()";    
    char* k =    "()(()";    
    char* l =    "(()()";    
    char* m =    "(()())";    
    char* n =    "((()))())";    
    char* o =    ")()())()()(";    
    char* p =    ")(((((()())()()))()(()))(";

    Solution a;
    if (a.longestValidParentheses(la) == 0 && a.longestValidParentheses(b) == 0 && a.longestValidParentheses(c) == 0
        && a.longestValidParentheses(d) == 0 && a.longestValidParentheses(e) == 2 && a.longestValidParentheses(f) == 2
        && a.longestValidParentheses(g) == 2 && a.longestValidParentheses(h) == 4 && a.longestValidParentheses(i) == 6
        && a.longestValidParentheses(j) == 4 && a.longestValidParentheses(k) == 2 && a.longestValidParentheses(l) == 4
        && a.longestValidParentheses(m) == 6 && a.longestValidParentheses(n) == 8 && a.longestValidParentheses(o) == 4
        && a.longestValidParentheses(p) == 22
        )
    {
        cout<<"Y!"<<endl;
    }
    else
    {
        cout<<"N!"<<endl;
    }

    return 0;
}
//end //提示:自動閱卷結束唯一標識,請勿刪除或增加。    
    然後用上面那段包含測試數據的測試代碼,代替用戶的main函數進行判斷:
//start 提示:自動閱卷起始唯一標識,請勿刪除或增加。
int main()
{    
    //write your code
    return 0;
}
//end //提示:自動閱卷結束唯一標識,請勿刪除或增加。  

    這樣,只要在出題時寫好測試代碼,機器便能實現自動判題了。但很快,我們發現上述這樣的測試代碼有兩個可以優化的地方:

  1. 一個for循環代替一系列if else;
  2. 不用讓機器跑完所有測試數據,而是只要有一組測試數據沒有通過,程序立即退出,即只要機器找到用戶第一組沒有通過的測試數據即可。

2.2、for循環代替if else
    如上節所說,如果測試數據比較少量,還好說,但數據量一大,那麼就得寫很長很長一段的if else,那對coding的人來說顯得非常業餘。於是,針對下面這樣一道題來看:

  合法字符串用n個不同的字符(編號1 - n),組成一個字符串,有如下2點要求:
    1、對於編號爲i 的字符,如果2 * i > n,則該字符可以作爲最後一個字符,但如果該字符不是作爲最後一個字符的話,則該字符後面可以接任意字符;
    2、對於編號爲i的字符,如果2 * i <= n,則該字符不可以作爲最後一個字符,且該字符後面所緊接着的下一個字符的編號一定要 >= 2 * i。

問有多少長度爲M且符合條件的字符串。

例如:N = 2,M = 3。則abb, bab, bbb是符合條件的字符串,剩下的均爲不符合條件的字符串。

輸入:n,m  (2<=n,m<=1000000000);

輸出:滿足條件的字符串的個數,由於數據很大,輸出該數Mod 10^9 + 7的結果。

函數頭部

int validstring(int n,int m) {}

    我們便可以寫出如下的測試代碼:

//start 提示:自動閱卷起始唯一標識,請勿刪除或增加。
int main() {
	const int n[] = {2,2,66,123,31542/*因爲題目還在Hero線上,故只公開一部分測試數據*/};
	const int m[] = {2,1000000000,634,10000,55555535 /*只公開一部分測試數據*/};
	const int answer[] = {2,999999994,171104439,8789556,605498333 /*只公開一部分測試數據*/};
	int i;
	for (i = 0; i < 4/*實際上i不止4組*/; ++i) {
		if (validstring(n[i],m[i]) != answer[i]) {
			break;
		}
	}
	if (i >= 4) {
		puts("Y!");
	}
	else {
		printf("N!");
	}
	return 0;
}
//end //提示:自動閱卷結束唯一標識,請勿刪除或增加。    
   如此,上面這樣的一段測試代碼便能讓機器自動判斷用戶提交的每一個程序是否正確。但就像你現在去做也會體會到,系統光告訴我的程序是對是錯,估計還遠遠不夠,即如果用戶的程序錯了,那系統得告訴他怎麼錯了呀?是因爲超時,還是程序本身的邏輯錯了。

    於是,系統很快便反饋了用戶出錯的第一組數據,怎麼實現的呢?很簡單,只要把上面那段判斷出用戶的程序是錯的那部分加上出錯的那一組數據即可:

//start 提示:自動閱卷起始唯一標識,請勿刪除或增加。
int main() {
	const int n[] = {2,2,66,123,31542/*因爲題目還在Hero線上,故只公開一部分測試數據*/};
	const int m[] = {2,1000000000,634,10000,55555535 /*只公開一部分測試數據*/};
	const int answer[] = {2,999999994,171104439,8789556,605498333 /*只公開一部分測試數據*/};
	int i;
	for (i = 0; i < 4/*實際上i不止4組*/; ++i) {
		if (validstring(n[i],m[i]) != answer[i]) {
			break;
		}
	}
	if (i >= 4) {
		puts("Y!");
	}
	else {
		printf("N!\n%d %d\n",n[i],m[i]);
	}
	return 0;
}
//end //提示:自動閱卷結束唯一標識,請勿刪除或增加。

    很快,我又發現,對於出題本身來說,構造它的完整而全面的測試數據已屬不易,現在還要針對每一道題目去寫一份測試代碼,而且是每一種語言C、C++、Java、C#都寫一遍,做一次就意識到這不對勁了。可這樣一段要求爲每一道題每一種編程語言的寫痛苦的測試代碼的過程持續了整整半年,直到今年10月份。

    那是否可以簡化寫這個測試代碼的工作,讓系統本身變得更加智能呢?因爲既然關鍵是測試數據的構造,那麼在有了測試數據的前提下,是否只要填測試數據了,而不必再寫測試代碼呢?請看下面本文第3節部分。


三、出題系統本身的持續改進與優化

3.1、漫長的寫測試代碼的過程

    如上文所述,直到今年10月份,hero後臺的判題機制一直都是,針對每一道題每一種語言單獨寫一份帶main函數的測試代碼,用這段測試代碼替換掉用戶程序裏的main函數。如下圖所示:右邊的start end 代碼 替換掉用戶程序裏的 start end 代碼:


    當然,這個寫測試代碼的過程中,得到了好友曹鵬的鼎力相助,若不是他的支持,我也堅持不了6個月,thanks。但即便有他的幫助,這個漫長的寫測試代碼的過程還是令人非常煎熬。

    此外,推動自己一定把出題的過程簡化,不想寫測試代碼的重要原因還有一個:即正因爲自己要對每一道題每一種編程語言都寫一份測試代碼,導致這種出題效率異常底下,這對於整個Hero系統是十分不利的。

    因此,團隊決定,把出題的接口開放,讓所有人都可以來Hero上出題,此功能爲:社會化出題。在Hero首頁的右側邊欄,如下:

    這個時候,問題就來了,讓自己寫測試代碼也就算了,雖然不輕鬆,但至少在曹鵬的幫助下還能應對,但怎麼可以讓用戶也去爲每一道題每一種語言寫測試代碼呢?然這一切只是自己的主觀判斷,並沒有太多的實際證據支撐我的判斷,於是團隊決定,暫時先讓社會化出題上線後再說。

3.2、出題時只填測試數據,不寫測試代碼

3.2.1、出題時一個框一個框的填測試數據

    我最擔心的糟糕結果還是出現了。10月初社會化出題上線,直到10月底,儘管有一些熱心的朋友在沒有任何獎勵的情況下來Hero上出題了,但幾乎沒有任何人願意寫測試代碼。

    儘管我們走了不少彎路,導致整個進展的過程非常緩慢,但至少還是在一直前進。

    得益於整個團隊,在11月份的時候,出題終於不用再手寫測試代碼了,只需要一組一組一個框一個框的去填測試數據了。


3.2.2、出題時批量填測試數據

    如果一道題目只有不到10組測試數據,那麼出題時一個框一個框的去填測試數據是沒什麼問題的。但問題是,不存在某一道題的測試數據少於10組的情況,多的話幾百組,甚至上千組,因此,我們很快發現必須支持批量填測試數據,於是到了今年12月底,出題系統改造成了如下圖所示:


3.3.3、後續的改進、優化

    當然,直到現在,出題系統還有很多需要改進、優化的地方,如需支持數組,支持多行輸入對應多行輸出,直到最終的完全OJ模式,這些請待本文後續更新。團隊還有很多事情要做,但我們一直在努力,在整個團隊的推動下,Hero也一直在前進,從未退步。

    

相關鏈接

  1. Hero在線編程網站:http://hero.csdn.net/
  2. 本文2.1節問題的在線挑戰地址:http://hero.csdn.net/Question/Details?ID=54&ExamID=52
  3. 本文2.2節問題的在線編程地址:http://hero.csdn.net/Question/Details?ID=74&ExamID=72


後記

    感謝大家關注本編程藝術系列,歡迎大家繼續關注或貢獻編程藝術githubhttps://github.com/julycoding/The-Art-Of-Programming-by-July,也希望各位繼續支持Hero。有何問題,歡迎隨時向我反饋,本文完。

    July、二零一三年十二月二十八日。


發佈了166 篇原創文章 · 獲贊 1萬+ · 訪問量 1604萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章