그래프(graph): 연결되어 있는 객체 간의 관계를 표현할 수 있는 자료구조
수학자 오일러(Euler)는 "모든 다리를 한 번만 건너서 출발했던 장소로 돌아올 수 있는가?" 라는 문제에 대한 답을 그래프 이론을 이용해서 증명했음
-> 그래프의 모든 정점에 연결된 간선의 개수가 짝수일 때만 경로만 존재함
정점(vertex): 위치
간선(edge): 위치 간의 관계
오일러 경로(Eulerian tour): 그래프에 존재하는 모든 간선을 한 번만 통과하면서 처음 정점으로 되돌아오는 경로
그래프: 정점(vertex)와 간선(edge)들의 집합, G=(V, E)
V(G): 그래프 G의 정점들의 집합, 여러 가지 특성을 가질 수 있는 객체를 의미(= 노드(node))
E(G): 그래프 G의 간선들의 집합, 정점들 간의 관계(= 링크(link))
간선의 종류
1. 무방향 그래프(undirected graph)
-> 간선을 통해서 양 방향으로 갈 수 있음을 나타내며 정점 A와 정점 B를 연결하는 간선은 (A, B)와 같이 정점의 쌍으로 표현 ((A, B) = (B, A))
2. 방향 그래프(directed graph)
-> 간선을 통해서 한쪽 방향으로만 갈 수 있음을 나타내며 정점 A에서 정점 B로만 갈 수 있는 간선은 <A, B>와 같이 표현 (<A, B> != <B, A>)
간선에 가중치 부여 유무
1. 가중치 그래프(weighted graph): 간선에 비용이나 가중치가 할당된 그래프(= 네트워크(network))
부분 그래프(subgraph): 그래프 G를 구성하는 정점의 집합 V(G)와 간선의 집합 E(G)의 부분 집합으로 이루어진 그래프
인접 정점(adjacent vertex): 간선에 의해 직접 연결된 정점
차수(degree): 무방향 그래프에서 하나의 정점에 인접한 정점의 수
-> 무방향 그래프에 존재하는 정점의 모든 차수를 합하면 그래프의 간선 수의 2배가 됨
진입 차수(in-degree): 방향 그래프에서 외부에서 오는 간선의 수
진출 차수(out-degree): 방향 그래프에서 외부로 향하는 간선의 수
-> 방향 그래프에 있는 정점의 진입 차수 또는 진출 차수의 합은 그래프의 간선의 수가 됨
경로 길이(path length): 경로를 구성하는데 사용된 간선의 수
단순 경로(simple path): 경로 중에서 반복되는 간선이 없는 경우
사이클(cycle): 단순 경로의 시작 정점과 종료 정점이 동일한 경우
연결 그래프(connected graph): 그래프에 있는 모든 정점쌍에 대해 항상 경로가 존재하는 그래프
-> 트리는 그래프의 특수한 형태로서 사이클을 가지지 않는 연결 그래프임
비연결 그래프(unconnected graph): 항상 경로가 존재하는 것이 아닌 그래프
완전 그래프(complete graph): 그래프에 속해 있는 모든 정점이 서로 연결되어 있는 그래프 -> 정점 n개면, 간선은 n(n-1)/2이 됨
객체: 정점의 집합과 간선의 집합
연산
create_graph() ::= 그래프를 생성
init(g) ::= 그래프 g를 초기화
insert_vertex(g,v) ::= 그래프 g에 정점 v를 삽입
insert_edge(g,u,v) ::= 그래프 g에 간선 (u,v)를 삽입
delete_vertex(g,v) ::= 그래프 g의 정점 v를 삭제
delete_edge(g,u,v) ::= 그래프 g의 간선 (u,v)를 삭제
is_empty(g) ::= 그래프 g가 공백 상태인지 확인
adjacent(v) ::= 정점 v에 인접한 정점들의 리스트를 반환
destory_graph(g) ::= 그래프 g를 제거
인접 행렬(adjacency matrix): 2차원 배열로 그래프를 메모리에 표현
인접 행렬의 시간복잡도
1. 두 정점을 연결하는 간선의 존재 여부를 시간 안에 즉시 알 수 있는 장점이 있음
2. 정점의 차수는 인접 행렬의 행이나 열을 조사하면 알 수 있으므로 의 연산에 의해 알 수 있음
-> 정점 i에 대한 차수 -> , 인접 행렬의 i번째 행에 있는 값을 모두 더하면 됨
3. 그래프에 존재하는 모든 간선의 수를 알아내려면 인접 행렬 전체를 조사해야 하기 때문에 의 시간이 요구 됨
#define MAX_VERTICES 50
typedef struct GraphType{
int n; // 정점의 개수
int adj_mat[MAX_VERTICES][MAX_VERTICES];
} GraphType;
// 그래프 초기화
void graph_init(GraphType *g){
g->n = 0;
for(int r=0;r<MAX_VERTICES;r++){
for(int 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(g->n <= start || g->n <= end ){
fprintf(stderr,"그래프: 정점 번호 오류");
return;
}
g->adj_mat[start][end] = g->adj_mat[end][start] = 1; // 무방향 그래프
}
인접 리스트(adjacency list): 그래프를 표현함에 있어 각각의 정점에 인접한 정점들을 연결 리스트로 표시한 것
인접 리스트의 시간복잡도
1. 그래프의 간선 (i, j)의 존재 여부나 정점 i의 차수를 알기 위해서는 인접 리스트에서의 정점 i의 연결리스트를 탐색해야 하므로 연결리스트에 있는 노드의 수(정점의 차수)만큼 시간이 필요함
2. n개 정점과 e개의 간선을 가진 그래프에서 전체 간선의 수를 알아내려면 헤더 노드를 포함한 모든 인접 리스트를 조사해야 하므로 의 연산이 요구됨
#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){
g->n = 0;
for(int 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(g->n <= u || g->n <= v){
fprintf(stderr,"그래프: 정점 번호 오류");
return;
}
node = (GraphNode *)malloc(sizeof(GraphNode));
node->vertex = v;
node->link = g->adj_list[u];
g->adj_list[u] = node;
}
그래프의 탐색: 그래프의 가장 기본적인 연산으로서, 하나의 정점으로부터 시작하여 차례대로 모든 정점들을 한 번씩 방문하는 것임
깊이 우선 탐색(depth first search: DFS): 미로를 탐색할 때 한 방향으로 갈 수 있을 때까지 계속 가다가 더 이상 갈 수 없게 되면 다시 가장 가까운 갈림길로 돌아와서 이곳으로부터 다른 방향으로 탐색을 진행하는 방법과 유사
1. 시작 정점에서 출발하여 먼저 시작 정점 v를 방문하고 방문하였다고 표시함
2. v에 인접한 정점들 중에서 아직 방문하지 않은 정점 u를 선택 만약 그러한 정점이 없다면 탐색은 종료
3. 지금 방문하지 않은 u가 있으면 u를 시작 정점으로 하여 깊이 우선 탐색을 다시 시작함
4. 이 탐색이 끝나면 다시 v에 인접한 정점들 중에서 아직 방문이 안된 정점을 찾고 없으면 종료, 있으면 다시 3번을 반복
depth_first_search(v)
v를 방문되었다고 표시;
for all u ∈ (v에 인접한 정점) do
if (u가 아직 방문되지 않았으면)
then depth_first_search(u)
int visited[MAX_VERTICES]; // 방문 여부를 기록하기 위한 배열
// 인접 행렬로 표현된 그래프에 대한 깊이 우선 탐색
void dfs_mat(GraphType *g, int v){
visited[v] = true; // 정점 v의 방문 표시
printf("%d ",v); // 방문한 정점 출력
for(int w =0;w<g->n;w++){ // 인접 정점 탐색
if(g->adj_mat[v][w] && !visited[w] ){ // adj_mat[v][w] == 1 : 정점 v와 정점 w가 인접한 것
dfs_mat(g,w); // 정점 w에서 DFS 새로 시작
}
}
}
int visited[MAX_VERTICES];
// 인접 리스트로 표현된 그래프에 대한 깊이 우선 탐색
void dfs_list(GraphType *g, int v){
visited[v] = true; // 정점 v의 방문 표시
printf("%d ",v); // 방문한 정점 출력
GraphNode *w;
for(w= g->adj_list[v]; w; w=w->link){ // 인접 정점 탐색
if(!visited[w->vertex]){
dfs_list(g, w->vertex); // 정점 w->vertex에서 DFS 새로 시작
}
}
}
너비 우선 탐색(breadth first search: BFS): 시작 정점으로부터 거리가 가까운 정점을 먼저 방문하고 멀리 떨어져 있는 정점을 나중에 방문하는 순회 방법 (거리란 시작 정점으로부터 어떤 정점까지의 경로 중 가장 짧은 경로의 길이)
breadth_first_search(v)
v를 방문되었다고 표시;
큐 Q에 정점 v를 삽입;
while(not is_empty(Q)) do
큐 Q에서 정점 w를 삭제;
for all u ∈ (w에 인접한 정점) do
if(u가 아직 방문되지 않았으면)
then u를 큐 Q에 삽입;
u를 방문되었다고 표시;
void bfs_mat(GraphType *g, int v){
QueueType q;
init(&q); // 큐 초기화
visited[v] = true; // 정점 v 방문 표시
printf("%d ",v); // 정점 출력
enqueue(&q, v); // 시작 정점을 큐에 저장
while(!is_empty(&q)){
v = dequeue(&q); // 큐에서 정점 추출
for(int w =0; w<g->n;w++){ // 인접 정점 탐색
if(g->adj_mat[v][w] && !visited[w]){
visited[w] = true; // 방문 표시
printf("%d ", w); // 정점 출력
enqueue(&q, w); // 방문한 정점을 큐에 저장
}
}
}
}
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); // 방문한 정점을 큐에 삽입
}
}
}
}
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 50
typedef int element;
typedef struct QueueNode{
element item;
struct QueueNode* link;
} QueueNode;
typedef struct{
QueueNode *front, *rear;
} QueueType;
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){
g->n = 0;
for (int i = 0; i < MAX_VERTICES;i++){
g->adj_list[i] = NULL;
}
}
// 정점 삽입 연산
void insert_vertex(GraphType *g, int v){
if(((g->n)+1) > MAX_VERTICES){
fprintf(stderr, "graph: Exceeding the number of vertices\n");
exit(1);
}
g->n++;
}
// 간선 삽입 연산
void insert_edge(GraphType *g, int u, int v){
if( g->n <= u || g->n <= v ){
fprintf(stderr, "graph: Number error at vertex\n");
exit(1);
}
GraphNode* node = (GraphNode*)malloc(sizeof(GraphNode));
node->vertex = v;
node->link = g->adj_list[u];
g->adj_list[u] = node;
}
void print(GraphType *g){
for (int i = 0; i < g->n; i++) {
printf("vertex: %d adj_vertex: ", i);
for (GraphNode* w = g->adj_list[i]; w;w=w->link){
printf("%d ", w->vertex);
}
printf("\n");
}
}
int visited[MAX_VERTICES];
// 연결 리스트 큐 소스를 여기에
void init(QueueType *q){
q->front = NULL;
q->rear = NULL;
}
// 공백 상태 검출 함수
int is_empty(QueueType *q){
return (q->front == NULL);
}
// 삽입 함수
void enqueue(QueueType *q, element item){
QueueNode* node = (QueueNode*)malloc(sizeof(QueueNode));
if(node == NULL){
fprintf(stderr, "Memory allocate error\n");
exit(1);
}else{
node->item = item; // 데이터 저장
node->link = NULL; // 링크 필드를 NULL로 설정
if(is_empty(q)){ // 큐가 공백이면
q->front = node;
q->rear = node;
}else{ // 큐가 공백이 아니면
q->rear->link = node; // 마지막에 삽입
q->rear = node;
}
}
}
element dequeue(QueueType *q){
QueueNode* node = q->front;
element item;
if(is_empty(q)){ // 공백 상태
fprintf(stderr, "Queue empty\n");
exit(1);
}else{
item = node->item; // 데이터를 꺼냄
q->front = q->front->link; // front를 다음 노드를 가리키도록 함
if(q->front == NULL){ // 공백 상태
q->rear = NULL;
}
free(node); // 노드 메모리 해제
return item; // 데이터 반환
}
}
element peek(QueueType* q)
{
if (is_empty(q)) { // 공백 상태
fprintf(stderr, "Queue empty\n");
exit(1);
} else {
return q->front->item; // 데이터 반환
}
}
void bfs_list(GraphType*g,int v){
QueueType q;
init(&q);
visited[v] = TRUE;
printf("%d ", v);
enqueue(&q, v);
while(!is_empty(&q)){
v = dequeue(&q);
for (GraphNode *w = g->adj_list[v]; w;w=w->link){
// printf("%d ", w->vertex);
if(!visited[w->vertex]){
visited[w->vertex] = TRUE;
printf("%d ", w->vertex);
enqueue(&q, w->vertex);
}
}
}
}
int main(){
GraphType g;
graph_init(&g);
// 인접 리스트 생성
for (int 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);
print(&g);
bfs_list(&g, 0);
}
연결 성분(connected component): 최대로 연결된 부분 그래프
1. 그래프 상의 임의의 정점을 선택해 깊이 우선 탐색이나 너비 우선 탐색을 시작하면 시작 정점으로부터 도달 가능한 모든 정점들이 하나의 연결 성분이 됨
2. 다음에 방문 안된 정점을 선택해서 다시 탐색을 실행하면 그 정점을 포함하는 연결 성분이 구해짐
3. 그래프 상의 모든 정점이 방문될 때가지 이 과정을 되풀이하면 그래프에 존재하는 모든 연결 성분들을 찾을 수 있음
void find_connected_component(GraphType *g){
count = 0;
for(int i=0; i<g->n;i++){
if(!visited[i]){ // 방문되지 않았으면
count++;
dfs_mat(g,i);
}
}
}
신장 트리(spanning tree): 그래프 내의 모든 정점을 포함하는 트리
깊이 우선 신장 트리: 맨 왼쪽 신장 트리, 시작 정점 0으로 깊이 우선 탐색할 때 사용된 간선으로만 만들어진 신장 트리
너비 우선 신장 트리: 중간 신장 트리, 시작 정점 0으로 너비 우선 탐색할 때 사용된 간선으로만 만들어진 신장 트리
depth_first_search(v)
v를 방문되었다고 표시;
for all u ∈ (v에 인접한 정점) do
if (u가 아직 방문되지 않았으면)
then (v,u)를 신장 트리 간선이라고 표시
depth_first_search(u)
최소 비용 신장 트리(MST: minimum spanning tree): 신장 트리 중에서 사용된 간선들의 가중치 합이 최소인 신장 트리
최소 신장 트리 구하는 방법
-> Kruskal, Prim이 제안한 알고리즘
-> 최소 비용 신장 트리가 간선의 가중치의 합이 최소여야함
-> 반드시 (n-1)개의 간선만 사용해야 함
-> 사이클이 포함되어서는 안됨
Kruskal 알고리즘
// 최소 비용 신장 트리를 구하는 kruskal의 알고리즘
// 입력: 가중치 그래프 G = (V, E), n은 노드의 개수
// 출력: Et, 최소 비용 신장 트리를 이루는 간선들의 집합
kruskal(G)
E를 w(e1) <= ... <= w(ee)가 되도록 정렬
Et <- Φ; ecounter <- 0
k <- 0
while ecounter < (n-1) do
k <- k + 1
if Et U {ek}가 사이클을 포함하지 않으면
then Et <- Et U {ek}; ecounter <- ecounter + 1
return Et
사이클 생성 여부 체크
union(x,y): 원소 x와 y가 속해 있는 집합을 입력으로 받아 2개의 집합의 합집합을 만듦
find(x): 원소 x가 속해 있는 집합을 반환
ex) S = {1,2,3,4,5,6}
1. 집합의 원소를 하나씩 분리하여 독자적인 집합으로 만듦
{1}, {2}, {3}, {4}, {5}, {6}
2. union(1,4), union(5,2)를 하면
{1,4}, {5,2}, {3}, {6}
3. union(4,5), union(3,6)을 하면
{1,4,5,2},{3,6}
집합을 구현하는데 여러가지 방법 존재
int parent[MAX_VERTICES]; // 부모 노드
int num[MAX_VERTICES]; // 각 집합의 크기
// 초기화
void set_init(int n){
for(int i=0;i<n;i++){
parent[i] = -1; // 루트의 정점은 부모 배열에서 -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; // 집합의 모든 원소들의 부모를 s로 설정
}
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];
}
}
#include <stdio.h>
#define MAX_VERTICES 100
#define INF 1000
int parent[MAX_VERTICES]; // 부모 노드
int num[MAX_VERTICES]; // 각 집합의 크기
// 집합 초기화
void set_init(int n)
{
for (int i = 0; i < n; i++) {
parent[i] = -1; // 루트의 정점은 부모 배열에서 -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; // 집합의 모든 원소들의 부모를 s로 설정
}
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
// 최소 힙 프로그램 - krusal에서 간선들의 가중치의 오름차순으로 정렬하기 때문
typedef struct{
element heap[MAX_ELEMENT];
int heap_size;
} HeapType;
// 힙 초기화 함수
void init(HeapType *h){
h->heap_size = 0;
}
int is_empty(HeapType*h){
if(h->heap_size == 0){
return true;
}else{
return false;
}
}
// 삽입함수
void insert_min_heap(HeapType*h, element item){
int 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){
element item = h->heap[1];
element tmp = h->heap[(h->heap_size)--];
int parent = 1;
int child = 2;
while(child <=h->heap_size){
// 현재 노드의 자식 노드 중 더 작은 자식 노드를 찾음
if((child < h->heap_size) && (h->heap[child].key > h->heap[child+1].key) ){
child++;
}
if(tmp.key <= h->heap[child].key)
break;
// 한단계 아래로 이동
h->heap[parent] = h->heap[child];
parent = child;
child *= 2;
}
h->heap[parent] = tmp;
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, 2, 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)){
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); // 두개의 집합을 합침
}
}
}
int main(){
kruskal(7);
}
시간복잡도
Prim의 알고리즘: 시작 정점에서부터 출발하여 신장 트리 집합을 단계적으로 확장해나가는 방법
Kruskal 알고리즘 = 간선 선택을 기반으로 하는 알고리즘, 이전 단계에서 만들어진 신장 트리와는 상관없이 무조건 최소 간선만을 선택하는 방법
Prim 알고리즘 = 정점 선택을 기반으로 하는 알고리즘, 이전 단계에서 만들어진 신장 트리를 확장하는 방법
// 최소 비용 신장 트리를 구하는 Prim의 알고리즘
// 입력: 네트워크 G=(V,E), S는 시작 정점
// 출력: Vt, 최소 비용 신장 트리를 이루는 정점들의 집합
Prim(G,s)
Vt <- {s}: vcounter <- 1
while vcounter < n do
(u,v)는 u∈Vt and v∉Vt인 최저 비용 간선;
if (그러한 (u,v)가 존재하면)
then Vt <- Vt U v; vcounter <- vcounter +1
else 실패
return Vt
// 최소 신장 트리를 구하는 Prim의 알고리즘
// 입력: 네트워크 G=(V,E), s는 시작 정점
// 출력: 최소 비용 신장 트리를 이루는 정점들의 집합
Prim(G, s)
for each u ∈ V do
dist[u] <- ∞
dist[s] <- 0
우선순위 큐Q에 모든 정점을 삽입(우선순위는 dist[])
for i <- 0 to n-1 do
u <- delete_min(Q)
화면에 u 출력
for each v ∈ (u의 인접 정점)
if(v ∈ Q and weight[u][v] < dist[v])
then dist[v] <- weight[u][v]
#include <stdio.h>
#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 7
#define INF 1000L
int weight[MAX_VERTICES][MAX_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[MAX_VERTICES];
int dist[MAX_VERTICES];
// 최소 dist[v]값을 갖는 정점을 반환
int get_min_vertex(int n){
int v;
for (int i = 0; i < n;i++){
if(!selected[i]){
v = i;
break;
}
}
for (int 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;
selected[u] = FALSE;
}
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(weight[u][v] != INF){
if(!selected[v] && weight[u][v] < dist[v]){
dist[v] = weight[u][v];
}
}
}
}
}
int main(){
prim(0, MAX_VERTICES);
}
최단 경로(shortest path): 네트워크에서 정점 u와 정점 v를 연결하는 경로 중에서 간선들의 가중치 합이 최소가 되는 경로를 찾는 문제
인접 행렬: 간선이 없으면 인접 행렬의 값은 0
가중치 인접 행렬: 간선의 가중치가 0일 수도 있기 때문에 간선이 없는 경우 값은 무한대(정수 중에서 상당히 큰 값)
Dijkstra 알고리즘: 하나의 시작 정점에서 모든 다른 정점까지의 최단 경로를 구함
최단 경로: 경로의 길이순으로 구해짐
집합 S를 시작 정점 v로부터의 최단 경로가 이미 발견된 정점들의 집합
Dijkstra에서는 시작 정점에서 집합 S에 있는 정점만을 거쳐서 다른 정점으로 가는 최단 거리를 기록하는 배열이 필요 -> 1차원 배열: distance
- distance의 초기값
알고리즘은 매 단계에서 집합 S 안에 있지 않은 정점 중에서 가장 distance 값이 작은 정점을 집합 S에 추가
distance[w] = min(distance[w], distance[u] + weight[u][w]) // 새로 추가된 정점 u를 거쳐서 정점까지 가는 거리와 기존 거리를 비교해서 더 작은 거리로 distance 값을 수정
// 입력: 가중치 그래프 G, 가중치는 음수가 아님
// 출력: distance 배열, distance[u]는 v에서 u까지의 최단 거리
shortest_path(G, v)
S <- {v}
for 각 정점 w ∈ G do
distance[w] <- weight[u][w];
while 모든 정점이 S에 포함되지 않으면 do
u <- 집합 S에 속하지 않는 정점 중에서 최소 distance 정점;
S <- S U {u}
for u에 인접하고 S에 있지 않은 각 정점 w do
if distanace[u] + weight[u][w] < distance[w]
then distance[w] <- distance[u] + weight[u][w];
#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]; // 방문한 정점 표시
void print(int n)
{
printf("S = ");
for (int i = 0; i < n; i++) {
printf("%d ", found[i]);
}
printf("\n");
printf("distance[] = ");
for (int i = 0; i < n; i++) {
printf("%d ", distance[i]);
}
printf("\n");
}
int choose(int distance[], int n, int found[]){ // 최소값을 선택
int min = INT_MAX;
int minpos = -1;
for (int 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){
// 초기화
for (int i = 0; i < n;i++){
distance[i] = weight[start][i];
found[i] = FALSE;
}
found[start] = TRUE; // 시작 정점 방문 표시
distance[start] = 0;
print(n);
for (int i = 0; i < n - 1;i++){
int u = choose(distance, n, found);
found[u] = TRUE;
for (int w = 0; w < n;w++){
if(!found[w]){
if(distance[u] + weight[u][w] < distance[w]){
distance[w] = distance[u] + weight[u][w];
}
}
}
print(n);
}
}
int main(){
shortest_path(0, MAX_VERTICES);
}
시간복잡도
인접행렬 weight
1. i == j, weight[i][j] =0
2. 정점 i, j 사이 간선이 존재하지 않으면, weight[i][j] = 무한대
3. 정점 i, j 사이 간선이 존재하면, weight[i][j] = 간선(i, j)의 가중치
floyd(G)
for k <- 0 to n-1
for i <- 0 to n-1
for j <- 0 to n-1
A[i][j] <- min(A[i][j], A[i][k]+A[k][j])
#include <stdio.h>
#define min(x,y) (((x) < (y)) ? (x):(y))
#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 A[MAX_VERTICES][MAX_VERTICES];
void print(int n){
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
printf("%4d ", A[i][j]);
}
printf("\n");
}
printf("\n");
}
void floyd(int n){
for (int i = 0; i < n;i++){
for (int j = 0; j < n;j++){
A[i][j] = weight[i][j];
}
}
for (int k = 0; k < n;k++){
for (int i = 0; i < n;i++){
for (int j = 0; j < n;j++){
A[i][j] = min(A[i][j], A[i][k] + A[k][j]);
}
}
print(n);
}
}
int main(){
floyd(MAX_VERTICES);
}
시간복잡도
크루스칼(Kruskal) | 다익스트라(Dijkstra) | |
---|---|---|
구현 방법 | 우선순위 큐 + union-find(서로소집합) | 우선순위 큐+ dp(점화식) |
중심 | 간선 | 정점 |
시작점 | 간선의 가중치가 작은 것부터 시작(오름차순 정렬) | 출발점이 정해져 있는 경우 |
Queue 진입 시점 | 모든 정점을 유선순위 큐에 넣고 시작 | dp 갱신될 때만(최단 경로 갱신) 그 정점에서 출발하는 간선을 우선순위 큐에 추가 |
용도 | 최소 신장 트리를 그릴 때 | 한 정점에서 다른 정점의 최단 거리를 구하는 경우 |
최단 거리 | 최소의 비용(최단 거리) 모든 점을 다 연결할 때 사용 모든 점을 다 이은 경로가 최단 거리라고 말할 수 있지만 임의의 두 점 간의 거리가 최단 거리라는 보장은 없음 | 임의의 두 점 간의 최단 거리 구할 때 사용 |
위상 정렬(topological sort): 방향 그래프에 존재하는 각 정점들의 선행 순서를 위배하지 않으면서 모든 정점을 나열하는 것
1. 진입 차수가 0인 정점을 선택
-> 진입 차수 0인 정점이 여러 개 존재할 경우 어느 정점을 선택해도 무방함(하나의 그래프에는 복수의 위산 순서가 있을 수 있음)
2. 선택된 정점과 여기에 부속된 모든 간선 삭제
3. 1,2을 반복하다가 모든 정점이 선택 및 삭제되면 종료
-> 이 과정에서 선택되는 정점의 순서를 위상 순서(topological order)라고 함
// input: 그래프 G=(V,E)
// output: 위상 정렬 순서
topo_sort(G)
for i <- 0 to n-1 do
if( 모든 정점이 선행 정점을 가지면 )
then 사이클이 존재하고 위상 정렬 불가;
선행 정점을 가지지 않는 정점 v를 선택;
v를 출력;
v와 v에 부속한 모든 간선들을 그래프에서 삭제;
// 위상 정렬을 수행함
void topo_sort(GraphType *g){
StackType s;
GraphNode *node;
// 모든 정점의 진입 차수를 계산
int *in_degree = (int *)malloc(g->n* sizeof(int));
for(int i=0;i<g->n;i++){ // 초기화
in_degree[i] = 0;
}
for(int i=0;i<g->n;i++){
node = g->adj_list[[i]; // 정점 i에서 나오는 간선들
while(node != NULL){
in_degree[node->vertex]++;
node = node->link;
}
}
// 진입 차수가 0인 정점(후보 정점)을 스택에 삽입
init(&s)
for(int 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; // 반환 값이 1이면 성공, 0이면 실패
}
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 50
typedef struct GraphNode{
int vertex;
struct GraphNode* link;
} GraphNode;
typedef struct{
int n; // wjdwjadml rotn
GraphNode* adj_list[MAX_VERTICES];
} GraphType;
// 그래프 초기화
void graph_init(GraphType *g){
g->n = 0;
for (int i = 0; i < MAX_VERTICES;i++){
g->adj_list[i] = NULL;
}
}
// 정점 삽입 연산
void insert_vertex(GraphType *g, int v){
if((g->n+1) >MAX_VERTICES){
fprintf(stderr, "graph: Exceeding the number of vertices\n");
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, "graph: Number error at vertex\n");
return;
}
node = (GraphNode*)malloc(sizeof(GraphNode));
node->vertex = v;
node->link = g->adj_list[u];
g->adj_list[u] = node;
}
// 스택 소스 추가
#define MAX_STACK_SIZE 100
typedef int element;
typedef struct {
element stack[MAX_STACK_SIZE];
int top;
} StackType;
// 스택 초기화 함수
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 data){
if(is_full(s)){
fprintf(stderr, "overflow\n");
exit(1);
}else{
s->stack[++(s->top)] = data;
}
}
// 삭제 함수
element pop(StackType *s){
if(is_empty(s)){
fprintf(stderr, "underflow\n");
exit(1);
}else{
return s->stack[(s->top)--];
}
}
// 피크 함수
element peek(StackType* s)
{
if (is_empty(s)) {
fprintf(stderr, "underflow\n");
exit(1);
} else {
return s->stack[s->top];
}
}
// 위상 정렬을 수행함
void topo_sort(GraphType* g)
{
StackType s;
GraphNode* node;
// 모든 정점의 진입 차수를 계산
int* in_degree = (int*)malloc(g->n * sizeof(int));
for (int i = 0; i < g->n; i++) { // 초기화
in_degree[i] = 0;
}
for (int i = 0; i < g->n; i++) {
node = g->adj_list[i]; // 정점 i에서 나오는 간선들
while(node != NULL){
in_degree[node->vertex]++;
node = node->link;
}
}
// 진입 차수가 0인 정점을 스택에 삽입
init(&s);
for (int 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; // 반환 값이 1이면 성공, 0이면 실패
}
int 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);
// 정점 21의 인접 리스트 생성
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언어로 쉽게 풀어쓴 자료구조』, 생능출판(2016)