개념정리/자료구조

자료구조 공부 #21 (그래프2)

한반가 2021. 6. 14. 15:20

2021.06.08 - [이론공부/자료구조] - 자료구조 공부 #20 (그래프)


신장 트리

  • 그래프 내의 모든 정점을 포함하는 트리
  • n개의 정점을 가지는 그래프의 신장트리는 n-1개의 간선을 가진다

신장 트리 예시

신장 트리는 그래프의 최소 연결 부분 그래프 이다.

 

 

신장트리는 깊이 너비 우선 탐색 도중에 사용된 간선만 모으면 만들 수 있다.

depth_first_search(v):
  v 를 방문되었다표시;
    for all u ∈ (v에 인접한 정점) do
      if (u가 아직 방문되지 않았으면)
         then (v,u)를 신장 트리 간선이라고 표시;
            depth_first_search(u)

 

 

최소비용 신장트리

  • 네트워크(그래프)에 있는 모든 정점들을 가장 적은 수의 간선과 비용으로 연결
  • MST의 응용 (Minimun Spanning Tree) = 최소 비용 신장트리
    • 도로 건설 - 도시들을 모두 연결하면서 도로의 길이를 최소가 되도록 하는 문제
    • 전기회로 - 단자들을 모두 연결하면서 전선의 길이를 최소로 하는 문제
    • 통신 - 전화선의 길이를 최소로 하면서 전화 케이블 망을 구성하는 문제
    • 배관 - 파이프를 모두 연결하면서 총길이를 최소로 하는 문제
    • kruskal 알고리즘과 prime 알고리즘이 대표적이다.

예시

 

MST 예시

  • MST 조건
    • 본래 그래프의 모든 노드를 포함
    • 간선의 가중치 합이 최소여야함
    • 모든 노드가 서로 연결되어 있어야함
    • 트리의 속성을 만족 해야함 (사이클 존재X)

 


Kruskal의 MST 알고리즘

  • 탐욕적인 방법 (Greedy Method)
    • 주요 알고리즘 설계기법
    • 각 단계에서 최선의 답을 선택하는 과정을 반복함으로써 최종적인 해답에 도달
    • 탐욕적인 방법은 항상 최적의 해답을 주는지 검증 필요
    • Kruskal MST 알고리즘은 최적의 해답임이 증명됬다.

현재 존재하는 가장 가중치가 작을걸 먼저 하는 구조이다

 

Kruskal의 알고리즘으로 정리하는 과정

 

 

Union-Find 알고리즘

  • 일반적으로 널리 사용되는 알고리즘으로 서로소 집합을 표현할 때 사용하는 알고리즘
  • Union-Find 연산
    • make-set(x) : 초기화, x를 유일한 원소로 하는 새로운 집합을 생성
    • union(x,y) : 합하기. x가 속한 집합과 y가 속한 집합을 합친다. x, y 두집합을 합치는 연산
    • find(x) : 찾기, x가 속한 집합의 대표값 (루트 노드 값)을 반환, x가 어떤 집합에 속해있는지 찾는 연산
  • Kruskal의 MST 알고리즘에서 사이클 검사에 사용함

포인터로 연결하면서 사이크 ㄹ검사

 

 

 

알고리즘

 

 

더보기
더보기
int parent[MAX_VERTICES];
void set_init(int n) { 
	for (int i = 0; i < n; i++)
		parent[i] = -1;
}

// curr가 속하는 집합을 반환한다. 
int set_find(int curr) {
	if (parent[curr] == -1)
		return curr; // 루트
	while (parent[curr] != -1) curr = parent[curr];
	return curr;
}

// 두개의 원소가 속한 집합을 합친다.
void set_union(int a, int b) {
	int root1 = set_find(a); // 노드 a의 루트를 찾는다. 
	int root2 = set_find(b); // 노드 b의 루트를 찾는다. 
	if (root1 != root2) // 합한다. 
		parent[root1] = root2;
}

 

 

C++로 Kruskal 알고리즘 구현

 

#include <stdio.h>
#include <stdlib.h>

#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 100
#define INF 1000


