Post

8. DFS (깊이 우선 탐색)

8. DFS (깊이 우선 탐색)

개념

그래프에서 한 경로를 끝까지 탐색한 후 되돌아와 다른 경로를 탐색하는 알고리즘이다.

핵심 특징

  • 깊이 우선 탐색: 더 이상 갈 곳이 없을 때까지 깊이 들어감
  • 스택(Stack) 또는 재귀 사용: LIFO 특성
  • 백트래킹의 기반: 모든 경로/조합 탐색에 활용

동작 원리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
그래프:
    1 --- 2 --- 5
    |     |
    3 --- 4

시작: 1

스택: [1]           방문: {1}
→ 1 방문, 인접 2 선택 (또는 3)

스택: [1, 2]        방문: {1, 2}
→ 2 방문, 인접 5 선택 (또는 4)

스택: [1, 2, 5]     방문: {1, 2, 5}
→ 5 방문, 더 이상 갈 곳 없음, 백트래킹

스택: [1, 2]
→ 2에서 미방문 4 선택

스택: [1, 2, 4]     방문: {1, 2, 5, 4}
→ 4 방문, 인접 중 미방문 3

스택: [1, 2, 4, 3]  방문: {1, 2, 5, 4, 3}
→ 3 방문, 완료

방문 순서: 1 → 2 → 5 → 4 → 3

DFS의 특징

특성설명
완전 탐색모든 정점 방문 가능
메모리 효율경로 길이만큼만 저장 (O(깊이))
최단 경로보장 안 됨
사이클 탐지가능 (방문 중인 정점 재방문)

핵심 연산 & 시간복잡도

연산인접 리스트인접 행렬
전체 탐색O(V + E)O(V²)
정점 방문O(1)O(1)
인접 정점 확인O(degree)O(V)
  • V: 정점 수, E: 간선 수
  • 공간복잡도: O(V) (방문 배열 + 스택/재귀)

구현 (No-STL)

재귀 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const int MAX_V = 100001;

int adj[MAX_V][100];
int adjSize[MAX_V];
bool visited[MAX_V];

void dfs(int cur) {
    visited[cur] = true;

    // cur 처리
    // ...

    for (int i = 0; i < adjSize[cur]; i++) {
        int next = adj[cur][i];
        if (!visited[next]) {
            dfs(next);
        }
    }
}

스택 버전 (명시적 스택)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const int MAX_V = 100001;

int adj[MAX_V][100];
int adjSize[MAX_V];
bool visited[MAX_V];

int stack[MAX_V];
int top = -1;

void push(int x) { stack[++top] = x; }
int pop() { return stack[top--]; }
bool isEmpty() { return top == -1; }

void dfsStack(int start) {
    push(start);

    while (!isEmpty()) {
        int cur = pop();

        if (visited[cur]) continue;
        visited[cur] = true;

        // cur 처리
        // ...

        // 역순으로 넣어야 작은 번호부터 방문
        for (int i = adjSize[cur] - 1; i >= 0; i--) {
            int next = adj[cur][i];
            if (!visited[next]) {
                push(next);
            }
        }
    }
}

STL

재귀 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <vector>

std::vector<int> adj[MAX_V];
bool visited[MAX_V];

void dfs(int cur) {
    visited[cur] = true;

    for (int next : adj[cur]) {
        if (!visited[next]) {
            dfs(next);
        }
    }
}

스택 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stack>
#include <vector>

std::vector<int> adj[MAX_V];
bool visited[MAX_V];

void dfsStack(int start) {
    std::stack<int> st;
    st.push(start);

    while (!st.empty()) {
        int cur = st.top();
        st.pop();

        if (visited[cur]) continue;
        visited[cur] = true;

        for (int next : adj[cur]) {
            if (!visited[next]) {
                st.push(next);
            }
        }
    }
}

경로 추적

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <vector>

std::vector<int> adj[MAX_V];
bool visited[MAX_V];
std::vector<int> path;

void dfsWithPath(int cur, int target) {
    visited[cur] = true;
    path.push_back(cur);

    if (cur == target) {
        // 경로 찾음: path에 저장됨
        return;
    }

    for (int next : adj[cur]) {
        if (!visited[next]) {
            dfsWithPath(next, target);
        }
    }

    path.pop_back();  // 백트래킹
}

2차원 그리드 DFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int N, M;
int grid[1001][1001];
bool visited[1001][1001];

int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};

void dfsGrid(int x, int y) {
    visited[x][y] = true;

    for (int d = 0; d < 4; d++) {
        int nx = x + dx[d];
        int ny = y + dy[d];

        if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
        if (grid[nx][ny] == 0) continue;
        if (visited[nx][ny]) continue;

        dfsGrid(nx, ny);
    }
}

