15.4 四種卡關情境

接下來是四個新手常見的卡關情境。它們初遇時都很嚇人,但其實各自有固定的嫌疑犯名單——對號入座,通常很快解決。

情境一:自己電腦編譯成功,OJ 上卻編譯錯誤

程式在本機跑得好好的,一提交就 CE(Compile Error)。常見原因:

  • 本機編譯器版本較舊或較寬鬆:有些舊編譯器(例如年代久遠的 Dev C++ 內建版)會放行不合標準的寫法;OJ 用的編譯器照標準來,直接打回票。
  • 用了非標準的函式或標頭檔:只有特定環境才有的東西(例如 Windows 專屬的函式),OJ 上不存在。

處理方式:把 OJ 給你的完整錯誤訊息讀完——編譯錯誤訊息會指出行號和原因,從第一條開始修(後面的錯常常只是第一條的連鎖反應)。AACPOJ 還內建了一個查錯工具:常見編譯錯誤偵測——把程式碼貼上去,它會自動指出十九種新手常見的編譯錯誤與警告,並附上對應的教學說明。

情境二:自己電腦上範例對,OJ 上範例就錯

最詭異的情境:一模一樣的範例輸入,本機印出正確答案,OJ 卻說 WA——連範例測資都沒過。兩大嫌疑犯:

  • 未初始化變數的未定義行為15.2 那支沒初始化 sum 的程式就是活案例——本機碰巧印出正確的 55,換一台機器(OJ 的評測機)就可能是垃圾值。未定義行為的可怕正在於「本機看起來完全正常」。
  • 輸出格式不符:多印了一個空格、少印一個換行、題目要求每筆答案一行你卻印成同一行、或是查錯用的輸出忘了刪(15.5 的清理守則)。人眼看兩份輸出「長得一樣」,OJ 是逐字元比對的。

處理方式:開著 -O2 -Wall 重新編譯聽聽警告(15.2 說過,未初始化的警告要開著最佳化才會出現);把輸出格式跟題目敘述逐字元對一遍,特別注意行尾與行數。

情境三:上傳後執行結果為 RE

RE(Runtime Error)=程式執行到一半異常終止。嫌疑犯名單:

  • 陣列越界:最大宗。特別注意「本機沒事、OJ 才爆」的版本——本機測的都是小測資,OJ 的大測資才會踩出「陣列開太小」(15.3 清單第 2 項)。
  • 除以零、對零取餘數:檢查每個 /% 的右邊有沒有可能是 0
  • stoi 轉換失敗:字串不是合法數字、或超出 int 範圍(10.9),會直接拋出例外。
  • .at() 越界vectorstring.at() 檢查範圍,越界就拋例外終止。

在本機重現 RE 時,訊息長這樣(vector<int> v(5); 卻去讀 v.at(5),本站實測):

terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 5) >= this->size() (which is 5)

看起來凶神惡煞,其實資訊滿滿:out_of_range 就是「越界」,後面連「索引 5 撞上大小 5」都告訴你了。處理方式:檢查所有中括號與除號;抓不到就用 15.2 延伸的 sanitizer,讓它直接報行號。順帶一提,[] 越界是未定義行為不保證會 RE(可能悄悄給你錯的值),所以「沒 RE」不等於「沒越界」。

情境四:上傳後執行結果為 TLE

TLE(Time Limit Exceeded)=時間內沒跑完。嫌疑犯:

  • 演算法本身太慢:用上冊 4.9 的方法估運算量——超過約 10^8 就該換做法,這不是「查錯」能救的,是解法要重想。
  • 死迴圈while 條件永遠成立、迴圈變數忘了更新、EOF 輸入模式寫錯導致永遠讀不到結束。懷疑死迴圈時,在迴圈裡印圈數(15.5),一眼看出它停不下來。
  • 輸出用 endl 狂洗版12.10 實測過,10^6 行輸出 endl'\n' 慢五十倍——大輸出量的題目光這個就能 TLE。
  • 大輸入量沒開加速12.10 的兩行開場白沒寫,cin10^6 筆資料的同步成本就可能吃掉大半時限。

處理方式:先估運算量分辨「演算法慢」還是「常數慢」;前者重想解法,後者檢查 endl 與開場白。

動手試試看:故意寫一個死迴圈(while (n != 1) 但迴圈裡忘了改 n)跑跑看,感受「程式沒回應」長什麼樣子;再故意用 v.at(v.size()) 觸發一次 RE,練習讀懂錯誤訊息。