본문으로 건너뛰기
Previous
Next
C++ 그래프 알고리즘 완벽 가이드 | BFS·DFS·다익스트라·최소신장트리 [실전]

C++ 그래프 알고리즘 완벽 가이드 | BFS·DFS·다익스트라·최소신장트리 [실전]

C++ 그래프 알고리즘 완벽 가이드 | BFS·DFS·다익스트라·최소신장트리 [실전]

이 글의 핵심

그래프 탐색(BFS/DFS), 최단 경로(다익스트라), 최소신장트리(Kruskal, Prim)를 C++로 구현. 문제 시나리오, 완전한 코드, 흔한 실수, 성능 팁, 프로덕션 패턴.

들어가며: “최단 경로가 10초나 걸려요”

그래프 알고리즘 선택의 함정

지도 앱에서 A지점에서 B지점까지 최단 경로를 찾을 때, 잘못된 알고리즘을 쓰면 노드 1만 개만 있어도 수 초가 걸립니다. 비유하면 “모든 도로를 일일이 걸어보는 것”과 “다익스트라로 효율적으로 찾는 것”의 차이입니다.

// 실행 예제
flowchart TD
  subgraph wrong[❌ 잘못된 선택]
    W1[모든 경로 탐색] --> W2[DFS 브루트포스]
    W2 --> W3[지수 시간 O(2^n)]
    W3 --> W4[1만 노드 → 수 초~수 분]
  end
  subgraph right[✅ 올바른 선택]
    R1[가중치 그래프] --> R2[다익스트라 O(E log V)]
    R2 --> R3[우선순위 큐 활용]
    R3 --> R4[1만 노드 → 밀리초 단위]
  end

이 글에서는 실제 문제 시나리오부터 BFS·DFS·다익스트라·최소신장트리·Union-Find의 완전한 C++ 구현, 흔한 실수, 베스트 프랙티스, 프로덕션 패턴까지 다룹니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

1. 문제 시나리오

시나리오 1: 지도 앱 최단 경로

상황: 5,000개의 교차로와 2만 개의 도로가 있는 도시에서 A→B 최단 거리를 찾아야 합니다. 잘못된 접근: DFS로 모든 경로를 탐색하면 지수 시간이 걸립니다. 해결: 가중치(거리)가 있는 그래프이므로 다익스트라 O(E log V) 사용.

시나리오 2: 소셜 네트워크 “친구의 친구” 탐색

상황: 사용자 A로부터 3단계 이내의 모든 연결 사용자를 찾아야 합니다. 잘못된 접근: DFS는 깊이 우선이라 “거리” 개념이 없습니다. 해결: BFS로 레벨별 탐색. 각 레벨이 “N단계 이내”에 해당합니다.

시나리오 3: 빌드 시스템 의존성 순서

상황: CMake/빌드 시스템에서 모듈 A→B→C 의존성이 있을 때, 올바른 빌드 순서를 정해야 합니다. 잘못된 접근: 단순 반복으로는 순환 의존성 감지가 어렵습니다. 해결: DFS 기반 위상 정렬. 사이클 감지와 함께 빌드 순서 생성.

시나리오 4: 네트워크 케이블 최소 비용 배치

상황: 100개 건물을 연결하는 데 케이블 비용을 최소화해야 합니다. 잘못된 접근: 모든 연결을 시도하면 비효율적입니다. 해결: 최소신장트리(MST) — Kruskal 또는 Prim. 모든 정점을 연결하면서 간선 비용 합 최소화.

시나리오 5: 게임 AI 경로 탐색 (2D 그리드)

상황: 탑뷰 게임에서 유닛이 장애물을 피해 목표 지점까지 이동해야 합니다. 셀 단위로 이동하며, 가중치는 없습니다. 잘못된 접근: A* 같은 휴리스틱 없이 DFS로 탐색하면 비효율적이지만, 단순 그리드라면 BFS가 더 적합합니다. 해결: BFS로 레벨별 확장. 각 셀을 정점, 상하좌우를 간선으로 모델링. 최단 이동 횟수(간선 수)를 O(V+E)에 구합니다.

시나리오 6: 동적 연결성 확인 (Union-Find)