사용 예시

연결 요소 개수 & 크기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <bits/stdc++.h>
using namespace std;

int N, M;
vector<int> adj[1001];
bool visited[1001];

int dfs(int cur) {
    visited[cur] = true;
    int size = 1;

    for (int next : adj[cur]) {
        if (!visited[next]) {
            size += dfs(next);
        }
    }

    return size;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> N >> M;

    for (int i = 0; i < M; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    int components = 0;
    vector<int> sizes;

    for (int i = 1; i <= N; i++) {
        if (!visited[i]) {
            sizes.push_back(dfs(i));
            components++;
        }
    }

    cout << "연결 요소 개수: " << components << '\n';
    return 0;
}

사이클 탐지 (무향 그래프)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[1001];
bool visited[1001];

// parent: 직전에 방문한 정점 (무향 그래프에서 역방향 간선 구분)
bool hasCycleDFS(int cur, int parent) {
    visited[cur] = true;

    for (int next : adj[cur]) {
        if (!visited[next]) {
            if (hasCycleDFS(next, cur)) {
                return true;
            }
        } else if (next != parent) {
            // 이미 방문한 정점인데 부모가 아님 → 사이클
            return true;
        }
    }

    return false;
}

bool hasCycle(int n) {
    for (int i = 1; i <= n; i++) {
        if (!visited[i]) {
            if (hasCycleDFS(i, -1)) {
                return true;
            }
        }
    }
    return false;
}

사이클 탐지 (유향 그래프)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[1001];
int state[1001];  // 0: 미방문, 1: 방문 중, 2: 방문 완료

bool hasCycleDFS(int cur) {
    state[cur] = 1;  // 방문 중

    for (int next : adj[cur]) {
        if (state[next] == 1) {
            // 방문 중인 정점 재방문 → 사이클
            return true;
        }
        if (state[next] == 0) {
            if (hasCycleDFS(next)) {
                return true;
            }
        }
    }

    state[cur] = 2;  // 방문 완료
    return false;
}

경로 존재 여부

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[1001];
bool visited[1001];

bool hasPath(int cur, int target) {
    if (cur == target) return true;

    visited[cur] = true;

    for (int next : adj[cur]) {
        if (!visited[next]) {
            if (hasPath(next, target)) {
                return true;
            }
        }
    }

    return false;
}

모든 경로 찾기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <bits/stdc++.h>
using namespace std;

vector<int> adj[1001];
bool visited[1001];
vector<int> path;
vector<vector<int>> allPaths;

void findAllPaths(int cur, int target) {
    visited[cur] = true;
    path.push_back(cur);

    if (cur == target) {
        allPaths.push_back(path);
    } else {
        for (int next : adj[cur]) {
            if (!visited[next]) {
                findAllPaths(next, target);
            }
        }
    }

    path.pop_back();
    visited[cur] = false;  // 백트래킹: 다른 경로에서 재방문 가능
}

섬의 개수 (2D 그리드)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <bits/stdc++.h>
using namespace std;

int N, M;
int grid[1001][1001];
bool visited[1001][1001];

int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};

void dfs(int x, int y) {
    visited[x][y] = true;

    for (int d = 0; d < 4; d++) {
        int nx = x + dx[d];
        int ny = y + dy[d];

        if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
        if (grid[nx][ny] == 0) continue;
        if (visited[nx][ny]) continue;

        dfs(nx, ny);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> N >> M;

    for (int i = 0; i < N; i++) {
        for (int j = 0; j < M; j++) {
            cin >> grid[i][j];
        }
    }

    int islands = 0;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < M; j++) {
            if (grid[i][j] == 1 && !visited[i][j]) {
                dfs(i, j);
                islands++;
            }
        }
    }

    cout << islands << '\n';
    return 0;
}

트리의 지름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <bits/stdc++.h>
using namespace std;

int N;
vector<pair<int, int>> adj[100001];  // {정점, 가중치}
bool visited[100001];
int farthestNode, maxDist;

