그래프(Graph)
[그래프란?]
- 그래프(graph) : 연결되어 있는 객체 간의 관계를 표현할 수 있는 자료 구조.
예) 지도
- 지도를 그래프로 표시하면 하나의 도시에서 다른 도시로 갈 때 최단 거리 경로가 어떤 것인지를 알고리즘을 이용해 쉽게 찾을 수 있음.
- 운영 체제에서 프로세스와 자원들이 어떻게 연관되는지를 그래프로 표현하게 되면 시스템의 효율이나 교착 상태 유무 등을 알아낼 수 있음.
- 그래프는 많은 문제들을 표현할 수 있는 훌륭한 논리적 도구.
- 선형 리스트나 트리의 구조로는 복잡한 문제들을 표현할 수 없음.
- 그래프 구조는 인접 행렬이나 인접 리스트로 메모리 에 표현되고 처리될 수 있으므로 광범위한 분야의 다양한 문제들을 그래프로 표현하여 컴퓨터 프로그래밍에 의해 해결할 수 있음.
- 그래프는 매우 보편적인 자료 구조로서 트리(tree)도 그래프의 하나의 특수한 종류로 볼 수 있음.
- 그래프 이론(graph theory) : 컴퓨터 학문 분야의 활발한 연구 주제이며 문제 해결을 위한 도구로서 많은 이론과 응용이 존재함.
- 그래프는 수학의 한 분야로서 수백 년간 연구되어 왔고 그래프의 많은 유용한 특성들이 밝혀져 왔음.
- 그러나 그래프 알고리즘을 컴퓨터 프로그램으로 구현하기 시작한 것은 오래되지 않았음.
- 그래프는 수학자 오일러(Euler)에 의해 처음 창안됨.
- 오일러는 위치라는 개념은 정점(vertex)으로, 위치 간의 관계는 간선(edge)로 표현하였음.
- 오일러는 그래프에 존재하는 모든 간선을 한 번만 통과하면서 처음 정점으로 되돌아오는 경로를 오일러 경로(Eulerian tour)라 정의하고, 그래프의 모든 정점에 연결된 간선의 개수가 짝수일 때만 오일러 경로가 존재한다는 오일러 정리를 증명함.
- 그래프는 정점(vertex)과 간선(edge)들의 집합으로 구성됨.
- 수학적으로는 G = (V, E)로 표시함. (V(G)는 그래프 G의 정점들의 집합을, E(G)는 그래프 G의 간선들의 집합을 의미함.)
- 정점은 여러 가지 특성을 가질 수 있는 객체를 의미하고, 간선은 이러한 정점들 간의 관계를 의미함.
- 정점(vertex)은 노드(node)라고도 불리며, 간선(edge)은 링크(link)라고도 불림.
- 그래프는 정점을 원으로, 간선을 선으로 그려서 사각적으로 표현하기도 하지만 이러한 사각적인 형태가 그래프의 정의는 아님.
- 그래프는 오직 정점과 간선의 집합일 뿐이며, 그래프의 시각적 표현은 이해를 돕는 역할만을 함에 주의해야 함.
- 그래프는 간선의 종류에 따라 무방향 그래프(undirected graph)와 방향 그래프(directed graph)로 구분됨.
- 무방향 그래프의 간선은 간선을 통해서 양 방향을 갈 수 있음을 나타내며 정점 A와 정점 B를 연결하는 간선은 (A, B)와 같이 정점의 쌍으로 표현함.
((A,B)와 (B, A)는 동일한 간선)
- 방향 그래프는 간선에 방향성이 존재하는 그래프로서 도로의 일방 통행길과 마찬가지로 간선을 통하여 한쪽 방향으로만 갈 수 있음을 나타냄.
- 점점 A에서 정점 B로만 갈 수 있는 간선은 <A, B>로 표시함. (<A, B>와 <B, A>는 서로 다른 간선)
- 그래프의 간선에 가중치를 할당하게 되면, 간선의 역할이 두 정점 간의 연결 유무뿐만 아니라 연결 강도까지 나타낼 수 있으므로 훨씬 복잡한 관계를 표현할 수 있게 됨.
- 가중치 그래프(weighted graph) 또는 네트워크(network) : 간선에 비용이나 가중치가 할당된 그래프
V(G1) = {0, 1, 2, 3), E(G1) = {(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)}
V(G2) = {0, 1, 2, 3}, E(G2) = {(0, 1), (0, 2)}
V(G3) = {0, 1, 2}, E(G3) = {<0, 1>, <1, 0>, <1, 2>}
- 부분 그래프(subgraph) : 그래프 G를 구성하는 정점의 집합 V(G)와 간선의 집합 E(G)의 부분집합으로 이루어진 그래프
- 인접 정점(adjacent vertex) : 간선에 의해 직접 연결된 정점
-> (a)의 그래프 G1에서 정점 0의 인접 저점은 정점 1, 정점 2, 정점 3
- 무방향 그래프에서의 정점의 차수(degree) : 하나의 정점에 인접한 정점의 수
-> 그래프 G1의 정점 0은 정점 차수가 3.
- 무방향 그래프에 존재하는 정점의 모든 차수를 합하면 그래프의 간선 수의 2배가 됨. (하나의 간선이 두 개의 정점에 인접하기 때문.)
- 진입 차수(in-degree) : 방향 그래프에서 외부에서 오는 간선의 수
- 진출 차수(out-degree) : 방향 그래프에서 외부로 향하는 간선의 수
-> 방향 그래프 G#의 정점 1은 내차수가 1, 외차수가 2.
- 방향 그래프에 있는 정점의 진입 차수 또는 진출 차수의 합은 방향 그래프의 간선의 수가 됨.
- 무방향 그래프에서 정점 s로부터 정점 e까지의 경로는 정점의 나열 s, v1, v2, ..., vk, e로서, 나열된 정점들 간에는 반드시 간선 (s, v1), (v1, v2), ..., (vk, e)가 존재해야 함.
- 만약 방향 그래프라면 간선 <s, v1>, <v1, v2>, ..., <vk, e>가 있어야 함.
-> 그래프 G1에서 0, 1, 2, 3은 경로이지만 0, 1, 3, 2는 경로가 아님. (간선 (1, 3)이 존재하지 않기 때문)
- 경로 길이(path length) : 경로를 구성하는 데 사용된 간선의 수
- 단순 경로(simple path) : 경로 중에서 반복되는 간선이 없을 경우의 경로
- 사이클(cycle) : 단순 경로의 시작 정점과 종료 정점이 동일할 때의 경로
-> 그래프 G1의 경로 1, 0, 2, 3은 단순 경로이고 경로 1, 0, 2, 0은 단순 경로가 아님.
-> 그래프 G1에서 경로 0, 1, 2, 0은 사이클이 됨.
-> 그래프 G3에서 경로 0, 1, 0도 사이클이 됨.
- 연결 그래프(connected graph) : 무방향 그래프 G에 있는 모든 정점쌍에 대하여 항상 경로가 존재한다면 G는 연결되었다고 하며, 이러한 무방향 그래프 G
- 그렇지 않은 그래프는 비연결 그래프라고 함.
-> (b)의 G2는 비연결 그래프.
- 트리(tree) : 그래프의 특수한 형태로서 사이클을 가지지 않은 연결 그래프.
-> (b), (c), (d)는 모두 연결되어 있을 뿐만 아니라 사이클이 없으므로 트리.
- 완전 그래프(complete graph) : 그래프에 속해 있는 모든 정점이 서로 연결되어 있는 그래프
- 무방향 완전 그래프의 정점 수를 n이라고 하면, 하나의 정점은 n-1개의 다른 정점으로 연결되므로 간선의 수는 n*(n-1)./2.
[그래프의 표현 방법]
- 그래프 문제를 프로그램에 의해 해결하기 위해서는 그래프(정점의 집합과 간선의 집합)를 컴퓨터 메모리에 저장하여야 함.
- 그래프를 메모리에 표현하는 방법으로는 2차원 배열을 사용하는 인접 행렬(adjacency matrix)과 연결 리스트를 사용하는 인접 리스트(adjacency list)의 두 가지 방법이 있음.
- 그래프 문제의 특성에 따라 위의 두 가지 표현 방법은 각각 메모리 사용량과 처리 시간 등에서 장단점을 가지므로 문제에 적합한 표현 방법을 선택해야 함.
(1) 인접 행렬
- 2차원 배열인 인접 행렬(adjacency matrix) M의 각 원소를 다음의 규칙에 의해 할당함으로써 그래프를 메모리에 표현할 수 있음.
if(간선 (i, j)가 그래프에 존재 M[i][j] = 1,
otherwise M[i][j] = 0.
- 우리가 다루고 있는 그래프에서는 자체 간선을 허용하지 않으므로 인접 행렬의 대각선 성분은 모두 0으로 표시됨.
- 무방향 그래프의 인접 행렬은 대칭 행렬이 됨. (무방향 그래프의 간선 (i, j)는 정점 i에서 정점 j로의 연결뿐만 아니라 정점 j에서 정점 i로의 연결을 동시에 의미하기 때문)
- 따라서 무방향 그래프의 경우, 배열의 상위 삼각이나 하위 삼각만 저장하면 메모리를 절약할 수 있음.
- 그러나 방향 그래프의 인접 행렬은 일반적으로 대칭이 아님.
- n개의 정점을 가지는 그래프를 인접 행렬로 표현하기 위해서는 간선의 수에 무관하게 항상 n²개의 메모리 공간이 필요함.
- 이에 따라 인접 행렬은 그래프에 간선이 많이 존재하는 밀집 그래프(dense graph)를 표현하는 경ㅇ에는 적합하나, 그래프 내에 적은 숫자의 간선만을 가지는 희소 그래프(sparse graph)의 경우에는 메모리의 낭비가 크므로 적합하지 않음.
- 인접 행렬을 이용하면 두 정점을 연결하는 간선의 존재 여부를 O(1) 시간 안에 즉시 알 수 있는 장점이 있음.
-> 정점 i와 정점 j를 연결하는 간선이 있는지를 알려면 M[i][j]의 값을 조사하면 바로 알 수 있음.
- 또한 정점의 차수는 인접 행렬의 행이나 열을 조사하면 알 수 있으므로 O(n)의 연산에 의해 알 수 있음.
- 정점 i에 대한 차수는 다음과 같이 인접 배열의 i번째 행에 있는 값을 모두 더하면 됨.
- 반면에 그래프에 존재하는 모든 간선의 수를 알아내려면 인접 행렬 전체를 조사해야 하므로 n²번의 조사가 필요하게 되어 O(n²)의 시간이 요구됨.
- 인접 행렬을 이용한 그래프 프로그램
// 인접 행렬을 이용한 그래프 프로그램
#include <stdio.h>
#define MAX_VERTICES 50
typedef struct GraphType {
int n; // 정점의 개수
int adj_mat[MAX_VERTICES][MAX_VERTICES];
} GraphType;
// 그래프 초기화
void graph_init(GraphType *g) {
int r, c;
g->n = 0;
for (r = 0; r < MAX_VERTICES; r++) {
for (c = 0; c < MAX_VERTICES; c++) {
g->adj_mat[r][c] = 0;
}
}
}
// 정점 삽입 연산
void insert_vertex(GraphType *g, int v) {
if (((g->n) + 1) > MAX_VERTICES) {
fprintf(stderr, "그래프: 정점의 개수 초과");
return;
}
g->n++;
}
// 간선 삽입 연산
void insert_edge(GraphType *g, int start, int end) {
if (start >= g->n || end >= g->n) {
fprintf(stderr, "그래프: 정점 번호 오류");
return;
}
g->adj_mat[start][end] = 1;
g->adj_mat[end][start] = 1;
}
(2) 인접 리스트
- 인접 리스트(adjacency list) : 그래프를 표현함에 있어 각각의 정점에 인접한 정점들을 연결 리스트로 표시한 것.
- 각 연결 리스트의 노드들은 인접 정점을 저장하게 됨.
- 각 연결 리스트들은 헤드 포인터를 가지고 있고 이 헤드 포인터들은 하나의 배열로 구성되어 있음.
- 따라서 정점의 번호만 알면 이 번호를 배열의 인덱스로 하여 각 정ㅇ점의 연결 리스트에 쉽게 접근할 수 있음.
- 무방향 그래프의 경우 정점 i와 정점 j를 연결하는 간선 (i, j)는 정점 i의 연결 리스트에 인접 정점 j로서 한 번 표현되고, 정점 j의 연결 리스트에 인접 정점 i로 다시 한 번 표현됨.
- 인접 리스트의 각각의 연결 리스트에 정점들이 입력되는 순서에 따라 연결 리스트 내에서 정점들의 순서가 달라질 수 있음.
- 정점의 수가 n개이고, 간선의 수가 e개인 무방향 그래프를 표시하기 위해서는 n개의 연결 리스트가 필요하고, n개의 헤드 포인터와 2e개의 노드가 필요함.
- 따라서 인접 리스트 표현은 간선의 개수가 적은 희소 그래프(sparse graph)의 표현에 적합함.
- 그래프의 간선 (i, j)의 존재 여부나 정점 i의 차수를 알기 위해서는 인접 리스트에서의 정점 i의 연결 리스트를 탐색해야 하므로 연결 리스트에 있는 노드의 수만큼, 즉 정점 차수만큼의 시간이 필요함.
- n개 정점과 e개의 간선을 가진 그래프에서 전체 간선의 수를 알아내려면 헤더 노드를 포함하여 모든 인접 리스트를 조사해야 하므로 O(n+e)의 연산이 요구됨.
- 인접 리스트를 이용한 그래프 프로그램
// 인접 리스트를 이용한 그래프 프로그램
#include <stdio.h>
#define MAX_VERTICES 50
typedef struct GraphNode {
int vertex;
struct GraphNode *link;
} GraphNode;
typedef struct GraphType {
int n; // 정점의 개수
GraphNode *adj_list[MAX_VERTICES];
} GraphType;
// 그래프 초기화
void graph_init(GraphType *g) {
int v;
g->n = 0;
for (v = 0; v < MAX_VERTICES; v++) {
g->adj_list[v] = NULL;
}
}
// 정점 삽입 연산
void insert_vertex(GraphType *g, int v) {
if (((g->n) + 1) > MAX_VERTICES) {
fprintf(stderr, "그래프: 정점의 개수 초과");
return;
}
g->n++;
}
// 간선 삽입 연산, v를 u의 인접 리스트에 삽입한다.
void insert_edge(GraphType *g, int u, int v) {
GraphNode *node;
if (u >= g->n || v >= g->n) {
fprintf(stderr, "그래프: 정점 번호 오류");
return;
}
node = (GraphNode *)malloc(sizeof(GraphNode));
node->vertex = v;
node->link = g->adj_list[u];
g->adj_list[u] = node;
}
[그래프의 탐색]
- 그래프의 탐색 : 그래프의 가장 기본적인 연산으로서 하나의 정점으로부터 시작하여 차례대로 모든 정점들을 한 번씩 방문하는 것.
- 그래프 탐색은 매우 중요함.
- 많은 문제들이 단순히 그래프의 노드를 탐색하는 것으로 해결됨.
- 대표적으로 특정한 정점에서 다른 정점으로 갈 수 있는지 없는지를 탐색을 통하여 알 수 있음.
- 그래프의 탐색 방법에는 깊이 우선 탐색과 너비 우선 탐색의 두 가지 탐색 방법이 있음.
(1) 깊이 우선 탐색
- 깊이 우선 탐색(depth first search : DFS) : 미로를 탐색할 때 한 방향으로 갈 수 있을 때까지 계속 가다가 더 이상 갈 수 없게 되면 다시 가장 가까운 갈림길로 돌아와서 이곳으로부터 다른 방향으로 다시 탐색을 진행하는 방법과 유사함.
- 깊이 우선 탐색은 그래프의 시작 정점에서 출발하여 먼저 시작 정점 v를 방문하고 방문하였다고 표시함.
- v에 인접한 정점들 중에서 아직 방문하지 않은 정점 u를 선택함.
- 만약 그러한 정점이 없다면 탐색이 종료됨.
- 만약 아직 방문하지 않은 정점 u가 있다면 u를 시작 정점으로 하여 깊이 우선 탐색을 다시 시작함.
- 이 탐색이 끝나게 되면 다시 v에 인접한 정점들 중에서 아직 방문이 안 된 정점을 찾음.
- 만약 없으면 종료하고, 있다면 다시 그 정점을 시작 정점으로 하여 깊이 우선 탐색을 다시 시작함.
- 깊이 우선 탐색도 자기 자신을 다시 호출하는 순환 알고리즘의 형태를 가지고 있음.
- 깊이 우선 탐색을 구현하는 데는 2가지의 방법이 있음.
1. 순환 호출을 이용하는 방법
2. 명시적인 스택을 사용하여 방문한 정점들을 스택에 저장하였다가 다시 꺼내어 작업하는 방법
- 이 글에서는 순환 호출을 이용하는 방법으로 깊이 우선 탐색을 구현하기로 함.
- 방문 여부를 기록하기 위해 배열 visited를 사용하며, 모든 정점의 visited 배열 값은 FALSE로 초기화되고 정점이 방문될 때마다 해당 정점의 visited 배열 값은 TRUE로 변경됨.
- 인접 배열로 표현된 그래프에 대한 깊이 우선 탐색 프로그램
// 인접 배열로 표현된 그래프에 대한 깊이 우선 탐색 프로그램
#include <stdio.h>
#define MAX_VERTICES 50
#define TRUE 1
#define FALSE 0
typedef struct GraphType {
int n; // 정점의 개수
int adj_mat[MAX_VERTICES][MAX_VERTICES];
} GraphType;
int visited[MAX_VERTICES];
// 인접 행렬로 표현된 그래프에 대한 깊이 우선 탐색
void dfs_mat(GraphType *g, int v) {
int w;
visited[v] = TRUE; // 정점 v의 방문 표시
printf("%d ", v); // 방문한 정점 출력
for (w = 0; w < g->n; w++) { // 인접 정점 탐색
if (g->adj_mat[v][w] && !visited[w]) {
dfs_mat(g, w); // 정점 w에서 DFS 새로 시작
}
}
}
- 인접 리스트로 표현된 그래프에 대한 깊이 우선 탐색 프로그램
// 인접 리스트로 표현된 그래프에 대한 깊이 우선 탐색 프로그램
#include <stdio.h>
#define MAX_VERTICES 50
#define TRUE 1
#define FALSE 0
int visited[MAX_VERTICES];
typedef struct GraphNode {
int vertex;
struct GraphNode *link;
} GraphNode;
typedef struct GraphType {
int n; // 정점의 개수
GraphNode *adj_list[MAX_VERTICES];
} GraphType;
// 인접 리스트로 표현된 그래프에 대한 깊이 우선 탐색
void dfs_list(GraphType *g, int v) {
GraphNode *w;
visited[v] = TRUE; // 정점 v의 방문 표시
printf("%d ", v); // 방문한 정점 출력
for (w = g->adj_list[v]; w; w = w->link) { // 인접 정점 탐색
if (!visited[w->vertex]) {
dfs_list(g, w->vertex); // 정점 w->vertex에서 DFS 새로 시작
}
}
}
(2) 너비 우선 탐색
- 너비 우선 탐색(breadth first search : BFS) : 시작 정점으로부터 가까운 장점을 먼저 방문하고 멀리 떨어져 있는 정점을 나중에 방문하는 순회 방법.
- 너비 우선 탐색을 위해서는 방문한 정점들을 차례로 저장한 후 꺼낼 수 있는 자료 구조인 큐(queue)가 필요함.
- 즉, 정점이 방문될 때마다 큐에 방문된 정점을 삽입하고, 더 이상 방문할 인접 정점이 없는 경우 큐의 앞에서 정점을 꺼내어 그 정점과 인접한 정점들을 모두 차례대로 방문하게 됨.
- 초기 상태의 큐에는 시작 정점만이 저장되고, 너비 우선 탐색 과정은 큐가 소진될 때까지 계속함.
- 너비 우선 탐색의 특징은 시작 정점으로부터 거리가 가까운 정점의 순서로 탐색이 진행된다는 것.
- 여기서 거리란 시작 정점으로부터 어떤 정점까지의 경로 중 가장 짧은 경로의 길이를 뜻함.
- 즉 너비 우선 탐색은 거리가 d인 정점들을 모두 방문한 다음, 거리가 (d+1)인 정점들을 모두 방문하게 됨.
- 거리가 0인 시작 정점을 방문한 후, 거리가 1인 모든 정점, 거리가 2인 모든 정점, 거리가 3인 모든 정점 등의 순서로 정점들을 방문해감.
- 인접 행렬로 표현된 그래프에 대한 너비 우선 탐색 프로그램
// 인접 행렬로 표현된 그래프에 대한 너비 우선 탐색 프로그램
#include <stdio.h>
#include <stdlib.h>
#define MAX_QUEUE_SIZE 100
#define MAX_VERTICES 50
#define TRUE 1
#define FALSE 0
typedef struct GraphType {
int n; // 정점의 개수
int adj_mat[MAX_VERTICES][MAX_VERTICES];
} GraphType;
int visited[MAX_VERTICES];
typedef int element;
typedef struct {
element queue[MAX_QUEUE_SIZE];
int front, rear;
} QueueType;
// 오류 함수
void error(char *message) {
fprintf(stderr, "%s\n", message);
exit(1);
}
// 초기화 함수
void init(QueueType *q) {
q->front = q->rear = 0;
}
// 공백 상태 검출 함수
int is_empty(QueueType *q) {
return (q->front == q->rear); // front == rear이면 공백 상태
}
// 포화 상태 검출 함수
int is_full(QueueType *q) {
return ((q->rear + 1) % MAX_QUEUE_SIZE == q->front); // front == (rear + 1)이면 포화 상태
}
// 삽입 함수
void enqueue(QueueType *q, element item) {
if (is_full(q)) {
error("큐가 포화상태입니다.");
}
q->rear = (q->rear + 1) % MAX_QUEUE_SIZE;
q->queue[q->rear] = item;
}
// 삭제 함수
element dequeue(QueueType *q) {
if (is_empty(q)) {
error("큐가 공백상태입니다.");
}
q->front = (q->front + 1) % MAX_QUEUE_SIZE;
return q->queue[q->front];
}
// 피크 함수
element peak(QueueType *q) {
if (is_empty(q)) {
error("큐가 공백상태입니다.");
}
return q->queue[(q->front + 1) % MAX_QUEUE_SIZE];
}
void bfs_mat(GraphType *g, int v) {
int w;
QueueType q;
init(&q); // 큐 초기화
visited[v] = TRUE; // 정점 v 방문 표시
printf("%d ", v); // 정점 출력
enqueue(&q, v); // 시작 정점을 큐에 저장
while (!is_empty(&q)) {
v = dequeue(&q); // 큐에서 정점 추출
for (w = 0; w < g->n; w++) { // 인접 정점 탐색
if (g->adj_mat[v][w] && !visited[w]) {
visited[w] = TRUE; // 방문 표시
printf("%d ", w); // 정점 출력
enqueue(&q, w); // 방문한 정점을 큐에 저장
}
}
}
}
- 인접 리스트로 표현된 그래프에 대한 너비 우선 탐색 프로그램
// 인접 리스트로 표현된 그래프에 대한 너비 우선 탐색 프로그램
#include <stdio.h>
#include <stdlib.h>
#define MAX_QUEUE_SIZE 100
#define MAX_VERTICES 50
#define TRUE 1
#define FALSE 0
int visited[MAX_VERTICES];
typedef struct GraphNode {
int vertex;
struct GraphNode *link;
} GraphNode;
typedef struct GraphType {
int n; // 정점의 개수
GraphNode *adj_list[MAX_VERTICES];
} GraphType;
typedef int element;
typedef struct {
element queue[MAX_QUEUE_SIZE];
int front, rear;
} QueueType;
// 그래프 초기화
void graph_init(GraphType *g) {
int v;
g->n = 0;
for (v = 0; v < MAX_VERTICES; v++) {
g->adj_list[v] = NULL;
}
}
// 정점 삽입 연산
void insert_vertex(GraphType *g, int v) {
if (((g->n) + 1) > MAX_VERTICES) {
fprintf(stderr, "그래프: 정점의 개수 초과");
return;
}
g->n++;
}
// 간선 삽입 연산, v를 u의 인접 리스트에 삽입한다.
void insert_edge(GraphType *g, int u, int v) {
GraphNode *node;
if (u >= g->n || v >= g->n) {
fprintf(stderr, "그래프: 정점 번호 오류");
return;
}
node = (GraphNode *)malloc(sizeof(GraphNode));
node->vertex = v;
node->link = g->adj_list[u];
g->adj_list[u] = node;
}
// 오류 함수
void error(char *message) {
fprintf(stderr, "%s\n", message);
exit(1);
}
// 초기화 함수
void init(QueueType *q) {
q->front = q->rear = 0;
}
// 공백 상태 검출 함수
int is_empty(QueueType *q) {
return (q->front == q->rear); // front == rear이면 공백 상태
}
// 포화 상태 검출 함수
int is_full(QueueType *q) {
return ((q->rear + 1) % MAX_QUEUE_SIZE == q->front); // front == (rear + 1)이면 포화 상태
}
// 삽입 함수
void enqueue(QueueType *q, element item) {
if (is_full(q)) {
error("큐가 포화상태입니다.");
}
q->rear = (q->rear + 1) % MAX_QUEUE_SIZE;
q->queue[q->rear] = item;
}
// 삭제 함수
element dequeue(QueueType *q) {
if (is_empty(q)) {
error("큐가 공백상태입니다.");
}
q->front = (q->front + 1) % MAX_QUEUE_SIZE;
return q->queue[q->front];
}
// 피크 함수
element peak(QueueType *q) {
if (is_empty(q)) {
error("큐가 공백상태입니다.");
}
return q->queue[(q->front + 1) % MAX_QUEUE_SIZE];
}
// 너비 우선 탐색(인접 리스트)
void bfs_list(GraphType *g, int v) {
GraphNode *w;
QueueType q;
init(&q); // 큐 초기화
visited[v] = TRUE; // 정점 v 방문 표시
printf("%d ", v); // 정점 v 출력
enqueue(&q, v); // 시작 정점을 큐에 저장
while (!is_empty(&q)) {
v = dequeue(&q); // 큐에서 정점 추출
for (w = g->adj_list[v]; w; w = w->link) { // 인접 정점 탐색
if (!visited[w->vertex]) { // 미방문 정점 탐색
visited[w->vertex] = TRUE; // 방문 표시
printf("%d ", w->vertex); // 정점 출력
enqueue(&q, w->vertex); // 방문한 정점을 큐에 삽입
}
}
}
}
void main() {
int i;
GraphType g;
graph_init(&g);
// 인접 리스트 생성
for (i = 0; i < 4; i++) {
insert_vertex(&g, i);
}
insert_edge(&g, 0, 1);
insert_edge(&g, 1, 0);
insert_edge(&g, 0, 3);
insert_edge(&g, 3, 0);
insert_edge(&g, 1, 2);
insert_edge(&g, 2, 1);
insert_edge(&g, 1, 3);
insert_edge(&g, 3, 1);
insert_edge(&g, 2, 3);
insert_edge(&g, 3, 2);
bfs_list(&g, 0);
}
- 너비 우선 탐색은 그래프가 인접 리스트로 표현되어 있으면 전체 수행 시간이 O(n+e)이며, 인접 행렬로 표현되어 있는 경우는 O(n²) 시간이 걸림.
- 너비 우선 탐색도 깊이 우선 탐색과 같이 희소 그래프인 경우 인접 리스트를 사용하는 것이 효율적임.
[연결 성분]
- 연결 성분(connected component) : 최대로 연결된 부분 그래프. -> 연결된 부분 그래프들 중에서 크기가 최대인 그래프.
- 연결 성분을 찾기 위해서 깊이 우선 탐색이나 너비 우선 탐색을 이용함.
- 먼저 그래프 상의 임의의 정점을 선택해 깊이 우선 탐색이나 너비 우선 탐색을 시작하면 시작 정점으로부터 도달 가능한 모든 정점들이 하나의 연결 성분이 됨.
- 다음에 방문 안 된 정점을 선택해서 다시 탐색을 실행하면 그 정점을 포함하는 연결 성분이 구해짐.
- 이러한 방법으로 그래프 상의 모든 정점이 방문될 때까지 이 과정을 되풀이하면 그래프에 존재하는 모든 연결 성분들을 찾을 수 있음.
- 깊이 우선 탐색 알고리즘을 이용해 연결 성분을 찾고자 할 경우, 깊이 우선 탐색 프로그램에서 정점 방문을 표시하는 문장인 visited[v] = TRUE를 visited[v] = count로 바꾸고, count를 증가시키면서 반복적으로 깊이 우선 탐색 프로그램을 호출함.
- 여기서 사용되는 count는 전역변수로서 0으로 초기화 되어야 함.
- 연결 성분 프로그램
// 연결 성분 프로그램
#include <stdio.h>
#define MAX_VERTICES 50
#define TRUE 1
#define FALSE 0
typedef struct GraphType {
int n; // 정점의 개수
int adj_mat[MAX_VERTICES][MAX_VERTICES];
} GraphType;
int visited[MAX_VERTICES];
int count;
// 인접 행렬로 표현된 그래프에 대한 깊이 우선 탐색
void dfs_mat(GraphType *g, int v) {
int w;
visited[v] = count; // 정점 v의 방문 표시
printf("%d ", v); // 방문한 정점 출력
for (w = 0; w < g->n; w++) { // 인접 정점 탐색
if (g->adj_mat[v][w] && !visited[w]) {
dfs_mat(g, w); // 정점 w에서 DFS 새로 시작
}
}
}
// 연결 성분
void find_connected_component(GraphType *g) {
int i;
count = 0;
for (i = 0; i < g->n; i++) {
if (!visited[i]) { // 방문되지 않았으면
count++;
dfs_mat(g, i);
}
}
}
[신장 트리]
- 신장 트리(spanning tree) : 그래프 내의 모든 정점을 포함하는 트리
- 신장 트리는 트리의 특수한 형태이므로 모든 정점들이 연결되어 있어야 하고 사이클을 포함해서는 안됨.
- 따라서 신장 트리는 그래프에 있는 n개의 정점을 정확히 (n-1)개의 간선으로 연결하게 됨.
- 그래프에서 신장 트리를 찾으려면 깊이 우선 탐색이나 너비 우선 탐색을 사용할 수 있음.
- 하나의 그래프에는 많은 신장 트리가 존재 가능함.
- 신장 트리는 깊이 우선 탐색이나 너비 우선 탐색 도중에 사용된 간선만 모으면 만들 수 있음.
- (b)의 맨 왼쪽 신장 트리는 시작 정점 0으로 깊이 우선 탐색할 때 사용된 간선ㅇ로만 만들어진 신장 트리(깊이 우선 신장 트리)
- (b)의 중간 신장 트리는 시작 정점 0으로 너비 우선 탐색할 때 사용된 간선으로만 만들어진 신장 트리(너비 우선 신장 트리)
- 신장 트리 알고리즘
depth_first_search(v) v를 방문되었다고 표시; for all u ∈ (v에 인접한 정점) do if (u가 아직 방문되지 않았으면) then (v,u)를 신장트리 간선이라고 표시; depth_first_search(u); |
- 신장 트리는 그래프의 최소 연결 부분 그래프가 됨.
- 여기서 최소의 의미는 간선의 수가 가장 적다는 의미.
- n개의 정점을 가지는 그래프는 최소한 n-1개의 간선을 가져야 하며 n-1개의 간선으로 연결되어 있으면 필연적으로 트리 형태가 되고 이것은 바로 신장 트리가 됨.
- 따라서 신장 트리는 통신 네트워크 구축에 사용됨.
예) n개의 위치를 연결하는 통신 네트워크를 최소의 링크를 이용하여 구축하고자 할 경우, 최소 링크 수는 n-1이 되고 따라서 신장 트리들이 가능한 대안이 됨.
예) 회사 내의 모든 전화기를 가장 적은 수의 케이블을 사용하여 연결하고자 한다면 신장 트리를 구함으로써 해결할 수 있음.
- 그러나 각 링크의 구축 비용은 똑같지 않음.
- 따라서 단순히 가장 적은 링크만을 사용한다고 해서 최소 비용이 얻어지는 것은 아님.
- 각 링크, 즉 간선에 비용을 붙여서 링크의 구축 비용까지를 고려하여 최소 비용의 신장 트리를 선택할 필요가 있음.
[최소 비용 신장 트리]
- 통신망, 도로망, 유통망 등은 대개 길이, 구축 비용, 전송 시간 등의 가중치를 간선에 할당한 그래프인 네트워크로 표현될 수 있음.
- 이러한 통신망, 도로망, 유통망을 가장 적은 비용으로 구축하고자 한다면, 네트워크에 있는 모든 정점들을 가장 적은 수의 간선과 비용으로 연결하는 최소 비용 신장 트리(MST : minimum spanning tree)가 필요하게 됨.
- 최소 비용 신장 트리 : 신장 트리 중에서 사용된 간선들의 가중치 합이 최소인 신장 트리
- 최소 비용 신장 트리를 구하는 방법으로는 Kruskal과 Prim이 제안한 알고리즘이 대표적임.
- 이 알고리즘들은 최소 비용 신장 트리가 간선의 가중치의 합이 최소이어야 하고, 반드시 (n-1)개의 간선만 사용해야 하며, 사이클이 포함되어서는 안 된다는 조건을 적절히 이용하고 있음.
(1) Kruskal의 MST 알고리즘
- Kruskal의 알고리즘은 탐욕적인 방법(greedy method)을 이용함.
- 탐욕적인 방법은 알고리즘 설계에 있어서 중요한 기법 중의 하나임.
- 탐욕적인 방법이란 결정을 해야 할 때마다 그 순간에 가장 좋다고 생각되는 것을 해답으로 선택함으로써 최종적인 해답에 도달함.
- 마치 음식을 먹을 때 매 순간에 가장 맛있다고 생각되는 것부터 먹는 것과 같음.
- 그 순간의 선택은 그 당시에는 최적임.
- 그러나 최적이라고 생각했던 해답들을 모아서 최종적인 해답을 만들었다고 해서, 그 해답이 전체적인 관점에서 최적이라는 보장은 없음.
- 따라서 탐욕적인 방법은 항상 최적의 해답을 주는지를 반드시 검증해야 함.
- 다행히 Kruskal의 알고리즘은 최적의 해답을 주는 것으로 증명되어 있음.
- Kruskal의 알고리즘은 최소 비용 신장 트리가 최소 비용의 간선으로 구성됨과 동시에 사이클을 포함하지 않는다는 조건에 근거하여, 각 단계에서 사이클을 이루지 않는 최소 비용 간선을 선택함.
- 이러한 과정을 반복함으로써 네트워크의 모든 정점을 최소 비용으로 연결하는 최적 해답을 구할 수 있음.
- Kruskal의 알고리즘은 먼저 그래프의 e개의 간선들을 가중치의 오름차순으로 정렬함.
- 정렬된 간선 리스트에서 순서대로 사이클을 형성하지 않는 간선을 선택하여 현재의 최소 비용 신장 트리의 집합에 추가함.
- 만약 사이클을 형성하면 그 간선은 제외됨.
- Kruskal의 알고리즘은 최소 비용 신장 트리를 구하는 다른 알고리즘보다 간단해 보임.
- 하지만, 다음 간선을 이미 선택된 간선들의 집합에 추가할 때 사이클을 생성하는지를 체크해야 함.
- 새로운 간선이 이미 다른 경로에 의하여 연결되어 있는 정점들을 연결할 때 사이클이 형성됨.
- union-find 알고리즘 : 지금 추가하고자 하는 간선의 양끝 정점이 같은 집합에 속해 있는지를 먼저 검사하는 알고리즘.
-> 간선의 양끝 정점이 서로 다른 집합에 속하는 경우 두 정점을 연결하여도 사이클이 형성되지 않음.
- union-find 연산은 Kruskal의 알고리즘에서만 사용되는 것은 아니고 일반적으로 널리 사용됨.
- union(x, y) 연산은 원소 x와 y가 속해 있는 집합을 입력으로 받아 2개의 집합의 합집합을 만듦.
- find(x) 연산은 원소 x가 속해 있는 집합을 반환함.
- 집합을 구현하는 데는 여러 가지 방법이 있을 수 있음. (비트 벡터, 배열, 연결 리스트를 이용하여 구현할 수 있음.)
- 그러나 가장 효율적인 방법은 트리 형태를 사용하는 것.
예) 집합 {1, 4}, {5, 2}, {3}, {6} 들은 (a)와 같이 표현되고, 이어서 union(4, 5)와 union(3, 6)을 한다면 (b)와 같은 트리로 변화하게 됨.
이 상태에서 find(4)를 한다면 루트인 정점 2가 집합의 대표로 반환되게 됨.
- find 연산에 소요되는 시간을 줄이려면 특정 정점과 루트와의 거리가 짧아야 함.
- 이것을 위하여 모든 정점이 루트 바로 아래에 있도록 하면 좋을 것.
- 이를 위하여 find 연산이 이루어질 때마다 모든 원소를 루트 노드 바로 밑에 놓음.
- 또한 union 연산 시에 정점의 개수가 적은 트리의 루트가 큰 트리의 루트를 가리키도록 하면 좋을 것.
- Kruskal의 최소 비용 신장 트리 프로그램
// Kruskal의 최소 비용 신장 트리 프로그램
#include <stdio.h>
#define MAX_VERTICES 100
#define INF 1000
int parent[MAX_VERTICES]; // 부모 노드
int num[MAX_VERTICES]; // 각 집합의 크기
// 초기화
void set_init(int n) {
int i;
for (i = 0; i<n; i++) {
parent[i] = -1;
num[i] = 1;
}
}
// vertex가 속하는 집합을 반환한다.
int set_find(int vertex) {
int p, s, i = -1;
for (i = vertex; (p = parent[i]) >= 0; i = p); // 루트 노드까지 반복
s = i; // 집합의 대표 원소
for (i = vertex; (p = parent[i]) >= 0; i = p) {
parent[i] = s; // 집합의 모든 원소들의 부모를 p로 설정
}
return s;
}
// 두개의 원소가 속한 집합을 합친다.
void set_union(int s1, int s2) {
if (num[s1] < num[s2]) {
parent[s1] = s2;
num[s2] += num[s1];
}
else {
parent[s2] = s1;
num[s1] += num[s2];
}
}
typedef struct {
int key; // 간선의 가중치
int u; // 정점 1
int v; // 정점 2
} element;
#define MAX_ELEMENT 100
typedef struct {
element heap[MAX_ELEMENT];
int heap_size;
} HeapType;
// 초기화 함수
init(HeapType *h) {
h->heap_size = 0;
}
// 히프 내용 출력 함수
print_heap(HeapType *h) {
int i;
int level = 1;
printf("\n===================");
for (i = 1; i <= h->heap_size; i++) {
if (i == level) {
printf("\n");
level *= 2;
}
printf("\t%d", h->heap[i].key);
}
printf("\n===================");
}
// 삽입 함수
void insert_min_heap(HeapType *h, element item) {
int i;
i = ++(h->heap_size);
// 트리를 거슬러 올라가면서 부모 노드와 비교하는 과정
while ((i != 1) && (item.key < h->heap[i / 2].key)) {
h->heap[i] = h->heap[i / 2];
i /= 2;
}
h->heap[i] = item; // 새로운 노드를 삽입
}
// 삭제 함수
element delete_min_heap(HeapType *h) {
int parent, child;
element item, temp;
item = h->heap[1];
temp = h->heap[(h->heap_size)--];
parent = 1;
child = 2;
while (child <= h->heap_size) {
// 현재 노드의 자식노드중 더 작은 자식노드를 찾는다.
if ((child < h->heap_size) && (h->heap[child].key) > h->heap[child + 1].key) {
child++;
}
if (temp.key <= h->heap[child].key) break;
// 한단계 아래로 이동
h->heap[parent] = h->heap[child];
parent = child;
child *= 2;
}
h->heap[parent] = temp;
return item;
}
// 정점 u와 정점 v를 연결하는 가중치가 weight인 간선을 히프에 삽입
void insert_heap_edge(HeapType *h, int u, int v, int weight) {
element e;
e.u = u;
e.v = v;
e.key = weight;
insert_min_heap(h, e);
}
// 인접 행렬이나 인접 리스트에서 간선들을 읽어서 최소 히프에 삽입
// 현재는 예제 그래프의 간선들을 삽입한다.
void insert_all_edges(HeapType *h) {
insert_heap_edge(h, 0, 1, 29);
insert_heap_edge(h, 1, 2, 16);
insert_heap_edge(h, 2, 3, 12);
insert_heap_edge(h, 3, 4, 22);
insert_heap_edge(h, 4, 5, 27);
insert_heap_edge(h, 5, 0, 10);
insert_heap_edge(h, 6, 1, 15);
insert_heap_edge(h, 6, 3, 18);
insert_heap_edge(h, 6, 4, 25);
}
// kruskal의 최소 비용 신장 트리 프로그램
void kruskal(int n) {
int edge_accepted = 0; // 현재까지 선택된 간선의 수
HeapType h; // 최소 히프
int uset, vset; // 정점 u와 정점 v의 집합 번호
element e; // 히프 요소
init(&h); // 히프 초기화
insert_all_edges(&h); // 히프에 간선들을 삽입
set_init(n); // 집합 초기화
while (edge_accepted < (n - 1)) { // 간선의 수 < (n-1)
e = delete_min_heap(&h); // 최소 히프에서 삭제
uset = set_find(e.u); // 정점 u의 집합 번호
vset = set_find(e.v); // 정점 v의 집합 번호
if (uset != vset) { // 서로 속한 집합이 다르면
printf("(%d,%d) %d \n", e.u, e.v, e.key);
edge_accepted++;
set_union(uset, vset); // 두개의 집합을 합친다.
}
}
}
void main()
{
kruskal(7);
}
- 이 union-find 알고리즘을 사용하면 Kruskal의 알고리즘의 시간 복잡도는 간선들을 정렬하는 시간에 좌우됨.
- 따라서 간선 e개를 퀵 정렬과 같은 효율적인 알고리즘으로 정렬한다면 Kruskal 알고리즘의 시간 복잡도는 O(elog₂e)이 됨.
(2) Prim의 MST 알고리즘
- Prim의 알고리즘은 시작 정점에서부터 출발하여 신장 트리 집합을 단계적으로 확장해나가는 방법임.
- 시작 단계에서는 시작 정점만이 신장 트리 집합에 포함됨.
- Prim의 방법은 앞 단계에서 만들어진 신장 트리 집합에 인접한 정점들 중에서 최소 간선으로 연결된 정점을 선택하여 트리를 확장함.
- 이 과정은 트리가 n-1개의 간선을 가질 때까지 계속됨.
- Kruskal의 알고리즘은 간선 선택을 기반으로 하는 알고리즘인 반면에, Prim의 알고리즘은 정점 선택을 기반으로 하는 알고리즘임.
- 또한 Kruskal의 알고리즘은 이전 단계에서 만들어진 신장 트리와는 상관없이 무조건 최소 간선만을 선택하는 방법인 데 반하여 Prim의 알고리즘은 이전 단계에서 만들어진 신장 트리를 확장하는 방식임.
- Prim의 최소 비용 신장 트리 프로그램
// Prim의 최소 비용 신장 트리 프로그램
#include <stdio.h>
#define TRUE 1
#define FALSE 0
#define VERTICES 7
#define INF 1000L
int adj_mat[VERTICES][VERTICES] = {
{ 0, 29, INF, INF, INF, 10, INF },
{ 29, 0, 16, INF, INF, INF, 15 },
{ INF, 16, 0, 12, INF, INF, INF },
{ INF, INF, 12, 0, 22, INF, 18 },
{ INF, INF, INF, 22, 0, 27, 25 },
{ 10, INF, INF, INF, 27, 0, INF },
{ INF, 15, INF, 18, 25, INF, 0 } };
int selected[VERTICES];
int dist[VERTICES];
// 최소 dist[v] 값을 갖는 정점을 반환
int get_min_vertex(int n) {
int v, i;
for (i = 0; i <n; i++)
if (!selected[i]) {
v = i;
break;
}
for (i = 0; i < n; i++) {
if (!selected[i] && (dist[i] < dist[v])) v = i;
}
return (v);
}
void prim(int s, int n) {
int i, u, v;
for (u = 0; u<n; u++) {
dist[u] = INF;
}
dist[s] = 0;
for (i = 0; i<n; i++) {
u = get_min_vertex(n);
selected[u] = TRUE;
if (dist[u] == INF) return;
printf("%d ", u);
for (v = 0; v<n; v++) {
if (adj_mat[u][v] != INF) {
if (!selected[v] && adj_mat[u][v]< dist[v]) {
dist[v] = adj_mat[u][v];
}
}
}
}
}
void main()
{
prim(0, VERTICES);
}
- Prim의 알고리즘은 주 반복문이 정점의 수 n만큼 반복하고, 내부 반복문이 n번 반복하므로 Prim의 알고리즘은 O(n²)의 복잡도를 가짐.
- Krustal의 알고리즘은 복잡도가 O(elog₂e)이므로 희박한 그래프를 대상으로 할 경우에는 Krustal의 알고리즘이 적합하고, 밀집한 그래프의 경우에는 Prim의 알고리즘이 유리하다고 할 수 있음.
[최단 경로]
- 최단 경로(shortest path) 문제 : 네트워크에서 정점 u와 정점 v를 연결하는 경로 중에서 간선들의 가중치 합이 최소가 되는 경로를 찾는 문제
- 간선의 가중치는 비용, 거리, 시간 등을 나타냄.
- 최단 경로를 발견하는 방법에는 2가지의 알고리즘이 있음.
1. Dijkstra 알고리즘 : 하나의 시작 정점에서 모든 다른 정점까지의 최단 경로를 구함.
2. Floyd 알고리즘 : 모든 정점에서 다른 모든 정점까지의 최단 경로를 구함.
- 가중치는 가중치 인접 행렬이라고 불리는 2차원 배열에 저장되어 있다고 가정.
- 여기서 인접 행렬과 가중치 인접 행렬과의 차이점을 주의 깊게 살펴보아야 함.
- 인접 행렬에서는 간선이 없으면 인접 행렬의 값이 0임.
- 그러나 가중치 인접 행렬에서는 간선의 가중치가 0일 수도 있기 때문에 0의 값이 간선이 없음을 나타내지 못함.
- 따라서 다른 방법을 강구해야 하는데, 이론적으로는 무한대의 값을 가중치 인접 행렬에 저장하면 됨.
- 즉, 무한대의 값이면 간선이 없다고 생각하면 됨.
- 그러나 컴퓨터에는 무한대의 값이 없음.
- 따라서 만약 간선이 존재하지 않으면 정수 중에서 상당히 큰 값을 무한대라고 생각하고 가중치 인접 행렬에 저장하는 것으로 함.
(1) Dijkstra의 최단 경로 알고리즘
- Dijkstra의 최단 경로 알고리즘 : 네트워크에서 하나의 시작 정점으로부터 모든 다른 정점까지의 최단 경로를 찾는 알고리즘.
- 최단 경로는 경로의 길이순으로 구해짐.
- 하나의 시작 정점으로부터 모든 다른 정점까지의 최단 경로 찾음.
- 집합 S -> 시작 정점 v로부터의 최단경로가 이미 발견된 정점들의 집합
- distance 배열 -> 최단경로가 알려진 정점들만을 이용한 다른 정점들까지의 최단경로 길이
-> distance 배열의 초기값(시작 정점 v)
- distance[v] = 0
- 다른 정점에 대한 distance 값은 시작정점과 해당 정점간의 가중치 값
- 매 단계에서 가장 distance 값이 작은 정점을 S에 추가
- distance 값이 가장 작은 정점을 u라고 하면, 시작 정점 v에서 정점 u까지의 최단거리는 경로 ①이 됨.
- 정점 w를 거쳐서 정점 u로 가는 가상적인 더 짧은 경로가 있다고 가정하면, 정점 v에서 정점 u까지의 거리는 정점 v에서 정점 w까지의 거리 ②와 정점 w에서 정점 u로 가는 거리③을 합한 값이 됨.
- 그러나 경로 ②는 경로 ①보다 항상 길 수 밖에 없음. (현재 distance 값이 가장 작은 정점은 u이기 때문.)
- 따라서 매 단계에서 distance 값이 가장 작은 정점들을 추가해나가면 시작 정점에서 모든 정점까지의 최단거리를 구할 수 있음.
- 새로운 정점이 S에 추가되면 distance값 갱신.
- 최단 경로 Dijkstra 프로그램
// 최단 경로 Dijkstra 프로그램
#include <stdio.h>
#define INT_MAX 2147483647 // 최대 정수
#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 7 // 정점의 수
#define INF 1000 // 무한대(연결이 없는 경우)
int weight[MAX_VERTICES][MAX_VERTICES] = { // 네트워크의 인접행렬
{ 0, 7, INF, INF, 3, 10, INF },
{ 7, 0, 4, 10, 2, 6, INF },
{ INF, 4, 0, 2, INF, INF, INF },
{ INF, 10, 2, 0, 11, 9, 4 },
{ 3, 2, INF, 11, 0, INF, 5 },
{ 10, 6, INF, 9, INF, 0, INF },
{ INF, INF, INF, 4, 5, INF, 0 } };
int distance[MAX_VERTICES]; // 시작 정점으로부터의 최단 경로 거리
int found[MAX_VERTICES]; // 방문한 정점 표시
int choose(int distance[], int n, int found[])
{
int i, min, minpos;
min = INT_MAX;
minpos = -1;
for (i = 0; i<n; i++) {
if (distance[i]< min && !found[i]) {
min = distance[i];
minpos = i;
}
}
return minpos;
}
void shortest_path(int start, int n) {
int i, u, w;
for (i = 0; i<n; i++) { // 초기화
distance[i] = weight[start][i];
found[i] = FALSE;
}
found[start] = TRUE; // 시작 정점 방문 표시
distance[start] = 0;
for (i = 0; i<n; i++) {
u = choose(distance, n, found);
found[u] = TRUE;
for (w = 0; w<n; w++) {
if (!found[w]) {
if (distance[u] + weight[u][w] < distance[w]) {
distance[w] = distance[u] + weight[u][w];
}
}
}
}
}
void main()
{
shortest_path(0, MAX_VERTICES);
}
- 위 프로그램의 효율성을 높이기 위해서는 최솟값을 선택하는 choose 함수를 우선순위 큐로 대치하면 더 빠르게 수행시킬 수 있음.
- 네트워크에 n개의 정점이 있다면, Dijkstra 최단 경로 알고리즘은 주 반복문을 n번 반복하고 내부 반복문을 2n번 반복하므로 O(n²)의 복잡도를 가짐.
(2) Floyd의 최단 경로 알고리즘
- 그래프에 존재하는 모든 정점 사이의 최단 경로를 구하려면 Dijkstra의 알고리즘을 정점의 수만큼 반복 실행하면 됨.
- 그러나 모든 정점 사이의 최단 거리를 구하려면 더 간단하고 좋은 알고리즘이 있음.
- Floyd 알고리즘 : 그래프에 존재하는 모든 정점 사이의 최단 경로를 한 번에 모두 찾아주는 알고리즘
- Floyd의 최단 경로 알고리즘은 2차원 배열 A를 이용하여 3중 반복을 하는 루프로 구성되어 있음.
- 인접 행렬 weight 구성
-> i==j이면, weight[i][j]=0
-> 두개의 정점 i, j 사이에 간선이 존재하지 않으면, weight[i][j]=∞
-> 정점 i, j 사이에 간선이 존재하면, weight[i][j]는 간선 (i, j)의 가중치
-> 배열 A의 초기 값은 인접 행렬 weight임.
- A^k [i][j]
-> 0부터 k까지의 정점만을 이용한 정점 i에서 j까지의 최단 경로 길이
- A^-1→A^0 → A^1 → … → A^n-1순으로 최단 경로 구해감.
- A^k-1까지 구해진 상태에서 k번째 정점이 추가로 고려되는 상황을 생각함.
- 0부터 k까지의 정점만을 사용하여 정점 i에서 정점 j로 가는 최단 경로는 다음의 2가지의 경우로 나뉘어짐.
1. 정점 k를 거치지 않는 경우: A^k[i][j] 는 k보다 큰 정점은 통과하지 않으므로 최단거리는 여전히 A^k-1[i][j]]임.
2. 정점 k를 거치는 경우: i에서 k까지의 최단거리 A^k-1[i][k]에 k에서 j까지의 최단거리 A^k-1[k][j]를 더한 값
- Floyd의 최단 경로 프로그램
// Floyd의 최단 경로 프로그램
#include <stdio.h>
#include <limits.h>
#define TRUE 1
#define FALSE 0
#define VERTICES 7 // 정점의 수
#define INF 10000 // 무한대 (연결이 없는 경우)
int weight[VERTICES][VERTICES] = { // 네트워크의 인접행렬
{ 0, 7, INF, INF, 3, 10, INF },
{ 7, 0, 4, 10, 2, 6, INF },
{ INF, 4, 0, 2, INF, INF, INF },
{ INF, 10, 2, 0, 11, 9, 4 },
{ 3, 2, INF, 11, 0, INF, 5 },
{ 10, 6, INF, 9, INF, 0, INF },
{ INF, INF, INF, 4, 5, INF, 0 } };
int A[VERTICES][VERTICES];
printA(int n) {
int i, j, k;
printf("===============================\n");
for (i = 0; i<n; i++) {
for (j = 0; j<n; j++) {
if (A[i][j] == INF) {
printf("INF ");
}
else printf("%3d ", A[i][j]);
}
printf("\n");
}
printf("===============================\n");
}
void floyd(int n) {
int i, j, k;
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
A[i][j] = weight[i][j];
}
}
for (k = 0; k<n; k++) {
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
if (A[i][k] + A[k][j] < A[i][j]) {
A[i][j] = A[i][k] + A[k][j];
}
}
}
printA(n);
}
}
void main()
{
floyd(VERTICES);
}
- 두 개의 정점 사이의 최단 경로를 찾는 Dijkstra의 알고리즘은 시간 복잡도가 O(n²)이므로, 모든 정점 쌍의 최단 경로를 구하려면 Dijkstra의 알고리즘을 n번 반복해야 하고, 따라서 전체 복잡도는 O(n³)이 됨.
- 한 번에 모든 정점 간의 최단 경로를 구하는 Floyd의 알고리즘은 3중 반복문이 실행되므로 시간 복잡도가 O(n³)으로 표현되고, 이는 Dijkstra의 알고리즘과 비교해도 차이가 없다고 할 수 있음.
- 그러나 Floyd의 알고리즘은 매우 간결한 반복 구문을 사용하므로 Dijkstra의 알고리즘보다 상당히 빨리 모든 정점 간의 최단 경로를 찾을 수 있음.
[위상 정렬]
- 방향 그래프에서 간선 <u, v>가 있다면 정점 u는 정점 v를 선행함.
- 방향 그래프의 위상 정렬(topological sort) : 방향 그래프에 존재하는 각 정점들의 선행 순서를 위배하지 않으면서 모든 정점을 나열하는 것
- 방향 그래프를 대상으로 위상 정렬을 하기 위한 알고리즘은 간단함.
- 먼저 진입 차수가 0인 정점을 선택하고, 선택된 정점과 여기에 부속된 모든 간선을 삭제함.
- 이와 같이 진입 차수 0인 정점의 선택과 삭제 과정을 반복해서 모든 정점이 선택 및 삭제되면 알고리즘이 종료됨.
- 진입 차수 0인 정점이 여러 개 존재할 경우 어느 정점을 선택하여도 무방함. (따라서 하나의 그래프에는 복수의 위상 순서가 있을 수 있음.)
- 이 과정에서 선택되는 정렬의 순서를 위상 순서(topological order)라 함.
- 위의 과정 중에 그래프에 남아 있는 정점 중에 진입 차수 0인 정점이 없다면, 이러한 그래프로 표현된 프로젝트는 실행 불가능한 프로젝트가 되고 위상 정렬 알고리즘은 중단됨.
- 그래프 위상 정렬 프로그램
// 그래프 위상 정렬 프로그램
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 50
#define MAX_STACK_SIZE 100
typedef struct GraphNode
{
int vertex;
struct GraphNode *link;
} GraphNode;
typedef struct GraphType {
int n; // 정점의 개수
GraphNode *adj_list[MAX_VERTICES];
} GraphType;
typedef int element;
typedef struct {
element stack[MAX_STACK_SIZE];
int top;
} StackType;
// 그래프 초기화
void graph_init(GraphType *g) {
int v;
g->n = 0;
for (v = 0; v<MAX_VERTICES; v++) {
g->adj_list[v] = NULL;
}
}
// 정점 삽입 연산
void insert_vertex(GraphType *g, int v) {
if (((g->n) + 1) > MAX_VERTICES) {
fprintf(stderr, "그래프: 정점의 개수 초과");
return;
}
g->n++;
}
// 간선 삽입 연산, v를 u의 인접 리스트에 삽입한다.
void insert_edge(GraphType *g, int u, int v) {
GraphNode *node;
if (u >= g->n || v >= g->n) {
fprintf(stderr, "그래프: 정점 번호 오류");
return;
}
node = (GraphNode *)malloc(sizeof(GraphNode));
node->vertex = v;
node->link = g->adj_list[u];
g->adj_list[u] = node;
}
// 스택 초기화 함수
void init(StackType *s) {
s->top = -1;
}
// 공백 상태 검출 함수
int is_empty(StackType *s) {
return (s->top == -1);
}
// 포화 상태 검출 함수
int is_full(StackType *s) {
return (s->top == (MAX_STACK_SIZE - 1));
}
// 삽입함수
void push(StackType *s, element item) {
if (is_full(s)) {
fprintf(stderr, "스택 포화 에러\n");
return;
}
else s->stack[++(s->top)] = item;
}
// 삭제함수
element pop(StackType *s) {
if (is_empty(s)) {
fprintf(stderr, "스택 공백 에러\n");
exit(1);
}
else return s->stack[(s->top)--];
}
// 피크함수
element peek(StackType *s) {
if (is_empty(s)) {
fprintf(stderr, "스택 공백 에러\n");
exit(1);
}
else return s->stack[s->top];
}
// 위상정렬을 수행한다.
int topo_sort(GraphType *g) {
int i;
StackType s;
GraphNode *node;
// 모든 정점의 진입 차수를 계산
int *in_degree = (int *)malloc(g->n * sizeof(int));
for (i = 0; i < g->n; i++) { // 초기화
in_degree[i] = 0;
}
for (i = 0; i < g->n; i++) {
GraphNode *node = g->adj_list[i];//정점 i에서 나오는 간선들
while (node != NULL) {
in_degree[node->vertex]++;
node = node->link;
}
}
// 진입 차수가 0인 정점을 스택에 삽입
init(&s);
for (i = 0; i < g->n; i++) {
if (in_degree[i] == 0) push(&s, i);
}
// 위상 순서를 생성
while (!is_empty(&s)) {
int w;
w = pop(&s);
printf("%d", w); //정점 출력
node = g->adj_list[w]; //각 정점의 진입 차수를 변경
while (node != NULL) {
int u = node->vertex;
in_degree[u]--; //진입 차수를 감소
if (in_degree[u] == 0) push(&s, u);
node = node->link; // 다음 정점
}
}
free(in_degree);
return (i == g->n); // 반환값이 1이면 성공, 0이면 실패
}
void main()
{
GraphType g;
graph_init(&g);
insert_vertex(&g, 0);
insert_vertex(&g, 1);
insert_vertex(&g, 2);
insert_vertex(&g, 3);
insert_vertex(&g, 4);
insert_vertex(&g, 5);
//정점 0의 인접 리스트 생성
insert_edge(&g, 0, 2);
insert_edge(&g, 0, 3);
//정점 1의 인접 리스트 생성
insert_edge(&g, 1, 3);
insert_edge(&g, 1, 4);
//정점 2의 인접 리스트 생성
insert_edge(&g, 2, 3);
insert_edge(&g, 2, 5);
//정점 3의 인접 리스트 생성
insert_edge(&g, 3, 5);
//정점 4의 인접 리스트 생성
insert_edge(&g, 4, 5);
//위상 정렬
topo_sort(&g);
}
내용 출처 : C언어로 쉽게 풀어쓴 자료구조 (천인국 외 지음, 생능출판사)
'Computer Science > Data Structure' 카테고리의 다른 글
[C++] 이진 탐색 트리(Binary Search Tree) (0) | 2021.05.16 |
---|---|
[C++] 트리 순회(Tree Traversal) (0) | 2021.05.15 |
[C] 탐색(Search) (0) | 2017.06.21 |
[C] 해싱(Hashing) (0) | 2017.06.20 |
[C] 정렬(Sorting) (0) | 2017.06.18 |
[C] 우선순위 큐(Priority Queue) (0) | 2017.06.17 |
[C] 트리(Tree) (0) | 2017.06.01 |
[C] 큐(Queue) (0) | 2017.05.23 |