int parent[MAX_VERTICES]; // 부모 노드
// 초기화
void set_init(int n) {
	for (int i = 0; i < n; i++)
		parent[i] = -1;
}


// curr가 속하는 집합을 반환한다.
int set_find(int curr) {
	if (parent[curr] == -1)
		return curr; // 루트
	while (parent[curr] != -1) curr = parent[curr];
	return curr;
}

// 두 개의 원소가 속한 집합을 합친다.
void set_union(int a, int b) {
	int root1 = set_find(a); // 노드 a의 루트를 찾는다. 
	int root2 = set_find(b); // 노드 b의 루트를 찾는다. 
	if (root1 != root2) // 합한다. 
		parent[root1] = root2;
}


struct Edge { // 간선을 나타내는 구조체
	int start, end, weight;
};


typedef struct GraphType {
	int n; // 간선의 개수
	struct Edge edges[2 * MAX_VERTICES];
} GraphType;


// 그래프 초기화
void graph_init(GraphType* g) {
	g->n = 0;
	for (int i = 0; i < 2 * MAX_VERTICES; i++) {
		g->edges[i].start = 0;
		g->edges[i].end = 0;
		g->edges[i].weight = INF;
	}
}
// 간선 삽입 연산
void insert_edge(GraphType* g, int start, int end, int w) {
	g->edges[g->n].start = start;
	g->edges[g->n].end = end;
	g->edges[g->n].weight = w;
	g->n++;
}
// qsort()에 사용되는 함수
int compare(const void* a, const void* b) {
	struct Edge* x = (struct Edge*)a;
	struct Edge* y = (struct Edge*)b;
	return (x->weight - y->weight);
}

// kruskal의 최소 비용 신장 트리 프로그램
void kruskal(GraphType* g) {
	int edge_accepted = 0; // 현재까지 선택된 간선의 수
	int uset, vset; // 정점 u와 정점 v의 집합 번호
	struct Edge e;
	set_init(g->n); // 집합 초기화
	qsort(g->edges, g->n, sizeof(struct Edge), compare);
	printf("크루스칼 최소 신장 트리 알고리즘 \n");
	int i = 0;
	while (edge_accepted < (g->n - 1)) { // 간선의 수 < (n-1)
		e = g->edges[i];
		uset = set_find(e.start); // 정점 u의 집합 번호
		vset = set_find(e.end); // 정점 v의 집합 번호
		if (uset != vset) { // 서로 속한 집합이 다르면
			printf("간선 (%d,%d) %d 선택\n", e.start, e.end, e.weight);
			edge_accepted++;
			set_union(uset, vset); // 두 개의 집합을 합친다.
		}
		i++;
	}
}


int main(void) {

	GraphType* g;
	g = (GraphType*)malloc(sizeof(GraphType));
	graph_init(g);
	insert_edge(g, 0, 1, 29);
	insert_edge(g, 1, 2, 16);
	insert_edge(g, 2, 3, 12);
	insert_edge(g, 3, 4, 22);
	insert_edge(g, 4, 5, 27);
	insert_edge(g, 5, 0, 10);
	insert_edge(g, 6, 1, 15);
	insert_edge(g, 6, 3, 18);
	insert_edge(g, 6, 4, 25);
	kruskal(g);
	free(g);

	return 0;

}
교재에서 이용한 코드를 그대로 참고함

 

 

Kruskal의 알고리즘 복잡도

  • Kruskal 알고리즘은 대부분 간선들을 정렬하는 시간에 좌우 된다
    • 사이클 테스트 등의 작접은 정렬에 비해 매우 신속하게 수행된다
  • 네트워크 간선 e개를 퀵 정렬과 같은 효율적인 알고리즘으로 정렬 한다면 Kruskal 알고리즘의 시간 복잡도는 O(elog2e)가 된다.

 


Prim의 MST 알고리즘

  • 시작 정점에서부터 출발하여 신장 트리 집합을 단계적으로 확장해 나감
  • 신장 트리 집합에 인접한 정점 중에서 최저 간선으로 연결된 정점 선택하여 신장 트리에 집합 추가함

 

