학습 목표

  • DFS 알고리즘이 무엇인지 이해한다.

  • DFS 알고리즘의 동작 과정을 이해한다.

  • DFS 알고리즘을 소스코드로 구현한다.

 

깊이 우선 탐색(DFS, Depth-First Search) 알고리즘이란?

 그래프 탐색 알고리즘입니다. 그래프 탐색 알고리즘은 그래프 내 모든 정점을 방문하는 방법입니다. DFS는 이름에서 알 수 있듯이 깊이를 우선시하는 탐색 알고리즘입니다. 다시 말해, 한 정점에서 연결된 간선을 살펴보면서 방문 가능한 정점이 있다면, 즉시 방문하는 방식으로 동작하는 알고리즘입니다. 

 

 위에서 언급했듯이 방문 가능한 정점이 있으면 바로 방문합니다. 이 동작 방식은 재귀 호출을 사용하여 구현하면 간단하게 구현됩니다. 정점을 방문하는 과정을 함수 호출, 더 이상 방문할 정점이 없을 경우 이전 정점으로 돌아오는 과정을 함수 리턴으로 생각해 구현합니다.

 

탐색 과정

알고리즘의 동작 과정은 다음 세 단계로 이루어집니다.

 

1. 탐색 시작 정점을 방문합니다.

2. 현재 정점(또는 시작 정점)에서 연결된 간선을 순차적으로 탐색합니다.

  • 탐색 중인 정점이 방문한 정점이라면, 다음 정점을 확인합니다.

  • 탐색 중인 정점이 방문하지 않은 정점이라면 이 정점을 방문하여 2번 과정을 진행합니다.(재귀 호출)

3. 더 이상 방문할 정점이 없다면 탐색을 끝냅니다.(함수 리턴)

 

 

 

 

소스 코드(인접 행렬)

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
#include <stdio.h>
#define TRUE 1
#define FALSE 0
bool visited[6];
int g[6][6]; //양방향 그래프
void addEdge(int u, int v) {
    g[u][v] = 1;
    g[v][u] = 1;
}
void dfs(int nowVertex) {
    printf("정점 %d 를 방문했습니다.\n", nowVertex);
    //방문 표시
    visited[nowVertex] = TRUE;
 
    //방문하지 않은 정점 조사
    for (int i = 0; i < 6; i++) {
        //연결되어 있고, 방문하지 않았다면
        if (g[nowVertex][i] == 1 && !visited[i]) {
            dfs(i);
        }
    }
}
int main() {
    addEdge(01);
    addEdge(02);
    addEdge(12);
    addEdge(13);
    addEdge(14);
    addEdge(25);
    for (int i = 0; i < 6; i++) {
        if (!visited[i]) {
            dfs(i);
        }
    }
    return 0;
}
 

시간 복잡도

 시간 복잡도 분석을 위해 두 가지 그래프 형태를 고려하겠습니다. 우선, 주어진 그래프가 인접 리스트일 경우에는 모든 정점을 확인하는 복잡도인 |V|, 각 정점에서 연결된 간선들을 확인하는 과정에서 |E| 만큼 소요됩니다. 따라서 인접 리스트 형태의 그래프를 DFS 방식으로 탐색할 경우 O(|V|+|E|) 만큼의 시간 복잡도를 보입니다.

 

 그래프가 인접 행렬 형태로 주어진다면 각 정점에서 연결된 간선들을 확인하는데 |V| 만큼 시간 복잡도가 필요합니다. 따라서 인접 행렬 형태의 그래프를 DFS 방식으로 탐색할 경우 O(|V|^2) 만큼의 시간 복잡도를 보입니다.

 

정리

  • 그래프 상의 모든 정점을 깊이 우선 방식으로 탐색하는 알고리즘이다.

  • 방문 가능한 정점이 있다면 즉시 방문하는 방식으로 동작한다.

  • 동작 원리를 고려해 재귀 함수로 구현하면 간단하다.

  • 시간 복잡도는 인접 리스트로 구현 시 O(|V|+|E|), 인접 행렬로 구현 시 O(|V|^2)이다.

 

안녕하세요!


그래프 알고리즘을 배우기 전에 반드시 알아야 할 부분이 있습니다!


바로 그래프의 표현 방법입니다.  그래프 관련 문제를 해결하기 위해서는 주어진 그래프를 표현해야 하고,


그 데이터를 바탕으로 그래프 알고리즘을 적용시켜 해결해야 합니다. 