상황: SNS에서 “A와 B가 친구인가?” 또는 “두 사용자가 같은 그룹에 속하는가?”를 반복적으로 질의합니다. 친구 관계가 실시간으로 추가됩니다. 잘못된 접근: 매번 BFS/DFS로 연결 여부를 확인하면 O(V+E) × 쿼리 수로 비효율적입니다. 해결: Union-Find(Disjoint Set Union). unitefind가 거의 상수 시간에 동작. Kruskal 알고리즘의 핵심 자료구조이기도 합니다.

알고리즘 선택 가이드

문제 유형추천 알고리즘시간 복잡도
최단 경로 (가중치 있음)다익스트라O(E log V)
최단 경로 (가중치 없음)BFSO(V + E)
연결 요소, 사이클 탐지DFSO(V + E)
동적 연결성 (unite + find)Union-FindO(α(n)) ≈ 상수
N단계 이내 탐색BFSO(V + E)
위상 정렬DFSO(V + E)
최소 연결 비용Kruskal / PrimO(E log E) / O(E log V)

2. 그래프 표현과 기본 구조

인접 리스트 vs 인접 행렬

flowchart LR
  subgraph adj_list[인접 리스트]
    L1[정점 0] --> L2[1, 2, 3]
    L3[정점 1] --> L4[0, 2]
    L5[정점 2] --> L6[0, 1]
  end
  subgraph adj_matrix[인접 행렬]
    M1["0 1 1 1"]
    M2["1 0 1 0"]
    M3["1 1 0 0"]
  end

인접 리스트: 희소 그래프(간선이 적음)에 유리. 공간 O(V + E), 이웃 탐색 O(degree). 인접 행렬: 밀집 그래프, 간선 존재 여부 O(1) 확인에 유리. 공간 O(V²).

C++ 그래프 기본 구조

#include <vector>
#include <utility>
// 가중치 있는 무방향 그래프 (인접 리스트)
// adj[v] = {(u, weight), ...}  → v에서 u로 가는 간선, 비용 weight
using Graph = std::vector<std::vector<std::pair<int, int>>>;
// 가중치 없는 그래프
using SimpleGraph = std::vector<std::vector<int>>;
// 그래프 생성 예: 정점 4개, 간선 (0-1, 2), (1-2, 3), (0-2, 5)
Graph buildGraph() {
    Graph g(4);
    g[0].emplace_back(1, 2);  // 0 → 1, 비용 2
    g[0].emplace_back(2, 5);  // 0 → 2, 비용 5
    g[1].emplace_back(0, 2);
    g[1].emplace_back(2, 3);
    g[2].emplace_back(0, 5);
    g[2].emplace_back(1, 3);
    return g;
}

3. BFS 너비 우선 탐색

핵심 아이디어

BFS는 를 사용해 같은 깊이(레벨)의 정점을 먼저 모두 방문한 뒤, 다음 레벨로 넘어갑니다. 가중치 없는 그래프에서 최단 경로(간선 개수)를 구할 때 사용합니다.

flowchart TD
  A[시작 0] --> B[1]
  A --> C[2]
  B --> D[3]
  B --> E[4]
  C --> E
  D --> F[5]
  E --> F

BFS 순서: 0 → 1, 2 → 3, 4 → 5 (레벨별)

완전한 BFS 구현

#include <vector>
#include <queue>
#include <algorithm>
// 가중치 없는 그래프에서 start부터 BFS
// 반환: 각 정점까지의 거리(간선 개수), 도달 불가 시 -1
std::vector<int> bfs(const SimpleGraph& g, int start) {
    const int n = static_cast<int>(g.size());
    std::vector<int> dist(n, -1);
    std::queue<int> q;
    dist[start] = 0;
    q.push(start);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : g[u]) {
            if (dist[v] == -1) {  // 미방문
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    return dist;
}
// 경로 복원이 필요한 경우: parent 배열 유지
struct BFSResult {
    std::vector<int> dist;
    std::vector<int> parent;  // parent[v] = u → u에서 v로 왔음
};
BFSResult bfsWithPath(const SimpleGraph& g, int start) {
    const int n = static_cast<int>(g.size());
    BFSResult res;
    res.dist.assign(n, -1);
    res.parent.assign(n, -1);
    std::queue<int> q;
    res.dist[start] = 0;
    q.push(start);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : g[u]) {
            if (res.dist[v] == -1) {
                res.dist[v] = res.dist[u] + 1;
                res.parent[v] = u;
                q.push(v);
            }
        }
    }
    return res;
}
// start → end 최단 경로 복원
std::vector<int> restorePath(const std::vector<int>& parent, int start, int end) {
    std::vector<int> path;
    for (int v = end; v != -1; v = parent[v]) {
        path.push_back(v);
    }
    std::reverse(path.begin(), path.end());
    if (!path.empty() && path[0] == start) return path;
    return {};  // 도달 불가
}