void dfs(int cur, int dist) {
    visited[cur] = true;

    if (dist > maxDist) {
        maxDist = dist;
        farthestNode = cur;
    }

    for (auto [next, weight] : adj[cur]) {
        if (!visited[next]) {
            dfs(next, dist + weight);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> N;

    for (int i = 0; i < N - 1; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }

    // 1. 임의의 정점에서 가장 먼 정점 찾기
    maxDist = 0;
    dfs(1, 0);

    // 2. 찾은 정점에서 가장 먼 정점까지의 거리 = 지름
    memset(visited, false, sizeof(visited));
    maxDist = 0;
    dfs(farthestNode, 0);

    cout << maxDist << '\n';
    return 0;
}

주의사항 / Edge Cases

스택 오버플로우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 재귀 깊이가 깊으면 스택 오버플로우 발생
// 해결 1: 스택 크기 늘리기 (컴파일 옵션)
// 해결 2: 명시적 스택 사용

// Linux에서 스택 크기 늘리기
// ulimit -s unlimited

// 코드에서 설정 (POSIX)
#include <sys/resource.h>
void setStackSize() {
    rlimit rl;
    getrlimit(RLIMIT_STACK, &rl);
    rl.rlim_cur = 256 * 1024 * 1024;  // 256MB
    setrlimit(RLIMIT_STACK, &rl);
}

재귀 vs 스택 방문 순서 차이

1
2
3
4
5
6
7
// 재귀: 인접 리스트 순서대로 방문
// 스택: 나중에 넣은 것 먼저 방문 (역순)

// 같은 순서로 방문하려면 스택에 역순으로 삽입
for (int i = adj[cur].size() - 1; i >= 0; i--) {
    st.push(adj[cur][i]);
}

백트래킹 시 방문 해제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 모든 경로 탐색 시
void dfs(int cur) {
    visited[cur] = true;
    path.push_back(cur);

    // ... 탐색 ...

    path.pop_back();
    visited[cur] = false;  // 다른 경로에서 재방문 허용
}

// 단순 도달 가능성 시
void dfs(int cur) {
    visited[cur] = true;
    // visited를 해제하지 않음
}

무향 그래프 사이클 판별

1
2
3
4
5
6
7
8
// 직전 정점(parent)을 기억해야 함
// 그렇지 않으면 양방향 간선을 사이클로 오인

// 잘못된 코드
if (visited[next]) return true;  // 모든 간선을 사이클로 판단

// 올바른 코드
if (visited[next] && next != parent) return true;

그래프가 비연결인 경우

1
2
3
4
5
6
// 모든 정점에서 DFS 시작해야 함
for (int i = 1; i <= N; i++) {
    if (!visited[i]) {
        dfs(i);
    }
}

면접 포인트

자주 나오는 질문

Q1. DFS의 시간복잡도가 O(V + E)인 이유는?

  • 모든 정점을 한 번씩 방문: O(V)
  • 각 간선을 한 번씩 확인: O(E)
  • 합: O(V + E)

Q2. 재귀 DFS vs 스택 DFS의 차이점?

  • 재귀: 코드 간결, 스택 오버플로우 위험
  • 스택: 코드 복잡, 깊이 제한 없음
  • 방문 순서가 다를 수 있음 (스택은 역순 삽입 필요)

Q3. DFS로 최단 경로를 구할 수 없는 이유는?

  • DFS는 깊이 우선으로 탐색하므로 먼저 찾은 경로가 최단이 아닐 수 있음
  • 모든 경로를 탐색하고 비교하면 가능하지만 비효율적
  • 최단 경로는 BFS (무가중치) 또는 다익스트라 (가중치) 사용

Q4. 무향 그래프와 유향 그래프에서 사이클 탐지 차이는?

  • 무향: parent 체크 필요 (양방향 간선 구분)
  • 유향: 방문 상태를 3단계로 (미방문/방문중/완료)
    • 방문 중인 정점을 다시 만나면 사이클

Q5. DFS를 사용하는 대표적인 알고리즘은?

  • 위상 정렬 (역순 완료 순서)
  • SCC (강한 연결 요소)
  • 단절점/단절선
  • 백트래킹 (순열, 조합, N-Queen 등)

코드 체크리스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 방문 배열 초기화
memset(visited, false, sizeof(visited));

// 2. 재귀 DFS
void dfs(int cur) {
    visited[cur] = true;

    for (int next : adj[cur]) {
        if (!visited[next]) {
            dfs(next);
        }
    }
}

// 3. 비연결 그래프 처리
for (int i = 1; i <= N; i++) {
    if (!visited[i]) {
        dfs(i);
    }
}

// 4. 백트래킹 (모든 경로)
void dfs(int cur) {
    visited[cur] = true;
    // ... 처리 ...
    visited[cur] = false;  // 복원
}

추천 문제

난이도문제링크핵심
SilverDFS와 BFSBOJ 1260기본
Silver바이러스BOJ 2606연결 요소
Silver연결 요소의 개수BOJ 11724연결 요소
Silver유기농 배추BOJ 10122D DFS
Silver단지번호붙이기BOJ 26672D + 크기
Gold트리의 지름BOJ 1167트리 DFS
Gold사이클 게임BOJ 20040사이클 탐지
Gold텀 프로젝트BOJ 9466유향 사이클
This post is licensed under CC BY 4.0 by the author.