그럼 이제 본격적인 설명에 앞서, 그래프를 표현하는 방법에는 어떤 것들이 있는지 간단한게 알아보고 가도록 하겠습니다.


그래프를 표현하는 방법은 세 가지가 있습니다.


  • 인접 행렬(Adjacency matrix) : 두 노드간의 엣지 상태를 행렬 형태로 표현
  • 인접 리스트(Adjacency list) : 각 노드 별로 연결되어있는 노드들만 표현
  • 간선 리스트(Edge list) : 위의 방법들과 달리 엣지만을 리스트 형태 혹은 배열 형태로 저장하는 방법입니다.

[그래프1 - 방향이 없는 그래프]

[그래프2 - 방향이 있는 그래프]



이제부터 위 그래프를 각 방법으로 표현해 보겠습니다.


인접 행렬(Adjacency matrix)


[그래프1 - 인접 행렬]

[그래프2 - 인접 행렬]



인접 행렬은 노드와 노드 사이의 관계를 행렬 형태로 표현하는 방법입니다.


위 행렬에서 표현된 값이 0이면 노드 자신을 의미, INF면 직접적으로 연결 X, 나머지 값들은 연결된 엣지의 가중치입니다.


그래프 형태에 따라서 간선의 방향이 없으면 대칭행렬 형태로, 방향이 있다면 일반 행렬처럼 표현하면 되는 방식입니다.


이 특성때문에 인접 행렬 방식은 다음과 같은 특징을 같습니다.


1) 두 노드간의 엣지에 직접 접근할 수 있습니다. 예) g[4][2] : 4번노드와 2번노드의 엣지


2) 모든 노드쌍의 관계를 표현해야 하기 때문에 공간복잡도가 O(V^2) 소요됩니다.


따라서 인접 행렬은 밀집 그래프(엣지수가 많은) 형태에서 좋은 성능을 발휘하고 적절한 공간을 소요한다고 여겨지지만,


희소그래프(엣지수가 적은) 형태에서는 공간낭비가 매우 심합니다.(어떤 문제에서는 이 특성때문에 메모리 초과가 발생하기도 합니다)



인접 리스트(Adjacency list)


 

 [그래프1 - 인접 행렬]

 

[그래프2 - 인접 행렬]


이 방법은 인접 행렬에서 INF와 같은 불필요한 데이터를 제거하고, 실제 존재하는 그래프만을 표현하기 위한 방식입니다.


표현하는 방법은 정점을 기준으로 연결되어 있는 간선을 리스트형태 추가시켜 나가면 됩니다. 


방향이 없는 그래프라면 각 두 노드 리스트에 [상대 노드, 가중치] 형태를 갖는 자료구조를 추가해주면 됩니다.


반면에 방향이 있다면, 출발 노드 리스트에 [목적지 노드, 가중치] 형태를 갖는 자료구조를 추가해주면 됩니다.


이런 리스트적 특징 때문에 인접 리스트에서는 다음 특징을 갖습니다.


1) 두 노드간의 엣지가 존재 여부를 조사하기 위해서는 순차 탐색을 해야하기 때문에 O(V) 시간이 소요됩니다.


2) 인접 행렬과는 달리, 존재하는 엣지만을 표현하기 때문에 O(E) 만큼의 공간이 소요됩니다.


위 특징 때문에 모든 엣지 혹은 적은 엣지를 조사해야하는 그래프 알고리즘에서 유용하게 활용됩니다. 


하지만, 밀집 그래프 형태의 문제에서는 1번 특징으로 인해 시간 초과 문제가 발생할 수 있습니다.



간선 리스트(Edge list)


 

 


간선 리스트는 다른 두 방식과는 달리 엣지를 중심으로 표현하는 방법입니다.


즉, 단순하게 [노드1(혹은 출발 노드), 노드2(혹은 도착 노드), 가중치] 형태의 자료구조를 리스트 또는 배열 형태로 저장하는 방식입니다.


따라서 모든 엣지를 살펴보는 알고리즘에서 간편하고 직관적으로 활용할 수 있습니다.




각 방법을 사용해서 주어진 그래프를 표현해 봤습니다.


코드 레벨에서 설명하지는 않았지만, 이는 앞으로 다룰 그래프 관련 알고리즘에서 적어도 한 번씩은 다룹니다.


그때 코드 레벨에서 설명해드리도록 하겠으니 "아! 이런 방식이구나" 정도의 느낌만 알고 가시면 됩니다!

+ Recent posts