BFS 사용 예시

SimpleGraph g(6);
g[0] = {1, 2}; g[1] = {0, 3, 4}; g[2] = {0, 4};
g[3] = {1, 5}; g[4] = {1, 2, 5}; g[5] = {3, 4};
auto res = bfsWithPath(g, 0);
auto path = restorePath(res.parent, 0, 5);  // {0,1,3,5} 또는 {0,2,4,5}, 거리 3

4. DFS 깊이 우선 탐색

핵심 아이디어

DFS는 스택(또는 재귀)으로 한 방향으로 깊이 들어갔다가, 막히면 백트래킹합니다. 연결 요소 탐색, 사이클 감지, 위상 정렬에 사용합니다.

flowchart TD
  A[0] --> B[1]
  B --> C[2]
  C --> D[3]
  D --> E[4]
  E --> F[5]

DFS 순서 (재귀): 0 → 1 → 2 → 3 → 4 → 5 (한쪽으로 끝까지)

완전한 DFS 구현

#include <vector>
#include <functional>
// 재귀 DFS
void dfsRecursive(const SimpleGraph& g, int u, std::vector<bool>& visited,
                  std::vector<int>& order) {
    visited[u] = true;
    order.push_back(u);
    for (int v : g[u]) {
        if (!visited[v]) {
            dfsRecursive(g, v, visited, order);
        }
    }
}
// 스택 기반 DFS (재귀 스택 오버플로우 방지)
std::vector<int> dfsIterative(const SimpleGraph& g, int start) {
    const int n = static_cast<int>(g.size());
    std::vector<bool> visited(n, false);
    std::vector<int> order;
    std::vector<int> stack;
    stack.push_back(start);
    while (!stack.empty()) {
        int u = stack.back();
        stack.pop_back();
        if (visited[u]) continue;
        visited[u] = true;
        order.push_back(u);
        for (int v : g[u]) {
            if (!visited[v]) {
                stack.push_back(v);
            }
        }
    }
    return order;
}
// 연결 요소(Connected Component) 개수
int countComponents(const SimpleGraph& g) {
    const int n = static_cast<int>(g.size());
    std::vector<bool> visited(n, false);
    int count = 0;
    for (int i = 0; i < n; ++i) {
        if (!visited[i]) {
            ++count;
            std::vector<int> order;
            dfsRecursive(g, i, visited, order);
        }
    }
    return count;
}

DFS로 사이클 감지 (무방향 그래프)

// 무방향 그래프에서 사이클 존재 여부
bool hasCycleUndirected(const SimpleGraph& g, int u, int parent,
                        std::vector<bool>& visited) {
    visited[u] = true;
    for (int v : g[u]) {
        if (!visited[v]) {
            if (hasCycleUndirected(g, v, u, visited)) return true;
        } else if (v != parent) {
            return true;  // 이미 방문한 정점인데 부모가 아님 → 사이클
        }
    }
    return false;
}
bool hasCycle(const SimpleGraph& g) {
    const int n = static_cast<int>(g.size());
    std::vector<bool> visited(n, false);
    for (int i = 0; i < n; ++i) {
        if (!visited[i] && hasCycleUndirected(g, i, -1, visited)) {
            return true;
        }
    }
    return false;
}

위상 정렬 (DAG)

