📖 附录 F
⏱️ 约 30 分钟
🎯 全部级别
附录 F:调试指南——常见 Bug 及修复方法
💡 为什么要有这份附录? 即便算法思路完全正确,只要一个 Bug 溜进代码,结果就会出错。本指南系统地整理了竞赛 C++ 代码中最常见的 Bug,按类别分类,方便快速定位。当你的解答出现 WA(Wrong Answer)、TLE(Time Limit Exceeded)、RE(Runtime Error)或 MLE(Memory Limit Exceeded)时,请先来这里查找原因。
用下面这张分类图快速判断你的 Bug 属于哪个类别:
拿到错误判决后,按照这个系统化的调试流程来排查:
F.1 整数溢出
C++ 中 Wrong Answer 最常见的根源。
问题:int 太小了
int 的最大值约为 2.1 × 10⁹(≈ 2 × 10⁹)。很多题目的中间值会超出这个范围。
// ❌ 错误:n=10^5 时,n*n 会溢出
int n = 100000;
int result = n * n; // = 10^10 → 超出 int 范围(最大 ~2×10^9)!
// ✅ 正确:乘之前先转成 long long
long long result = (long long)n * n; // = 10^10,在 long long 范围内
// 或:
long long n_ll = n;
long long result2 = n_ll * n_ll;
什么时候该用 long long
| 场景 | 需要 long long 吗? |
|---|---|
| 数组元素最大 10⁹,需要区间求和 | ✅ 需要(和最大 10⁹ × 10⁵ = 10¹⁴) |
| 最多 10⁵ 个元素的前缀和 | ✅ 需要(安全默认选择) |
| 矩阵元素、DP 中间值 | ✅ 需要 |
| Dijkstra 中的距离 | ✅ 需要(dist[u] + w 可能溢出 int) |
| 简单计数器(0 到 10⁶ 以内的 N) | ❌ int 足够 |
| 下标和循环变量 | ❌ int 足够 |
危险操作举例
📄 查看代码:危险操作举例
// ❌ 溢出示例:
int a = 1e9, b = 1e9;
cout << a + b; // 溢出(结果 > INT_MAX)
cout << a * 2; // 溢出
cout << a * a; // 灾难性溢出
// ❌ 比较时溢出:
if (a * b > 1e18) ... // a*b 本身可能已经溢出了!
// ✅ 安全版本:
cout << (long long)a + b;
cout << (long long)a * 2;
cout << (long long)a * a;
if ((long long)a * b > (long long)1e18) ... // 用 long long 比较
INF 值的选择
// ❌ 错误:在 Dijkstra 中用 INT_MAX 作为无穷大
const int INF = INT_MAX;
if (dist[u] + w < dist[v]) ... // dist[u] + w 在 dist[u]=INT_MAX 时溢出!
// ✅ 正确:使用安全的哨兵值
const long long INF = 1e18; // 用于 long long 距离
const int INF_INT = 1e9; // 用于 int 距离(留有加法空间)
F.2 差一错误(Off-By-One)
WA 第二常见的根源。
数组下标
// ❌ 错误:访问 A[n] 越界
int A[n];
for (int i = 0; i <= n; i++) cout << A[i]; // A[n] 未定义!
// ✅ 正确
for (int i = 0; i < n; i++) cout << A[i]; // 下标 0..n-1
// 或(1-indexed):
for (int i = 1; i <= n; i++) cout << A[i]; // 下标 1..n
前缀和公式
// ❌ 错误:区间和差一
// sum(L, R) 应该是 P[R] - P[L-1],而不是 P[R] - P[L]
cout << P[R] - P[L]; // 少了 A[L]!
// ✅ 正确
cout << P[R] - P[L-1]; // P[0]=0 正确处理 L=1 的边界情况
二分查找边界
📄 查看代码:二分查找边界
// 查找第一个 A[i] >= target 的下标(lower_bound 行为):
// ❌ 错误:常见二分查找写法错误
int lo = 0, hi = n - 1;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (A[mid] < target) lo = mid; // BUG:应为 lo = mid + 1
else hi = mid - 1; // BUG:应为 hi = mid
}
// ✅ 正确:标准 lower_bound 模板
int lo = 0, hi = n; // hi = n(而非 n-1),以允许"未找到"的情况
while (lo < hi) {
int mid = (lo + hi) / 2;
if (A[mid] < target) lo = mid + 1; // target 在 [mid+1, hi]
else hi = mid; // target 在 [lo, mid]
}
// lo = hi = 第一个 A[i] >= target 的下标;lo=n 表示未找到
循环边界
📄 查看代码:循环边界
// ❌ 常见错误:循环多跑或少跑一次
for (int i = 1; i < n; i++) ... // 若本意是 i=0 到 n-1,则漏了 i=0
for (int i = 0; i <= n-1; i++) ... // 正确但写法混乱;推荐 i < n
// DP 表格填充:注意递推是否访问了 i-1
// ❌ 如果 dp[i] 用到 dp[i-1],而 i 从 0 开始,则 dp[-1] 未定义!
for (int i = 0; i <= n; i++) {
dp[i] = dp[i-1] + ...; // BUG:i=0 时访问 dp[-1]!
}
// ✅ 从 i=1 开始,或单独初始化 dp[0] 作为边界条件
dp[0] = BASE_CASE;
for (int i = 1; i <= n; i++) {
dp[i] = dp[i-1] + ...; // 安全:dp[i-1] 始终有效
}
F.3 未初始化的变量
📄 查看代码:F.3 未初始化的变量
// ❌ 错误:dp 数组未初始化
int dp[1005][1005]; // 在 C++ 中包含垃圾值!
// dp[i][j] 可能因上一个测试用例或操作系统内存而非零
// ✅ 正确方式:
// 方式一:memset(按字节填充,用 0 或 0x3f 模拟正无穷)
memset(dp, 0, sizeof(dp)); // 全部置 0
memset(dp, 0x3f, sizeof(dp)); // 置为 ~1.06e9(适合用作 int 的"无穷大")
// 方式二:vector 显式初始化
vector<vector<int>> dp(n+1, vector<int>(m+1, 0));
// 方式三:fill
fill(dp, dp + n, 0);
// ⚠️ 警告:memset(dp, -1, sizeof(dp)) 将每个字节置为 0xFF
// 对 int:0xFFFFFFFF = -1(可用于"未访问"标记)
// 对 long long:0xFFFFFFFFFFFFFFFF = -1(同样有效)
// 但 memset(dp, 1, sizeof(dp)) 得到 0x01010101 = 16843009,而不是 1!
全局变量 vs 局部变量
📄 查看代码:全局变量 vs 局部变量
// C++ 中全局数组默认初始化为零
// 局部(栈)数组则不会初始化
int globalArr[100005]; // ✅ 初始化为 0
int globalDP[1005][1005]; // ✅ 初始化为 0
int main() {
int localArr[1000]; // ❌ 未初始化(含垃圾值)
int localDP[100][100]; // ❌ 未初始化
// 建议:将大数组声明为全局变量,既避免栈溢出又保证初始化
}
F.4 栈溢出(递归过深)
📄 查看代码:F.4 栈溢出(递归过深)
// C++ 默认栈大小通常为 1~8 MB
// 递归层数过深会超出限制 → 运行时错误(段错误)
// ❌ 危险:在深度为 10^5 的树上递归 DFS
void dfs(int u) { dfs(children[u]); } // 长链场景下会栈溢出!
// ✅ 解法一:用显式栈改写为迭代
void dfs_iterative(int start) {
stack<int> st;
st.push(start);
while (!st.empty()) {
int u = st.top(); st.pop();
for (int v : children[u]) st.push(v);
}
}
// ✅ 解法二:增大栈大小(平台相关,竞赛评测机通常允许)
// Linux 下编译并运行:ulimit -s unlimited && ./sol
// 经验法则:
// 递归深度 ≤ ~10^4:通常安全
// 递归深度 ~10^5:有风险,考虑迭代
// 递归深度 ~10^6:几乎必定栈溢出 → 必须用迭代
F.5 模运算 Bug
📄 查看代码:F.5 模运算 Bug
// 当题目要求输出 mod 10^9+7 时:
const int MOD = 1e9 + 7;
// ❌ 错误:忘记取模,long long 溢出
long long dp = 1;
for (int i = 0; i < n; i++) dp *= A[i]; // ~18 次大数乘法后溢出!
// ❌ 错误:减法下溢(结果为负的模)
long long ans = (a - b) % MOD; // a < b 时,C++ 返回负数!
// ✅ 正确:减法取模前加上 MOD
long long ans = ((a - b) % MOD + MOD) % MOD; // 保证非负
// ❌ 错误:DP 中忘记对中间值取模
dp[i][j] = dp[i-1][j] + dp[i][j-1]; // 迭代次数多时可能溢出
// ✅ 正确:每次加法后立即取模
dp[i][j] = (dp[i-1][j] + dp[i][j-1]) % MOD;
// ✅ 正确的模意义快速幂:
long long modpow(long long base, long long exp, long long mod) {
long long result = 1;
base %= mod;
while (exp > 0) {
if (exp & 1) result = result * base % mod; // ← 每次乘法后取模!
base = base * base % mod;
exp >>= 1;
}
return result;
}
F.6 图/BFS/DFS Bug
📄 查看代码:F.6 图/BFS/DFS Bug
// ❌ BFS:忘记在入队之前标记已访问
// 这会导致同一节点被处理多次!
queue<int> q;
q.push(src);
while (!q.empty()) {
int u = q.front(); q.pop();
visited[u] = true; // ❌ 出队后才标记 → 同一节点可能多次入队
for (int v : adj[u]) if (!visited[v]) q.push(v);
}
// ✅ 正确:入队时立即标记已访问
visited[src] = true;
queue<int> q;
q.push(src);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true; // ✅ 入队前标记
q.push(v);
}
}
}
// ❌ DFS:多个测试用例之间忘记重置 visited
// 多测问题中,每次测试用例开始前必须重新初始化 visited[]!
memset(visited, false, sizeof(visited));
// ❌ Dijkstra:距离数组用 int 而非 long long
int dist[MAXN]; // ❌ 边权最大 10^9 时,累加后溢出!
long long dist[MAXN]; // ✅
F.7 I/O Bug
📄 查看代码:F.7 I/O Bug
// ❌ 错误:大量输入时未加 ios_base::sync_with_stdio(false)
// 不加此行,cin/cout 与 C 标准 I/O 同步 → 速度极慢!
// N = 10^6 的输入量下,可能是 AC 与 TLE 的差距。
// ✅ 每道竞赛题的 main() 开头都应加上:
ios_base::sync_with_stdio(false);
cin.tie(NULL);
// ❌ 错误:使用 endl(每行都刷新缓冲区 → 很慢)
for (int i = 0; i < n; i++) cout << ans[i] << endl; // 慢!
// ✅ 正确:用 "\n" 代替
for (int i = 0; i < n; i++) cout << ans[i] << "\n"; // 快
// ❌ 错误:关闭同步后混用 cin 与 scanf/printf
ios_base::sync_with_stdio(false);
scanf("%d", &n); // BUG:解除同步后混用 C 和 C++ I/O!
// ✅ 正确:选一种方式并坚持使用
// 要么只用 cin/cout,要么只用 scanf/printf
// USACO 文件 I/O(有时题目要求):
freopen("problem.in", "r", stdin);
freopen("problem.out", "w", stdout);
// 加上这两行后,cin/cout 会自动读写文件
F.8 二维数组越界与方向
📄 查看代码:F.8 二维数组越界与方向
// 网格 BFS:边界检查差一错误
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
// ❌ 错误(实际上下面这种写法是正确的——只是确保四个条件都写到)
for (int d = 0; d < 4; d++) {
int nx = x + dx[d], ny = y + dy[d];
if (nx >= 0 && ny >= 0 && nx < n && ny < m) // ✅ 这个边界检查是正确的!
// 确保四个条件都写全
}
// ❌ 错误:弄混行列(转置了行和列)
// 若网格是 N 行 × M 列:
// A[row][col]:row 取值 0..N-1,col 取值 0..M-1
// 边界条件:row < N,col < M(不能写成 row < M!)
// ❌ 错误:多源 BFS 计算距离时,同一格子被多次访问(忘记距离检查)
if (!visited[nx][ny]) { // ✅ 只访问未访问的格子
visited[nx][ny] = true;
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
F.9 DP 专项 Bug
📄 查看代码:F.9 DP 专项 Bug
// ❌ 错误:0/1 背包内层循环方向错了
// 必须从高到低遍历容量,防止物品被重复使用!
for (int i = 0; i < n; i++) {
for (int j = W; j >= weight[i]; j--) { // ✅ 从高到低
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
// 若从低到高遍历:
for (int j = weight[i]; j <= W; j++) { // ❌ 从低到高 = 完全背包!
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
// ❌ 错误:LIS 二分查找时混淆 upper_bound 与 lower_bound
// 严格递增 LIS:用 lower_bound(找第一个 >= x 的位置,替换)
// 非严格递减 LIS:用 upper_bound(找第一个 > x 的位置,替换)
auto it = lower_bound(tails.begin(), tails.end(), x); // 严格递增
auto it = upper_bound(tails.begin(), tails.end(), x); // 非严格递减
// ❌ 错误:忘记初始化边界条件
// dp[0] 或 dp[i][0] 或 dp[0][j] 必须在主循环之前显式赋值
dp[0][0] = 0; // 始终初始化边界条件!
F.10 内存超限(MLE)
📄 查看代码:F.10 内存超限(MLE)
// 常见的 MLE 原因:
// ❌ 数组过大
int dp[10005][10005]; // = 10^8 个 int = 400MB → 超出典型的 256MB 限制!
// 计算方式:N × M × sizeof(类型) 字节
// int:4 字节,long long:8 字节
// 256MB = 256 × 10^6 字节
// int 数组最多约 6400 万个元素
// long long 数组最多约 3200 万个元素
// ✅ 一维 DP 的空间优化:
// 若 dp[i] 只依赖 dp[i-1],使用滚动数组:
vector<long long> dp(2, 0); // dp[cur] 和 dp[prev]
for (int i = 0; i < n; i++) {
dp[1 - cur] = f(dp[cur]); // 在 0 和 1 之间交替
cur = 1 - cur;
}
// ✅ 二维 DP 的空间优化(背包类):
// 若 dp[i][j] 只依赖 dp[i-1][...],只保留两行
vector<int> prev_row(W+1, 0), curr_row(W+1, 0);
快速诊断清单
拿到 WA/RE/TLE 时,按这个清单逐项检查:
Wrong Answer(WA):
-
整数溢出?(增加
long long强制转换或更换类型) - 数组边界、循环边界、区间和公式差一?
-
数组未初始化?(添加
memset或用带初始化的vector) - DP 递推方向错误?(0/1 背包:必须从高到低)
-
二分查找模板错误?(在
[1,2,3]中搜索2验证一下) - 边界情况:空输入、N=0、N=1、全部相等的元素?
Runtime Error(RE):
-
数组越界?(添加边界检查或改用
vector) - 递归过深导致栈溢出?(改写为迭代)
- 空指针/无效指针解引用?
- 除以零?
Time Limit Exceeded(TLE):
-
缺少
ios_base::sync_with_stdio(false); cin.tie(NULL);? - N=10⁵ 时用了 O(N²) 算法,应该用 O(N log N)?
- DP 中重复计算?(需要记忆化)
- BFS 中同一节点被多次访问?(入队前标记已访问)
Memory Limit Exceeded(MLE):
- 二维数组过大?(计算 N × M × sizeof 字节数)
- 递归 DFS 的隐式调用栈过深?
- 在循环中动态分配内存?
💡 专业建议: 打印中间值!
cerr << "DEBUG: dp[3] = " << dp[3] << "\n";cerr输出到标准错误流(而非标准输出),不会影响竞赛评测机上的输出。提交前记得删除所有cerr调试行。