Debug 時注意事項
- check Debug 列表 「競程選手最常犯的四大錯誤」裡的各個項目。
- 這類的 bug 都和沒看清楚題目測試資料範圍有關,舉例來說:
- 索引值用到 1~100000,陣列宣告為
a[100000]真的 ok 嗎? - 這題的數值範圍最大值雖然只有 10⁶,但答案可能是 10⁴ 個輸入的數值加起來的結果耶,答案用
int儲存真的夠嗎? - n 的上界真的是 10⁵ 嗎?該不會其實是 2 × 10⁵ 吧?
- 這題 n 的上界是 10⁶ 耶,你在程式碼怎麼寫成 100000?
- 這題雖然 n 的上界是 10⁵ 可是要讀入 2n 個數呢,陣列大小只開 10^5 夠嗎?
- 索引值用到 1~100000,陣列宣告為
- ⚠ 關鍵提醒:無論你再怎麼相信你的程式碼不會發生整數溢位,都還是嘗試把所有
int改為long long傳一次看看(可直接寫#define int long long並把int main改為signed main)
- 這類的 bug 都和沒看清楚題目測試資料範圍有關,舉例來說:
- 編譯程式碼時加上
-Wall-Wextra-Wconversion-Wshadow這幾個參數,檢查是否有潛在的錯誤。 - 檢查 Debug 列表 「細節(變數使用、邊界邏輯)思考不清楚造成的錯誤」。
- 若此題和後面列出的特定主題有關,去檢查相關主題的常見錯誤。
- 比較常見的錯誤都檢查過但仍有錯的話,再從頭到尾仔細想過程式每一行是否正確,連最開頭的
#define等都不可忽略 - 若能找到錯誤的測試資料,就模擬自己的程式碼(可藉由
cout一些變數來檢驗),看看到底哪一步和自己預期的結果不符 - 若找不到錯誤的測試資料,先嘗試看看一些在極端情況(尤其是極小)的測試資料是否能輸出正確答案,例如:
- 若輸入只要一個非負整數,依序嘗試 0, 1, 2, 3,... 是否輸出正確答案
- 輸入只有一個數列,依序嘗試:[1], [2], [1,1], [1, 2], [2, 1] [2, 2], [1, 1, 1], [1, 1, 2], [1, 1, 3], ...
- 輸入為一個圖,嘗試一個點、兩個點、三個點的圖等等
- ⚠ 關鍵提醒:請適時檢查自己到底有沒有讀錯題目或傳錯題目
- 若是極小測資都試不出錯誤,那可想想是不是存在一些只有在極大的測試資料會發生的 bug (尤其是整數溢位、陣列開太小、答案忘記取模等)
- 請仔細想想,你使用的演算法真的是對的嗎?是否有某些你覺得很顯然就沒仔細證明的步驟其實是錯的呢?
- 情非得已時可嘗試暴力對拍
- 若真的想求助別人時,請先比對自己的程式碼和其他人寫的相同演算法的程式碼有哪些地方不一樣,仔細思考那些不一樣的地方是否有機會造成錯誤。(若是老師上課講解過的題,通常能在投影片或影片裡能看到老師的程式碼。若還是看不出來,就一步一步的把自己的程式碼改成和別人的程式碼一樣,每改一個小地方就測試看看是否能輸出正確答案,藉此來定位到底錯在哪。
常見情境
1. 程式碼在自己電腦上能編譯成功,但在 OJ 上卻編譯錯誤
-
上傳時選擇的編譯器版本是否正確呢?(例如使用了
gcd()函式,編譯器版本卻使用 C++14) -
是否忘記 include 某些函式所需的標頭檔呢?(有些環境在使用某些函式庫時,就會自動去 include 其他你沒 include 的函式庫)
-
是否有在全域變數裡使用了某個變數、函數、結構體名稱和 OJ 環境提供的函式庫裡的變數名稱撞名呢?例如以下程式碼在 Codeforces 上使用 GNU G++23 去編譯:
#include<iostream> using namespace std; int data; int main() { cin >> data; cout << data << '\n'; }會有以下編譯錯誤訊息:
program.cpp:5:12: error: reference to 'data' is ambiguous -
是否使用了 OJ 上不支援的 C++ 擴充功能呢?(如以下程式碼在 CLANG 編譯器無法通過)
#include<iostream> using namespace std; int main() { int n; cin >> n; int d[n] = {1}; cout << d[0] << '\n'; } -
是否在全域變數裡開了一個超大(超過記憶體限制)的陣列呢?(如以下程式碼)
#include<iostream> using namespace std; int a[1000000000]; int main() { cout << a[999999999] << '\n'; } -
是否在全域變數開了一個很大(如 10⁷ 個元素) 的陣列且還給了非 0 的初始值呢?(如以下程式碼)
#include<iostream> using namespace std; int a[10000000] = {1}; int main() { cout << a[9999999] << '\n'; } -
是否加了 OJ 上不支援的編譯、硬體優化呢?
- 目前 Codeforces 上編譯選項若選 GNU G++20 13.2 (64 bit, winlibs),將無法使用硬體優化,必須換其他 C++ 選項
- 目前 NTU CPC Online Judge 不支援某些 pragma 參數
2. 在自己電腦上執行 Sample 輸出的結果正確,但在 OJ 上範例測試資料就錯時
- 你的程式在 Sample 的輸出真的是對的嗎?是否有多輸出 Debug 訊息呢?空白和換行的格式有符合題目需求嗎?輸出是字串時,大小寫是否正確呢?是否有拼錯單字呢?是否有輸出一些不可視字元呢?
- 你的程式碼是否含有未定義行為呢?最常見的有:
- 陣列開太小
- 使用到不該使用記憶體位置
- 區域變數未初始化
- 是否有使用
cin.tie(0); ios_base::sync_with_stdio(false);時,卻把 std 的cin、cout和 C 語言的scanf/printf/puts混用的情況呢? - 是否已讀入資料後才執行
cin.tie(0); ios_base::sync_with_stdio(false);呢? - 是否用了一些特殊的編譯優化(如
O3以上的優化、unroll-loops等)或是加了些硬體優化導致出錯呢? - 該題的 I/O 格式是標準輸入/標準輸出嗎?是否其實是需要讀檔寫檔呢?
- 非常確定自己在 OJ 上錯的是 Sample 嗎?有沒有可能出題者並沒有把範例放在第一組測試資料?
- 是否有使用了執行結果並不總是一樣的內建函式呢?
- 例如使用 sort 函式時,若兩個被排序的不同物件被比較函式視為一樣時,並不保證誰會排前面誰會排後面。
3. 遇到 RE(Runtime Error) 的可能原因
-
陣列/
vector用到的索引值超出合法範圍 常見情境,使用 while 迴圈檢查陣列裡有哪些數滿足,檢查到第一個不滿足或結尾位置就要停下來,但忘記判斷結尾位置。// 給一個長度為 n 的數列,判斷有多少數和數列第一個數一樣。 int n; cin >> n; for(int i = 0; i < n; i++) { cin >> a[i]; } int ans = 0; while(a[ans + 1] == a[0]) ans++; cout << ans << '\n'; -
使用內建函數時傳錯參數(可參考以下例子)
例一:
sort(d + 3, d + 2);例二:
set<int> d{1,2,3}; d.erase(d.end()); -
使用
STL容器時,insert/push/push_back太多元素,記憶體不足造成函式執行失敗,因此 RE(也可能是MLE)(可參考以下例子)set<int> d; for(int i = 0; i < 100000000; i++) d.insert(i); -
在函式(包括主函式)裡面開太大的記憶體
int main() { int a[1000000000]; } -
整數除法或取餘時除以 0
-
在 Codeforces 系統上因為 bug 造成輸出太多東西時也會得到
RE。
4. 遇到 Time Limit Exceed 的時候
-
請仔細分析自己程式碼的時間複雜度,是否正確
-
是否記得加 I/O 優化呢?
-
是否是因為執行太多次
cout << endl;而超時呢? -
是否不小心寫出無窮迴圈呢?(以下是一個例子)
for(int i = 0; i < n; i--) { /*...*/ } -
請確認程式碼使用的內建函式的執行時間複雜度是否和你預期的一樣 (見 15. 內建函式或語法誤用導致 Time Limit Exceed)
-
是否有某個函式使用
pass by value傳遞某個記憶體很大的物件且呼叫非常多次呢? (可在 此 youtube 影片 此錯誤的示範) -
是否有在多次(例如在迴圈裡或在一個被多次呼叫的函式裡)宣告一個元素非常多的
vector或是在迴圈裡面宣告一個很大的陣列並初始化呢?要記得宣告 N 個元素的vector或初始化一個長度 為 N 的陣列時間複雜度為O(N),所以若宣告 T 次,時間複雜度就是O(Tn)。 -
程式碼含有未定義行為也有可能超時(陣列開太小等)
-
unordered_set/unordered_map在測資有刻意構造時,每次操作的時間複雜度會退化到O(容器裡元素數量) -
請確認是否有寫出一些常見的大常數程式碼,如:
- 在元素數量量級超過 10⁶ 時使用關聯容器會很慢
- 無序關聯容器比陣列慢
- 在使用
int即可的陣列以long long宣告,如有大量的陣列取值操作,會慢 2 倍左右 - 用多層迴圈去使用多維陣列裡的值時,陣列維度的順序會嚴重影響常數(可在 此 youtube 影片 看詳細例子)
-
在 Codeforces 上,有非常多題若使用無序關聯容器(
unordered_set/unordered_map) 可能會被刻意構造的測資導致執行時間達到 worst case,也就是單個操作要花O(n) -
在 Codeforces 上,大量使用
long long時,使用 32 bit 的編譯器會比較慢(也可能有其他因素造成不同 C++ 編譯器會嚴重影響程式執行時間常數)
Bug 列表
1. 語法班學生常見錯誤
- 語法錯誤請見 AA 競程 hackmd:C++ 新手常見語法錯誤搜集
- 模板打錯
#include<iostream> using namespace std; int main() { } - 忘記分號
cin,cout用錯運算子 (>>,<< 用反)- 忘記宣告變數、變數宣告在使用之後
- 大括號位置放錯
- 指派運算子順序搞反
- 使用到全形符號
- 同一個 token 中出現多餘空白
- 變數名稱大小寫混用
- 誤以為 C++ 的運算式使用的括號有大中小括號之分。
- 模板打錯
- 輸出兩個以上的變數時,變數之間忘記以換行或空白做為分隔
- 在 Codeforces 上誤把
long當成long long使用(Codeforces 上的long只有 32 bits - 還沒讀入變數就先使用該變數做事了
- 把
=和==搞混
2. 競程新手常見錯誤
- I/O 太慢 (見 5. I/O 相關)
- 忘記陣列、
vector、string索引值是從 0 開始 - 不會估算程式碼執行時間,以至於浪費大量時間寫一個一定不會通過的演算法
- 不知道讀入 C 語言字串時,字串末尾會多加一個
null字元,導致字元陣列開不夠大 vector或string的 size 不夠大就使用不在合法範圍的索引值- 不知道浮點數無法正確儲存任何數值/不知道
float儲存的數值誤差非常大/不知道浮點數計算、使用浮點數相關函式會有誤差
例子一:
long long v = pow(3, 39);
cout << v << endl; // 在 CF 上輸出 4052555153018976256,但應為 4052555153018976267例子二:
long long x = 1000000000000000001;
long long y = 10;
long long v = ceil(x * 1. / y);
cout << v << endl; // 在 CF 上輸出 100000000000000000,但應為 100000000000000001- 誤以為
^是指數的意思
3. 增加編譯參數後能發現的錯誤
- 函式忘記回傳值
- 忘記輸入/輸出
- 變數忘記給初始值
- 應該使用浮點數的變數宣告成整數,並且在程式碼裡有把其他浮點數指派給該變數
- 因為變數遮蔽導致使用錯誤的變數
4. 競程選手最常犯的四大錯誤
integer overflow- 應為
long long的變數宣告成int - 輸出需取餘的題有某個運算因忘記取餘而 overflow
- 有些題(如二分搜題) 就算開
long long也有機會integer overflow,可用其他方式避免// 以下幾個例子讓大家更正確理解 C++ 運算的原理 int a = 100000, b = 100000; long long x = a * b; // 錯誤 long long x = (long long)(a * b); // 錯誤 long long x = a * b * 1LL; // 錯誤 long long x = a * 1LL * b; // 正確 long long x = (long long)a * b; // 正確
- 應為
- 陣列、
vector大小相關- 陣列、
vector開太小 - 再多組測資的題目裡陣列、
vector開太大導致超時 - 用字元陣列儲存字串時,忘記陣列大小至少比字串長度多 1
- 陣列、
- 初始化相關:
- 忘記初始化(可藉由增加編譯參數
-Wall迅速發現) - 給了錯誤的初值
- 多組詢問時忘記重新初始化
- 忘記初始化(可藉由增加編譯參數
special case(輸入的值在範圍的極小值或極大值時) 特判錯誤或忘記特判
5. 細節(變數使用、邊界邏輯)思考不清楚造成的錯誤
- i,j,k,n,m 等變數搞混 (例如某個指令應該要使用變數 i,卻用成變數 j)
- 編號 0 ~ n-1 或 1 ~ n 搞混
- 編號從 0 或從 1 開始搞混
- 迴圈的起始條件或結束條件沒想清楚(差個正負 1,或是
<=、<搞混) continue不小心把某個迴圈內每輪都要執行的事情跳過了
6. I/O 相關
- 不知道
endl很慢(請參考AA 競程 Level 1 試聽課 20:影響執行時間常數的因素 4 --- I/O 處理方式) - 不知道使用
cin且輸入的資料量很大時,必須加 I/O 優化(請參考AA 競程 Level 1 試聽課 20:影響執行時間常數的因素 4 --- I/O 處理方式) - 不知道使用 I/O 優化時,C 語言和 C++ 的輸入輸出不能混用
- 不知道 I/O 優化一定要加在第一次輸入之前
7. 浮點數相關
-
使用
float導致算出來答案誤差非常大 -
輸出的小數點位數不夠多(或沒使用
fixed) -
不知道浮點數運算會有誤差
-
不知道很多數學函式擁有浮點數誤差而去使用 (請參考此篇文章:PSA: don't use these functions unless you really, really need to)
-
不知道使用科學記號的常數一定是浮點數,寫出類似以下的程式碼,誤以為這樣寫
INF的值會是 10¹⁸+1,但其實輸出INF看就知道,+1 會因為浮點數誤差而被忽略,實際上INF值為 10¹⁸。long long INF = 1e18 + 1; -
不知道輸出固定位數的規則並不是四捨五入,而是四捨六入五成雙 (請嘗試以下程式碼)
#include <iomanip> #include <iostream> using namespace std; int main() { cout << fixed << setprecision(1) << 1.25 << '\n'; } -
答案為 0 時,輸出 -0``
-
不知道就算使用 I/O 優化,讀入浮點數還是很慢,若實際上輸入的值是整數,請一律要使用整數讀入
calculate_circle_area_double() { // 很慢 double x; cin >> x; cout << x * x * 3.14159; } calculate_circle_area_int() { // 比較快 int x; cin >> x; cout << x * x * 3.14159; }
8. 多組詢問相關 Bug
- 新的詢問忘記初始化
- 每次都對整個陣列或
vector做初始化,但題目的測試資料組數很多,只保證所有測試料 n 的總和不會太大。必須改為每個詢問只初始化該組詢問會影響到的記憶體。 - 多組詢問裡每次詢問都開一個很大的
vector或陣列(忘記動態的宣告vector或陣列時,vector或陣列的初始化也要花時間) - 一個檔案有多組測試資料時,還沒讀完所有 input 就
return或break或continue了(尤其容易錯在有特判 case 的時候)
9. C 語法和 Python 的差別
-
比較運算子的用法
int a = 1, b = 4, c = 3; if(a < b < c) { // 錯誤寫法 cout << "a < b and b < c\n"; } else { cout << "a >= b or b >= c\n"; } if(a < b && b < c) { // 正確寫法 cout << "a < b and b < c\n"; } else { cout << "a >= b or b >= c\n"; }a = 1 b = 4 c = 3 if a < b < c: print("1 < 4 < 3") else: print("not 1 < 4 < 3") a = -3 b = -2 c = -1 if a < b < c: print("-3 < -2 < -1") else: print("not -3 < -2 < -1") -
整數除法和取餘的規則 (CF 597 A. Divisibility)
-
if else 匹配方式
int x = 4; if(x > 2) if(x == 3) cout << "x == 3\n"; else cout << "x <= 2\n"; cout << "Done\n";x = 4 if x > 2: if x < 3: print("x is 3") else: print("x <= 2") print("Done")10. C++ 語法相關
-
記錯運算符號的優先順序
- 且(&&)的優先級高於或(||)
- 位元運算和其他運算的優先順序
-
沒把型態為
unsigned int的size()轉型成int -
忘記傳參數
pass by value是複製整個 object,執行時間和 object 大小有關 -
函數傳的參數是陣列時,並無法使用 sizeof 取得陣列大小
-
對
vector使用Ranged-based for,卻在迴圈裡修改vector大小。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> d{1, 2};
for (int x : d) {
cout << x << '\n';
if (x <= 5) d.push_back(x + 2);
}
}11. 邏輯控制
continue,break,return搞混- if 裡忘記
return或break(尤其是在判斷special case時) - if,else if 搞混
12. unspecified behavior
- a[i] = i++;`` 這類一個 expression 裡使用對一個變數 Increment/decrement operators 卻讓該變數出現 2 次以上。
cin>> x >> a[x];可能會先讀入 a[x]在讀入 x``。只要在一個 expression 裡中括號取值運算以及放在索引值的那個變數也會做改變,就可能會出問題,在 C++ 17 以後才有嚴格定義。(類似狀況)
13. 變數或狀態初值
- 以初值為 0 或 -1 代表一個還沒被處理過的狀態,但沒考慮到就算處理過也有可能是 0 或 -1
- 代表無限大或無限小的初值設得不夠大或不夠小
- 初值是無限大時,設為
INT_MAX或LLONG_MAX,但卻把它拿去運算造成integer overflow
14. 內建函式或語法誤用導致 Time Limit Exceed
- 誤以為
strlen()的時間複雜度是O(1)。 實際上執行時間是O(len), len 為字串長度) - 誤以為
vector的insert()、erase()的時間複雜度是O(1)。 實際上執行時間和插入、移除的位置至end()的距離有關 - 對於關聯容器的
lower_bound()、upper_bound()使用方法錯誤
set<int> s;
auto it = lower_bound(s.begin(), s.end(), v); // 錯誤,時間複雜度和 s 大小有關
auto it2 = s.lower_bound(v); // 正確,時間複雜度為 O(log(s.size()))unordered_set/unordered_map的成員函式clear()的時間複雜度和bucket的 size 有關,若一個檔案有多組測試資料,第一個詢問就先插入很多東西,第二個詢問以後就每個詢問只插入一個東西,就會超時。(相關討論:https://codeforces.com/blog/entry/127773)
15. 內建函式或語法誤用導致 Wrong Answer(或 RE)
accumulate()的誤用 參考程式碼- 誤以為
isdigit()、isupper()、islower()等函式當成立時是回傳 1(實際上是回傳非零的數) - 使用
pow()、sqrt()等數學函式會造成誤差 vector套vector用resize()調整大小會出問題,應該要使用assign。vector<vector<int>> d(3, vector<int>(2)); d.resize(4, vector<int>(3)); /* 執行完此行後,vector 會長相如下: * { * {0, 0}, * {0, 0}, * {0, 0}, * {0, 0, 0}, * } */- 使用
lcm()傳入兩個int,導致 overflow 如下:cout << lcm(100000, 99999) << '\n'; // 預期輸出 9999900000,但輸出 1409965408
16. 內建函式或語法誤用發生未定義行為
- sort 的比較函式在兩物件相等時回傳
true __log()、__builtin_clz()、__builtin_ctz()等函式傳入 0
特定主題的 Bug 列表
1. 和排列有關的題
- 把排列和排列的反函數搞反,例如,題目要你輸出數字 i 的位置在哪,你卻輸出成第 i 個位置得數字是什麼。
2. 排序
- 起手步是排序但忘記寫,並且範例測試資料也不會因此出錯
- sort 沒有規定等價的物件會按照原來的順序排列,
stable_sort才會 - 排序時所用的比較函式在兩個物件相等時回傳
true會造成錯誤,WA,RE,TLE 都是有可能再犯此錯誤時取得的評測結果。(因為比較函式是在定義「小於」的關係,若兩物 x 和 y 相等,x 並沒有小於 y。)
3. 答案需要對某個值取餘輸出的題
- 根本忘記答案要取餘
- 計算過程中因為有減法,可能最終答案是負的,必須把它轉為非負(也就是說最終答案要加上
if(ans<0)ans+=MOD;) if(x >= MOD) x -= MOD;中的>=寫成>special case、初值忘記取餘(尤其是模數為 1 的時候)- 計算過程因忘記取餘而 overflow(在讀入的數字本來就超出
int範圍時特別容易犯這種錯) - 取餘運算用太多次造成常數太大超時
- 除數是常數時,用全域變數儲存但沒有加
const導致常數太大超時
4. 二維地圖實作
- 編號從 0 開始和從 1 開始搞混
- 座標編號方式和題目不一樣。例如寫成 (x,y) 是從左往右第 x 個數,從下往上第 y 個數,但題目的描述卻是從上往下第 x 個數,從左往右第 y 個數。
- 地圖長和寬的變數使用搞混
- 使用到超出邊界的位置
5. 位元運算與位元相關函式
- 2 的 n 次方在程式碼裡寫成 2 << n``
- 2 的 n 次方的運算結果在
long long範圍時,寫成 1 << n,但應為 1LL << n,就算 n 是long long型態也會錯,這是使用#define int long long也無法避免integer overflow的少數例子之一 - 搞錯位元運算和其他運算的優先級,例如說,以為 2 的 n 次方再加 1 可寫成 1 << n + 1
,但實際上必須寫成 (1 << n) + 1。 - 不知道使用
a << b或a >> b時,b 的值大於等於 a 的資料型態的位元數或 b < 0 時是未定義行為。// #include <iostream> using namespace std; int main() { int x; cin >> x; // try entering 128 or -1 as input cout << (1 << x) << '\n'; // undefined behavior: shifting left by 128 bits exceeds int width cout << (1 >> x) << '\n'; // undefined behavior: shifting right by 128 bits exceeds int width } - 不知道使用
a << b時,a << b的運算結果超過 a 的資料型態所能儲存的範圍是未定義行為#include <iostream> using namespace std; int main() { cout << (1 << 32LL) << '\n'; } a << b在 a 是負數的時候,是未定義行為a >> b在 a 是負數的時候,C++17 以前是實作定義行為cout一個含有位元運算的表達式時,忘記把表達式加括號,可能會編譯錯誤或輸出非預期的東西int n = 5; cout << 1 << n << endl; // 輸出非預期結果int a = 2; int b = 3; cout << a ^ b << endl; // 編譯錯誤- builtin 系列函式當傳入的參數是
long long資料型態時,應加上ll的後綴 __lg()、__builtin_ctz()、__builtin_clz()等函式傳入的參數為 0 時是未定義行為- 沒使用
#pragma GCC target("lzcnt,popcnt")導致__builtin_popcount和__builtin_ctz不夠快 (但照理說競程不該考這種東西...)
6. 關聯容器(Associative Containers)
- 共通注意事項:
-
沒使用成員函式的
lower_bound、upper_bound,而去使用<algorithm>中的lower_bound、upper_bound造成TLEset<int> d{1,2,3}; lower_bound(d.begin(),d.end(), 2); // 錯誤,時間複雜度為O(d 的大小) d.lower_bound(2); // 正確 -
用太多關聯容器
O(log{n})時間複雜度的成員函式如insert、erase、find、lower_bound、upper_bound等導致TLE -
iterator指在begin()的位置時再把此iterator--是未定義(指在end()時再++也是未定義)(cppreference 說明) -
若
iteratorptr已失效或指在end()的位置,使用erase(ptr)或*ptr等都是未定義行為 -
當一個
iterator被 erase 後,該iterator就失效,不能再使用,舉例來說,以下程式碼會出錯:// 讀入 n 個數的多重集,有 m 次操作,每次操作給定一個數 x,移除多重集裡 >= x 的最小的數,並把所有移除的數總和輸出 int n, m; multiset<int> d; cin >> n >> m; while(n--) { int x; cin >> x; d.insert(x); } long long ans = 0; while(m--) { int x; cin >> x; auto it = d.lower_bound(x); d.erase(it); ans += *it; // 此為錯誤寫法,正確寫法應把此行放在 erase 之前 } cout << ans << '\n';
multiset 的陷阱:
- 呼叫
T.erase(x)時,會把 container 裡所有的 x 都移除,若只想移除恰一個 x,必須使用T.erase(T.find(x)); - 若使用
count(x)來判斷容器裡是否存在至少一個 x 有機會超時,因為count(x)的執行時間正比於 x 在 container 裡的數量,應使用T.find(x) != T.end()來判斷
map 的陷阱:
- 對於
map<T1,T2> A,只要使用了A[x]就相當於在 A 裡面插入的 key 為 x 的東西,有些問題map裡只有 N 個東西,但會需要詢問 Q 次map是否有 key 為某個值的東西,若使用[]來判斷map裡是否有某個東西,就會讓 A 裡的元素數量高達O(N + Q),可能因此超時。
7. 二分搜
以下以兩種常見的整數二分搜寫法為例。假設 valid() 是一個判定函式,在搜尋範圍上呈現先 false 再 true 的單調模式,我們要找使得 valid(x) = true 的最小 x。
寫法 A:while(lo < hi),迴圈結束時 lo == hi 即為答案位置。
int lo = 0, hi = INF;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (valid(mid)) hi = mid;
else lo = mid + 1;
}
// lo == hi 是答案位置寫法 B:while(lo <= hi),用 ans 記錄最後一次滿足條件的 mid。
int lo = 0, hi = INF, ans = -1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (valid(mid)) { ans = mid; hi = mid - 1; }
else lo = mid + 1;
}
// ans 即為答案-
lo和hi的初始值沒有涵蓋所有可能的答案。例如忘記答案可能是 0(lo設成 1)、忘記答案可能是最大值(hi設太小)。對答案二分搜時,hi應該設為答案的理論上界而非輸入資料範圍的上界。寧可把範圍設大一點,多跑幾次迭代只多O(log),遠比WA好。 -
使用
mid = (lo + hi) / 2時,當lo + hi超過long long上限(約 9.2 × 10¹⁸)會溢位。此外,C++ 的整數除法是向零取整而非向下取整,當lo為負、hi為正時,mid可能偏移導致搜尋方向錯誤或無窮迴圈。應一律使用mid = lo + (hi - lo) / 2。// 危險:lo + hi 可能溢位 long long mid = (lo + hi) / 2; // 安全寫法 long long mid = lo + (hi - lo) / 2; -
判定函式內部的運算也可能溢位,這在對答案二分搜的題目中特別常見。例如 CSES - Factory Machines(錯誤程式碼)中,
now += mid / k[i]每一項最大約 2 × 10¹⁸,累加 n 項後超過long long上限。修正方法是累加過程中一旦now >= t就提前return true,不繼續加。乘法也可能溢位,例如(mid / w) * (mid / h)兩個long long相乘超過上限,可改用除法判斷或__int128。判定函式裡的每一步運算都要想:「把最大值帶進去會不會爆?」bool valid(long long mid) { long long now = 0; for (int i = 0; i < n; i++) { now += mid / k[i]; if (now >= t) return true; // 提前結束,避免溢位 } return false; } -
使用
while (lo < hi)的寫法尋找最後一個滿足條件的值時(即lo = mid而非lo = mid + 1),mid = (lo + hi) / 2向下取整會導致當hi = lo + 1時mid = lo,使得lo = mid永遠不動,陷入無窮迴圈。修正方式是改成mid = (lo + hi + 1) / 2(向上取整),或改用while (lo <= hi)的寫法(lo = mid + 1和hi = mid - 1保證每次範圍都會縮小)。// 錯誤:找最後一個 <= x 的值 while (lo < hi) { int mid = (lo + hi) / 2; // hi = lo + 1 時 mid = lo if (a[mid] <= x) lo = mid; // lo 永遠不變 → 無窮迴圈 else hi = mid - 1; } -
使用
while (lo < hi)的寫法時,迴圈收斂後lo == hi是正確位置,但valid(lo)可能從未被呼叫過。如果你在valid()裡計算並儲存答案(例如存到全域的ans變數),最後一步若走lo = mid + 1,新的lo值從未傳入valid,ans仍是舊值。修正方式是迴圈結束後補一次valid(lo)。while (lo < hi) { /* ... */ } valid(lo); // 必須補這一行,否則 ans 可能是錯的 -
浮點數二分搜使用
while (lo + EPS < hi)時,當lo、hi很大(例如 10¹⁸),浮點數的精度不足以區分hi - lo和EPS的差異,hi - lo可能永遠大於EPS,導致無窮迴圈或超時。改為固定迭代次數是比較安全的寫法,迭代 100 次精度可達 2^{-100}。double lo = 0, hi = 1e18; for (int iter = 0; iter < 100; iter++) { double mid = (lo + hi) / 2; if (valid(mid)) hi = mid; else lo = mid; } -
二分搜的前提是判定函式的回傳值在搜尋範圍上呈現
false...false true...true(或反過來)的單調模式,如果不滿足就不能用二分搜。例如:給 n 個數字,能不能選恰好 k 個使得總和 = S?「能選出總和 ≥ S 的子集」不代表「能選出總和恰好 = S 的子集」,判定函式不單調。
8. 圖論相關問題
- 讀題時沒看清楚題目是否有所有點是連通的這個條件,若不連通可能會有更多瑣碎細節要處理。
- 讀題時沒看清楚題目是單向邊還是雙向邊以至於細節寫錯,且有些題在單/雙向邊的解法並不同。
- 讀題時沒看清楚題目會不會有重邊或自連邊,若有重邊或有自連邊可能會有更多瑣碎細節要處理。
9. DFS 等圖論相關問題
- DFS 時,忘記標記一個點是否走過,或忘記遇到標記過的點就 return(尤其是在 DAG 的問題犯這個 Bug 也可以通過測資)
- DFS 時,雖然有標記一個點已經走過,但離開遞迴函式時起把標既取消以至於超時(這樣的 DFS 時間複雜度是指數量級)
- 遇到非連通圖的問題時,忘記每個點都要當起點 DFS 一次
10. 動態規劃問題
- 初始狀態的初值給錯
- 不合法的狀態的值也轉移到合法的狀態
- 用
dp值是否大於 0 來判斷一個狀態是否已經算過,但實際上狀態的答案可能是 0 (0 只是個舉例,也有可能是 -1) - 沒有按照拓撲排序的順序計算
DP每個狀態的值 - 計數題要求輸出答案對 P 取餘的結果,且範圍必須落在 [0, P-1] 之間,但卻因為計算過程中有減法而造成輸出有負數
- push 的思維也使用 DFS 的寫法
11. Dijkstra
- 同一個點從
priority_queue裡拿出來的每一次都會枚舉他相鄰的所有邊,時間複雜度變為O(n^2 log n),如下:
while(!pq.empty()){
now = pq.top();
pq.pop();
for(auto x : adj[now.second]){
if( dis[now.second] + x.second < dis[x.first] ){
dis[x.first] = dis[now.second] + x.second ;
pq.push(mkpr(dis[x.first], x.first));
}
}
}12. 倍增法
- 倍增法時常需要計算不超過 n 的最大的 2 的幂次方,若此件事用
log(n)的時間計算且每組詢問又重複計算這樣的事情log(n)次,將會使得時間複雜度變為O(Q log{n}²)以致於超時。 - 使用
__lg()函式時不小心傳入 0 導致不可預期結果(此為未定義行為)。
13. BIT
- 詢問時詢問到位置 0,也就是程式碼會執行 pos = 0; for(;pos <= n; pos += pos & -pos) ans += bit[pos];`` 造成超時。
14. 線段樹
- 線段樹大小只開
2n,而不是4n。 - 線段樹區間修改或區間詢問的部分
return的條件寫錯,導致遞迴至區間裡的每個葉子節點。
相關資源
AA 競程 · Debug 手冊
本手冊內容由 dreamoon 撰寫,版權歸 AA 競程(aacpschool.com) 所有。
原始內容來源:GitHub — aacpschool/aacp-resources
未經授權,禁止轉載、修改或用於商業用途。如需引用,請標註出處並附上原始連結。
© 2022–2026 AA Competitive Programming School. All rights reserved.