#include <algorithm>
enum class Color { White, Gray, Black };
bool topoSortDFS(const SimpleGraph& g, int u, std::vector<Color>& color,
                 std::vector<int>& order) {
    color[u] = Color::Gray;  // 방문 중
    for (int v : g[u]) {
        if (color[v] == Color::Gray) return false;  // 사이클
        if (color[v] == Color::White && !topoSortDFS(g, v, color, order)) {
            return false;
        }
    }
    color[u] = Color::Black;
    order.push_back(u);
    return true;
}
// 위상 정렬 결과 (역순으로 저장됨 → reverse 필요)
// 실패 시 빈 벡터 (사이클 존재)
std::vector<int> topologicalSort(const SimpleGraph& g) {
    const int n = static_cast<int>(g.size());
    std::vector<Color> color(n, Color::White);
    std::vector<int> order;
    for (int i = 0; i < n; ++i) {
        if (color[i] == Color::White && !topoSortDFS(g, i, color, order)) {
            return {};  // 사이클
        }
    }
    std::reverse(order.begin(), order.end());
    return order;
}

5. 다익스트라 최단 경로

핵심 아이디어

다익스트라는 가중치가 음이 아닌 그래프에서 단일 출발 최단 경로를 구합니다. 우선순위 큐로 “현재까지 가장 가까운 정점”을 반복적으로 확장합니다.

flowchart LR
  A[시작] -->|2| B
  A -->|5| C
  B -->|3| C
  B -->|1| D
  C -->|1| D

다익스트라: 시작점에서 각 정점까지의 최단 거리를 O(E log V)에 구합니다.

완전한 다익스트라 구현

#include <vector>
#include <queue>
#include <limits>
#include <algorithm>
constexpr int INF = std::numeric_limits<int>::max();
// 다익스트라: start에서 각 정점까지의 최단 거리
std::vector<int> dijkstra(const Graph& g, int start) {
    const int n = static_cast<int>(g.size());
    std::vector<int> dist(n, INF);
    std::priority_queue<std::pair<int, int>,
                       std::vector<std::pair<int, int>>,
                       std::greater<>> pq;  // (거리, 정점) 최소 힙
    dist[start] = 0;
    pq.emplace(0, start);
    while (!pq.empty()) {
        auto [d, u] = pq.top();
        pq.pop();
        if (d > dist[u]) continue;  // 이미 더 좋은 경로로 갱신됨
        for (const auto& [v, w] : g[u]) {
            int newDist = dist[u] + w;
            if (newDist < dist[v]) {
                dist[v] = newDist;
                pq.emplace(newDist, v);
            }
        }
    }
    return dist;
}
// 경로 복원 포함
struct DijkstraResult {
    std::vector<int> dist;
    std::vector<int> parent;
};
DijkstraResult dijkstraWithPath(const Graph& g, int start) {
    const int n = static_cast<int>(g.size());
    DijkstraResult res;
    res.dist.assign(n, INF);
    res.parent.assign(n, -1);
    std::priority_queue<std::pair<int, int>,
                       std::vector<std::pair<int, int>>,
                       std::greater<>> pq;
    res.dist[start] = 0;
    pq.emplace(0, start);
    while (!pq.empty()) {
        auto [d, u] = pq.top();
        pq.pop();
        if (d > res.dist[u]) continue;
        for (const auto& [v, w] : g[u]) {
            int newDist = res.dist[u] + w;
            if (newDist < res.dist[v]) {
                res.dist[v] = newDist;
                res.parent[v] = u;
                pq.emplace(newDist, v);
            }
        }
    }
    return res;
}

다익스트라 사용 예시

// 그래프: 0→1(2), 0→2(4), 1→2(1), 1→3(7), 2→3(3), 3→4(1)
// 0→4 최단: 0-1-2-3-4 = 7. restorePath(parent, 0, 4) → {0,1,2,3,4}
Graph g(5);
g[0].emplace_back(1, 2); g[0].emplace_back(2, 4);
g[1].emplace_back(2, 1); g[1].emplace_back(3, 7);
g[2].emplace_back(3, 3); g[3].emplace_back(4, 1);
auto res = dijkstraWithPath(g, 0);  // res.dist[4] == 7

6. 최소신장트리 Kruskal·Prim

Kruskal 알고리즘

아이디어: 간선을 비용 오름차순으로 정렬한 뒤, 사이클을 만들지 않으면 추가. Union-Find로 사이클 감지.

