알고리즘 디버깅 노트: 실수에서 배우는 최적화

경험을 통해 배운 디버깅 사례들을 정리합니다. 비슷한 실수를 반복하지 않기 위한 기록입니다.

위상 정렬: 인덱스 실수

원본 코드:

while (front < rear) {
    int cur = queue[front++];
    for (int idx = adj[cur]; idx; idx = nxt[idx]) {
        int nxtNode = to[idx];  // 정상
        indeg[nxtNode]--;
        if (indeg[nxtNode] == 0) {
            dist[nxtNode] = dist[cur] + 1;
            queue[rear++] = nxtNode;
        }
    }
}

오타:

int nxtNode = to[cur];  // idx 대신 cur 사용

인접 리스트 순회 시 현재 노드가 아닌 엣지 인덱스로 접근해야 합니다.

정적 배열 크기: 오차 하나의 폭

// 문제: 2×10^5 범위
struct Edge { int dest, nxt; } graph[200005];  // 누락된 0
// 올바른 선언
struct Edge { int dest, nxt; } graph[2000005];

최단 경로 개수: 자료형 선택

NOI2007 소셜 네트워크 문제에서 경로 개수가 10^10을 초과할 수 있어 long long 필수:

long long pathCnt[101][101];

비트마스킹: 연산자 우선순위

// 위험한 코드
if (j < width && (1 << j) & state == 0 && i > 0)

// 안전한 코드
if (j < width && (((1 << j) & state) == 0) && (i > 0))

비트 연산은 산술/비교 연산보다 우선순위가 낮습니다. 명시적 괄호 사용이 필수적입니다.

TSP 동적 계획법: 두 가지 전이 방식

상향식(Top-down) 접근:

for (int mask = 1; mask < pow3[n]; mask++) {
    for (int last = 0; last < n; last++) {
        if ((mask / pow3[last]) % 3 == 2) continue;
        for (int prev = 0; prev < n; prev++) {
            if (last == prev) continue;
            dp[last][mask + pow3[last]] = min(
                dp[last][mask + pow3[last]],
                dp[prev][mask] + cost[prev][last]
            );
        }
    }
}

하향식(Bottom-up) 접근:

for (int mask = 1; mask < pow3[n]; mask++) {
    for (int from = 0; from < n; from++) {
        for (int to = 0; to < n; to++) {
            if (from == to) continue;
            if (dp[from][mask] == INF) continue;
            if ((mask / pow3[from]) % 3 < 2) {
                dp[to][mask + pow3[to]] = min(
                    dp[to][mask + pow3[to]],
                    dp[from][mask] + cost[from][to]
                );
            }
        }
    }
}

순열 DP:边界的教訓

// 위험: 크기가 정확히 1 작음
long long memo[(1 << 16) - 1][16];

// 안전: 충분한 여유 공간
long long memo[(1 << 16) + 1][16];

또한 인덱스 순서 실수 주의:

// 오류
if (memo[j][mask]) continue;

// 정상
if (memo[mask][j]) continue;

세그먼트 트리: 게으른 전파

void apply(int node, int left, int right, int val) {
    tree[node] = (val == 0) ? (right - left + 1) : 0;
    lazy[node] = val;
}

void propagate(int node, int left, int right) {
    if (lazy[node] != -1) {
        int mid = (left + right) >> 1;
        apply(node << 1, left, mid, lazy[node]);
        apply(node << 1 | 1, mid + 1, right, lazy[node]);
        lazy[node] = -1;
    }
}

태그의 의미와 전파 시점을 명확히 구분해야 합니다.

이분 탐색: 경계값 처리

문제 조건: 0 ≤ M ≤ N ≤ 50000

// 위험
const int MAXN = 50000;

// 안전
const int MAXN = 50005;

세그먼트 트리 등에서는 4배 이상 할당이 표준입니다.

태그: algorithm debugging bitmask dynamic-programming segment-tree

6월 25일 21:08에 게시됨