Kruskal 알고리즘과 Prime 알고리즘 비교

  • Kruskal : Edge 기반 - 무조건 최저 간선만을 선택 -> edge의 가중치로 정렬이 필요함
  • Prime : Vertex 기반 - 이전 단뎨에서 만들어진 신장 트리를 확장하는 방식

Prim 알고리즘 과정

 

Prime 알고리즘 슈도코드

 

 

C++로 Pim 구현

#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 100
#define INF 1000L

typedef struct GraphType {
	int n; // 정점의 개수
	int weight[MAX_VERTICES][MAX_VERTICES];
} GraphType;

int selected[MAX_VERTICES];
int distance[MAX_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] && (distance[i] < distance[v])) v = i;
	return (v);
}


void prim(GraphType* g, int s) {
	int i, u, v;
	for (u = 0; u < g->n; u++)
		distance[u] = INF;
	distance[s] = 0;
	for (i = 0; i < g->n; i++) {
		u = get_min_vertex(g->n);
		selected[u] = TRUE;
		if (distance[u] == INF) return;
		printf("정점 %d 추가\n", u);
		for (v = 0; v < g->n; v++)
			if (g->weight[u][v] != INF) {
				if (!selected[v] && g->weight[u][v] < distance[v]) {
					distance[v] = g->weight[u][v];
				}
			}
	}
}


int main(void) {
	GraphType g = { 7,
	{ { 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 } }
	};
	prim(&g, 0);
	return 0;
}

 

 

코드 구조 이해, 결과

2차원 행렬 구조식으로 작동하며 결과는 아래와 같다

 

Prim의 알고리즘 복잡도

  • 주 반복문이 정점의 수 n만큼 반복, 내부 반복문이 n번 반복하므로 Prim의 알고리즘은 O(n^2)의 복잡도를 가진다
  • 희소 그래프 - edge의 수가 얼마 없는 그래프
    • O(elog2e)인 Kruskal 알고리즘이 유리
  • 밀집 그래프 - edge의 수가 최대 간선 수에 가까운 그래프
    • O(n^2)인 Prim 알고리즘이 유리

 


 

최단 경로

  • 네트워크에서 정점 u와 정점 v를 연결하는 경로 중에서 간선들의 가중치 합이 최소가 되는 경로
  • 간선의 가중치는 비용, 거리, 시간 등

 

최단경로 예시

 

가중치 인접행렬

최단경로 알고리즘

  • Dijkstra 알고리즘 : 하나의 시작 정점에서 다른 정점까지의 최단경로 계산
  • Floyd 알고리즘 : 모든 정점에서 다른 모든 정점까지의 최단경로를 계산

 

 

Dijkstra의 최단경로 알고리즘

  • 하나의 시작 정점으로부터 모든 다른 정점까지의 최단 경로 찾는다
  • 집합 S : 시작 정점 v로부터의 최단 경로가 이미 발견된 정점들의 집합
  • distance 배열 : 최단 경로가 알려진 정점들만을 이용한 다른 정점들까지의 최단 경로 길이
  • 매 단계에서 가장 distance 값이 작은 정점을 S에 추가

 

최단경로 구하는 알고리즘

 

 

 

자연어로 알고리즘 설명

 

 

 