#include <algorithm>
#include <numeric>
struct Edge {
    int u, v, weight;
    bool operator<(const Edge& e) const { return weight < e.weight; }
};
// Union-Find
class UnionFind {
    std::vector<int> parent, rank;
public:
    explicit UnionFind(int n) : parent(n), rank(n, 0) {
        std::iota(parent.begin(), parent.end(), 0);
    }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    bool unite(int x, int y) {
        int px = find(x), py = find(y);
        if (px == py) return false;
        if (rank[px] < rank[py]) std::swap(px, py);
        parent[py] = px;
        if (rank[px] == rank[py]) ++rank[px];
        return true;
    }
};
// Kruskal: MST 간선 목록과 총 비용
std::pair<std::vector<Edge>, int> kruskal(int n, std::vector<Edge>& edges) {
    std::sort(edges.begin(), edges.end());
    UnionFind uf(n);
    std::vector<Edge> mst;
    int totalCost = 0;
    for (const auto& e : edges) {
        if (uf.unite(e.u, e.v)) {
            mst.push_back(e);
            totalCost += e.weight;
        }
    }
    return {mst, totalCost};
}

Prim 알고리즘

아이디어: 한 정점에서 시작해, “MST에 포함된 정점 집합”에 가장 가까운 간선을 반복적으로 추가.

// Prim: 인접 리스트 그래프에서 MST 비용
int prim(const Graph& g, int start = 0) {
    const int n = static_cast<int>(g.size());
    std::vector<bool> inMST(n, false);
    std::vector<int> minEdge(n, INF);
    std::priority_queue<std::pair<int, int>,
                       std::vector<std::pair<int, int>>,
                       std::greater<>> pq;
    minEdge[start] = 0;
    pq.emplace(0, start);
    int totalCost = 0;
    while (!pq.empty()) {
        auto [w, u] = pq.top();
        pq.pop();
        if (inMST[u]) continue;
        inMST[u] = true;
        totalCost += w;
        for (const auto& [v, weight] : g[u]) {
            if (!inMST[v] && weight < minEdge[v]) {
                minEdge[v] = weight;
                pq.emplace(weight, v);
            }
        }
    }
    return totalCost;
}

MST 사용 예시

int n = 4;
std::vector<Edge> edges = {{0,1,2},{0,2,5},{1,2,3},{1,3,4},{2,3,1}};
auto [mst, cost] = kruskal(n, edges);  // cost = 6 (0-1, 1-2, 2-3)

7. Union-Find (분리 집합)

핵심 아이디어

Union-Find는 동적 연결성을 효율적으로 다루는 자료구조입니다. find(x): x가 속한 집합의 대표 원소 반환, unite(x, y): x와 y가 속한 집합을 합침. 경로 압축랭크 기반 합치기로 거의 상수 시간에 동작합니다.

flowchart LR
  subgraph before[unite 전]
    A1[0,1,2] 
    B1[3,4]
  end
  subgraph after["unite(1,3) 후"]
    C1[0,1,2,3,4]
  end

완전한 Union-Find 구현

#include <vector>
#include <numeric>
// 경로 압축 + 랭크 기반 합치기. find, unite: O(α(n)) ≈ 상수
class UnionFind {
    std::vector<int> parent, rank;
public:
    explicit UnionFind(int n) : parent(n), rank(n, 0) {
        std::iota(parent.begin(), parent.end(), 0);
    }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    bool unite(int x, int y) {
        int px = find(x), py = find(y);
        if (px == py) return false;
        if (rank[px] < rank[py]) std::swap(px, py);
        parent[py] = px;
        if (rank[px] == rank[py]) ++rank[px];
        return true;
    }
    bool connected(int x, int y) { return find(x) == find(y); }
};

Union-Find 사용 예시: 연결 요소 개수

// 간선 목록이 주어졌을 때 연결 요소 개수
int countComponents(int n, const std::vector<std::pair<int,int>>& edges) {
    UnionFind uf(n);
    for (const auto& [u, v] : edges) {
        uf.unite(u, v);
    }
    int count = 0;
    for (int i = 0; i < n; ++i) {
        if (uf.find(i) == i) ++count;  // 각 집합의 루트만 카운트
    }
    return count;
}
// 사용 예: 정점 5개, 간선 (0,1), (1,2), (3,4) → 연결 요소 2개
int main() {
    std::vector<std::pair<int,int>> edges = {{0,1}, {1,2}, {3,4}};
    int n = 5;
    int comp = countComponents(n, edges);  // 2
    return 0;
}

Union-Find 활용: 사이클 감지 (무방향 그래프)

// 간선을 순서대로 추가할 때, 새 간선이 사이클을 만드는지 O(α(n))에 판단
bool hasCycleWithUnionFind(int n, const std::vector<std::pair<int,int>>& edges) {
    UnionFind uf(n);
    for (const auto& [u, v] : edges) {
        if (!uf.unite(u, v)) return true;  // 이미 같은 집합 → 사이클
    }
    return false;
}

8. 자주 발생하는 에러와 해결법

문제 1: 다익스트라에서 음수 간선 사용

증상: 최단 경로가 잘못된 값으로 나옵니다. 원인: 다익스트라는 음수 간선을 지원하지 않습니다. 음수 간선이 있으면 벨만-포드 사용.

// ❌ 잘못된 사용: 음수 가중치
g[0].emplace_back(1, -5);  // 다익스트라 결과 오류
// ✅ 해결: 벨만-포드 또는 음수 간선 제거/변환

문제 2: BFS/DFS에서 무방향 그래프 중복 간선

증상: 같은 정점이 여러 번 큐/스택에 들어가 성능 저하. 원인: 무방향 그래프에서 (u,v)와 (v,u) 둘 다 있어서, 방문 체크 전에 중복 push.

// ❌ 잘못된 패턴
for (int v : g[u]) {
    q.push(v);  // visited 체크 없이 push
}
// ✅ 올바른 패턴
for (int v : g[u]) {
    if (dist[v] == -1) {  // 또는 !visited[v]
        dist[v] = dist[u] + 1;
        q.push(v);
    }
}

문제 3: 우선순위 큐에서 “이미 갱신된” 원소 처리

증상: 다익스트라가 느리거나 메모리를 많이 씁니다. 원인: 같은 정점에 대해 (d1,v), (d2,v), (d3,v) 등 여러 항목이 큐에 쌓입니다. d > dist[v] 체크 필수.

// ✅ 필수 체크
while (!pq.empty()) {
    auto [d, u] = pq.top();
    pq.pop();
    if (d > dist[u]) continue;  // 이 줄 없으면 성능/정확도 문제
    // ...
}

문제 4: Kruskal에서 Union-Find 경로 압축 누락

증상: 대규모 그래프에서 Kruskal이 매우 느립니다. 원인: find에서 경로 압축을 하지 않으면 트리가 한쪽으로 치우쳐 O(n)에 수렴.

// ❌ 느린 find
int find(int x) {
    while (parent[x] != x) x = parent[x];
    return x;
}
// ✅ 경로 압축
int find(int x) {
    if (parent[x] != x) parent[x] = find(parent[x]);
    return parent[x];
}

문제 5: 위상 정렬에서 사이클 미감지

증상: DAG가 아닌 그래프에 위상 정렬을 적용하면 잘못된 결과 또는 무한 루프. 원인: 사이클이 있으면 “진입 차수 0”인 정점이 영원히 생기지 않을 수 있음.

// ✅ DFS 기반 위상 정렬: Gray(방문 중) 재방문 시 사이클
if (color[v] == Color::Gray) return false;  // 사이클 감지

문제 6: DFS 재귀 스택 오버플로우

증상: 정점 10만 개 이상에서 스택 오버플로우. 원인: 재귀 깊이가 V까지 갈 수 있음.

// ❌ 대규모 그래프에서 위험
void dfsRecursive(..., int u, ...) {
    for (int v : g[u]) if (!visited[v]) dfsRecursive(g, v, ...);  // 깊이 V
}
// ✅ 스택 기반 반복 DFS 사용 (본문 4절 참고)

문제 7: 정점 인덱스 범위 오류

증상: 런타임 크래시. 원인: 1-based 입력을 0-based로 변환하지 않음.

// ❌ g[u] when u=1..n → g[n] 범위 초과
// ✅ int u0 = u - 1, v0 = v - 1; g[u0].push_back(v0);

문제 8: INF 및 거리 합 오버플로우