S = { 0, 4 }
distance[0] = 0
distance[1] = min(distance[1], distance[4]+weight[4][1] = min(7, 3+2) = 5
distance[2] = min(distance[2], distance[4]+weight[4][2] = 
distance[3] = min(distance[3], distance[4]+weight[4][3] = min(, 3+11) = 
distance[4] = 3
distance[5] = min(distance[5], distance[4]+weight[4][5] = min(10, 3+) = 10
distance[6] = min(distance[6], distance[4]+weight[4][6] = min(, 3+5) = 8 

 

 

 

 

C++로 구현

 

이 그래프에대한 구현

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 100
#define INF 1000000 /* 무한대 (연결이 없는 경우) */

typedef struct GraphType {
	int n; // 정점의 개수
	int weight[MAX_VERTICES][MAX_VERTICES];
} GraphType;

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 print_status(GraphType* g) {
	static int step = 1;
	printf("STEP %d: ", step++);
	printf("distance: ");
	for (int i = 0; i < g->n; i++) {
		if (distance[i] == INF)
			printf(" * ");
		else
			printf("%2d ", distance[i]);
	}
	printf("\n");
	printf(" found: ");
	for (int i = 0; i < g->n; i++)
		printf("%2d ", found[i]);
	printf("\n\n");
}

void shortest_path(GraphType* g, int start) {
	int i, u, w;
	for (i = 0; i < g->n; i++) { /* 초기화 */
		distance[i] = g->weight[start][i];
		found[i] = FALSE;
	}
	found[start] = TRUE; /* 시작 정점 방문 표시 */
	distance[start] = 0;
	for (i = 0; i < g->n - 1; i++) {
		print_status(g);
		u = choose(distance, g->n, found);
		found[u] = TRUE;
		for (w = 0; w < g->n; w++)
			if (!found[w])
				if (distance[u] + g->weight[u][w] < distance[w])
					distance[w] = distance[u] + g->weight[u][w];
	}
}

int main(void) {
	GraphType g = { 7,
	{ { 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 } }
	};
	shortest_path(&g, 0);
	return 0;
}

 

Dijkstra의 최단 경로 알고리즘 복잡도

  • 네트워크에 n개의 정점이 있다면, Dijkstra의 최단경로 알고리즘은 주 반복문을 n번 반복하고 내부 반복문을 2n번 반복하므로 O(n^2)의 시간 복잡도를 가진다

 


Floyd 알고리즘

  • Dijkstra 알고리즘은 정점의 수 만큼 반복수행으로 최단 경로를 찾는다
  • Floyd 알고리즘은 모든 정점사이의 최단 경로를 한번에 찾아준다.
    • 2차원 배열을 이용하여 3중 반복하는 Loop로 구성
    • 인접행렬 weight[i][j] 구성 방법은 동일
    • 2차원 배열을 A라고 하면, 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가지가 나누어진다
      • 정점 k를 거치지 않은 경우
        • A(k)[i][j]는 k보다 큰 정점은 통과하지 않으므로 최단거리는 여전히 A(k-1)[i][j]이다
      • 정점 k를 거치는 경우
        • j에서 k까지의 최단거리 A(k-1)[i][j]에 k에서 j까지의 최단거리 A(k-1)[k][j]를 더한값

 

 

일일이 넘어가면서 배열에 값을 채워나가는 과정을 가지는듯 하다

 

.Floyd의 최단경로 알고리즘 복잡도

  • 네트워크에 n개의 정점이 있다면, Floyd의 최단경로 알고리즘은 3중 반복실행되므로 시간복잡도는 O(n^3) 이다
  • 모든 정점상의 최단경로를 구하려면 Dijkstra의 알고리즘 O(n^2)을 n번 반복해도 되며, 이경우 전체 복잡도는 O(n^3)가 된다
  • Dijkstra와 Floyd 두 알고리즘의 비교에서 보면 시간복잡도는 동일한 결과를 나타내고 있지만 Floyd 알고리즘이 매우 간결한 반복구문을 사용하므로 Dijkstra 알고리즘보다 빨리 모든 정점 간의 최단 경로를 찾을 수 있다.
  •  

위상 정렬

  • 방향 그래프에서 간선 <u, v>가 있다면 정점 u는 정점 v를 실행함
  • 방향 그래프 정점들의 선행 순서를 위배하지 않으면서 모든 정점을 나열
  • 선수 과목은 과목들의 선행 관계 표현

  • 위상 순서
    • (0, 1, 2, 3, 4, 5), (1, 0, 2, 3, 4, 5)
  • (2, 0, 1, 3, 4, 5)는 위상 순서가 아님
    • 2번 정점이 0번 정점 앞에 오기 때문

 

사이클이 없는 방향 그래프에서 적용 가능

 

스택 형식으로 작동됨

 

C++ 구현

// 위상 정렬을 수행한다.
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);
	printf("\n");
	return (i == g->n); // 반환값이 1이면 성공, 0이면 실패
}

int main(void) {
	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);
	// 동적 메모리 반환 코드 생략
	return 0;
}