증상: 다익스트라 결과가 음수. 원인: int 거리 합 오버플로우.

// ❌ int newDist = dist[u] + w;
// ✅ using ll = long long; ll newDist = dist[u] + w;

9. 베스트 프랙티스

1. 그래프 타입에 맞는 표현 선택

그래프 특성추천이유
희소, 가중치 있음vector<vector<pair<int,int>>>공간·순회 효율
밀집, 가중치 없음행렬 또는 vector<vector<int>>간선 존재 O(1)
MST (간선 리스트)vector<Edge>정렬·스캔 유리

2. const 참조로 그래프 전달

// ✅ 큰 그래프를 복사하지 않음
std::vector<int> bfs(const SimpleGraph& g, int start);
// ❌ 값으로 전달 시 복사 발생
std::vector<int> bfs(SimpleGraph g, int start);

3. 정점 수를 명시적으로 사용

// ✅ const int n = static_cast<int>(g.size()); 로 일관 사용
// ❌ g.size() 반복 호출, size_t와 int 혼용

4. 알고리즘별 전제 조건 문서화

다익스트라: 가중치 음수 불가, 단일 출발
BFS: 가중치 없음 또는 동일 가중치
위상 정렬: DAG 가정, 사이클 시 빈 결과
Kruskal: 무방향, 연결 그래프 가정

10. 성능 최적화 팁

1. 인접 리스트 vs 인접 행렬

그래프 밀도추천이유
희소 (E ≈ V)인접 리스트공간·탐색 모두 유리
밀집 (E ≈ V²)인접 행렬간선 존재 O(1) 확인

2. 다익스트라·BFS/DFS·Kruskal 요약

// 다익스트라: std::priority_queue + (d > dist[u]) 스킵
// BFS/DFS: order.reserve(g.size()) 로 재할당 감소
// Kruskal: std::sort(edges) 후 Union-Find로 사이클 감지

3. 벤치마크 참고 (V=10,000, E=50,000)

알고리즘시간 (대략)
BFS~1ms
DFS~1ms
다익스트라~5ms
Kruskal~3ms
Prim~4ms

11. 프로덕션 패턴

1. 그래프 빌더 패턴

class GraphBuilder {
    Graph g;
public:
    explicit GraphBuilder(int n) : g(n) {}
    GraphBuilder& addEdge(int u, int v, int w = 1) {
        g[u].emplace_back(v, w); return *this;
    }
    Graph build() { return std::move(g); }
};
// 사용: GraphBuilder(10).addEdge(0,1,2).addEdge(1,2,3).build();

2. 알고리즘 결과 캐싱

template<typename Key>
class CachedDijkstra {
    std::unordered_map<Key, std::vector<int>> cache;
    Graph g;
public:
    const std::vector<int>& shortestPaths(int start) {
        if (auto it = cache.find(start); it != cache.end()) return it->second;
        cache[start] = dijkstra(g, start);
        return cache[start];
    }
};

3. 에러 처리 및 검증

std::optional<std::vector<int>> dijkstraSafe(const Graph& g, int start) {
    if (start < 0 || start >= static_cast<int>(g.size())) return std::nullopt;
    return dijkstra(g, start);
}

4. 로깅 및 디버깅

#ifdef DEBUG_GRAPH
#define LOG_VISIT(u, v, d) std::cerr << "Visit " << u << " -> " << v << " dist=" << d << "\n"
#else
#define LOG_VISIT(u, v, d) ((void)0)
#endif

5. 단위 테스트

namespace graph { std::vector<int> bfsImpl(const SimpleGraph& g, int start); }

12. 실전 통합 예제

문제: “도시 연결 비용 최소화 + 최단 경로”

설명: N개 도시와 M개 도로가 있습니다. (1) MST로 최소 연결 비용, (2) 도시 0에서 각 도시까지 최단 거리를 구합니다. Kruskal다익스트라를 한 그래프에 적용하는 예제입니다.

#include <vector>
#include <queue>
#include <algorithm>
#include <limits>
#include <numeric>
using Graph = std::vector<std::vector<std::pair<int,int>>>;
constexpr int INF = std::numeric_limits<int>::max();
struct Edge { int u, v, w; bool operator<(const Edge& e) const { return w < e.w; } };
// Union-Find (Kruskal용)
class UnionFind {
    std::vector<int> parent, rank;
public:
    explicit UnionFind(int n) : parent(n), rank(n, 0) {
        std::iota(parent.begin(), parent.end(), 0);
    }
    int find(int x) { return parent[x] != x ? parent[x] = find(parent[x]) : x; }
    bool unite(int x, int y) {
        int px = find(x), py = find(y);
        if (px == py) return false;
        if (rank[px] < rank[py]) std::swap(px, py);
        parent[py] = px;
        if (rank[px] == rank[py]) ++rank[px];
        return true;
    }
};
int mstCost(int n, std::vector<Edge>& edges) {
    std::sort(edges.begin(), edges.end());
    UnionFind uf(n);
    int cost = 0;
    for (const auto& e : edges)
        if (uf.unite(e.u, e.v)) cost += e.w;
    return cost;
}
std::vector<int> dijkstra(const Graph& g, int start) {
    int n = static_cast<int>(g.size());
    std::vector<int> dist(n, INF);
    std::priority_queue<std::pair<int,int>, std::vector<std::pair<int,int>>, std::greater<>> pq;
    dist[start] = 0; pq.emplace(0, start);
    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d > dist[u]) continue;
        for (const auto& [v, w] : g[u]) {
            int nd = dist[u] + w;
            if (nd < dist[v]) { dist[v] = nd; pq.emplace(nd, v); }
        }
    }
    return dist;
}
int main() {
    int n = 4;
    Graph g(n);
    std::vector<Edge> edges = {{0,1,2},{0,2,5},{1,2,3},{1,3,4},{2,3,1}};
    for (const auto& e : edges) {
        g[e.u].emplace_back(e.v, e.w);
        g[e.v].emplace_back(e.u, e.w);
    }
    int mst = mstCost(n, edges);   // 6
    auto dist = dijkstra(g, 0);    // {0,2,5,6}
    return 0;
}

핵심: Union-Find(Kruskal) + 우선순위 큐(다익스트라).

13. 구현 체크리스트

  • 그래프 방향/무방향, 가중치·음수 간선 여부 확인
  • BFS/DFS: 방문 체크, 정점 범위 검증, 비연결 그래프 고려
  • 다익스트라: d > dist[u] 스킵, INF 오버플로우(long long), 음수 간선 없음
  • Kruskal: Union-Find 경로 압축 + 랭크, 간선 정렬
  • 프로덕션: 입력 검증, 캐싱, 로깅

정리

항목설명
BFS가중치 없음, 최단 거리(간선 수), 레벨별 탐색
DFS연결 요소, 사이클, 위상 정렬
다익스트라가중치 있음(음수 X), 단일 출발 최단 경로
KruskalMST, Union-Find, 간선 정렬
PrimMST, 우선순위 큐
Union-Find동적 연결성, Kruskal의 사이클 감지
핵심 원칙:
  1. 문제 유형에 맞는 알고리즘 선택
  2. 방문 체크·스킵 로직으로 중복 방지
  3. Union-Find 경로 압축 + 랭크 필수
  4. 대규모 그래프에서는 재귀 대신 반복 DFS
  5. 프로덕션에서는 입력 검증·캐싱 고려

자주 묻는 질문 (FAQ)

  • 실무 활용: 네트워크 라우팅, 지도 앱 최단 경로, 소셜 네트워크 분석, 빌드 의존성, 게임 AI 경로 탐색
  • 선행 학습: STL 컨테이너, 재귀·스택, Union-Find
  • 심화: LeetCode·백준 그래프 유형, 《알고리즘 설계》 한 줄 요약: BFS·DFS·다익스트라·MST·Union-Find를 상황에 맞게 선택하고, 흔한 실수를 피해 실무에 적용하세요.

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ 그래프 알고리즘 완벽 가이드 | BFS·DFS·다익스트라·최소신장트리 [실전]」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ 그래프 알고리즘 완벽 가이드 | BFS·DFS·다익스트라·최소신장트리 [실전]」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, 그래프, 알고리즘, 다익스트라, BFS, DFS, 최소신장트리 등으로 검색하시면 이 글이 도움이 됩니다.