1. 신장 트리(Spanning Tree)

  • 그래프의 모든 정점을 최소한 적은 간선만 사용해 연결하는 트리를 말한다
  • 정점이 N개라면 N-1개의 간선만 사용해 모든 정점을 연결한 형태를 이루게 된다
  • 신장 트리는 그 구조상 사이클을 포함하지 않는다
  • 만드는 방법 : 한 정점을 기준으로 DFS/BFS를 실행할 때, 탐색한 간선들이 곧 하나의 신장 트리가 된다

 

 

2. 최소 신장 트리(Minimum Spanning Tree, MST)

  • 최소 신장 트리란, 가중치 있는 그래프에서, 신장트리를 이루는 간선의 가중치 총합이 최소가 되는 신장트리를 말한다
  • 모든 정점들을 최소 비용으로 연결하는 것

 

 

3. 최소 신장 트리 구현

  • 크루스칼(KRUSKAL) 알고리즘 (간선 기반)
    https://visualgo.net/en/mst
     
    • Greedy하게 간선을 선택하여 최소 신장 트리를 완성한다
    • 간선의 가중치가 작을수록 선택에 우선순위를 두되,
    • 해당 간선을 선택함으로 인해 트리에 사이클이 발생한다면, 선택하지 않고 넘어간다
    • n-1개의 간선을 선택하면, MST가 완성된 것이다
    • 사이클의 발생 여부 탐색 : 간선의 양쪽 정점 사이에 이미 경로가 존재한다면, 사이클이 발생하게 된다
    • 분리집합을 이용해 간선 선택시 양 정점의 집합을 합쳐, 사이클 발생을 막을 수 있다

 

  • 프림(PRIM) 알고리즘 (정점 기반)
    https://visualgo.net/en/mst
    • 한 정점에서 시작해 최적의 간선만을 선택해 트리를 확장해 나간다
    • 현재 트리에 속한 정점들의 인접 정점 중, 가장 적은 비용으로 진행할 수 있는 정점을 선택, 트리에 포함시킨다
    • n-1개의 정점을 선택하면, MST가 완성된 것이다
    • 다익스트라와 형태가 거의 비슷하지만, 다익스트라는 특정 노드를 기준으로 누적된 거리를 기준으로 한다

 

 

4. JAVA 기초 코드(문제링크)

  • Kruskal 코드
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;

public class Main{
	public static void main(String[] args)throws Exception {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		StringTokenizer st = new StringTokenizer(br.readLine(), " ");
		int n = pint(st.nextToken());
		int m = pint(st.nextToken());

		//간선 리스트..대신 크기가 주어지니 간선배열 사용
		edge[] e = new edge[m];
		for (int i = 0; i < m; i++) {
			e[i]=new edge();
			st = new StringTokenizer(br.readLine(), " ");
			
			e[i].n1=pint(st.nextToken());
			e[i].n2=pint(st.nextToken());
			e[i].cost=pint(st.nextToken());
		}
		//정렬, 작은 간선부터 확인
		Arrays.sort(e);
		//분리집합 초기화
		init(n);
		
		int cnt=0, i=0;//현재 선택한 edge의 갯수, 현재 edge index
		int totalCost=0;//전체cost
		while(true) {
			//union이 정상적으로 수행 됬다면 ( = cycle을 이루지 않는 간선이 tree에 포함되었다면)
			if(union(e[i].n1, e[i].n2)) {
				//cnt, cost 반영
				cnt++;
				totalCost+=e[i].cost;
			}
			if(cnt==n-1)break;//n-1개의 간선을 선택했다면 MST의 완성
			i++;
		}
		//최소 cost의 2개의 마을로 분할하는 법 = MST에서 가장 cost가 큰 edge 하나 자르기
		System.out.println(totalCost-e[i].cost);
	}

	//분리집합
	static int[]parents;
	static void init(int n) {
		parents=new int[n+1];
		for (int i = 0; i <= n; i++) {
			parents[i]=i;
		}
	}
	static int find(int n) {
		if(parents[n]==n)return n;
		return parents[n]=find(parents[n]);
	}
	static boolean union(int n1, int n2) {
		int p1=find(n1);
		int p2=find(n2);
		
		if(p1==p2)return false;
		parents[p1]=p2;
		return true;
	}
	
	static int pint(String s) {
		return Integer.parseInt(s);
	}
	//간선 객체
	static class edge implements Comparable<edge>{
		int n1;
		int n2;
		int cost;
		@Override
		public int compareTo(edge e) {
			return this.cost-e.cost;
		}		
	}
}

 

  • Prim 코드
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

public class Main{
	public static void main(String[] args)throws Exception {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		StringTokenizer st = new StringTokenizer(br.readLine(), " ");
		int n = pint(st.nextToken());
		int m = pint(st.nextToken());

		ArrayList<ArrayList<int[]>>graph = new ArrayList<>();
		for (int i = 0; i <= n; i++) {
			graph.add(new ArrayList<>());
		}
		
		//그래프 입력
		for (int i = 0; i < m; i++) {
			st = new StringTokenizer(br.readLine(), " ");
			int x = pint(st.nextToken());
			int y = pint(st.nextToken());
			int cost = pint(st.nextToken());

			graph.get(x).add(new int[] {y,cost});
			graph.get(y).add(new int[] {x,cost});
		}
		
		int totalCost=0, cnt=0, max=-1;
		//전처리
		boolean[] isV = new boolean[n+1];
		PriorityQueue<int[]>pq = new PriorityQueue<>(new Comparator<int[]>() {
			@Override
			public int compare(int[] o1, int[] o2) {
				// TODO Auto-generated method stub
				return o1[1]-o2[1];
			}
		});
		for (int i = 1; i <= n; i++) {
			pq.offer(new int[] {i,Integer.MAX_VALUE});
		}
		//임의의 시작 노드 설정
		pq.offer(new int[] {1,0});
		
		while(cnt!=n) {//n개의 정점을 선택하면 MST의 완성
			int[] curNode = pq.poll();

			if(isV[curNode[0]])continue;//이미 방문한 node면 무시한다
			
			isV[curNode[0]]=true;
			totalCost+=curNode[1];
			if(max<curNode[1])max=curNode[1];//MST의 가장 큰 edge 저장
			cnt++;
			
			for (int i = 0; i < graph.get(curNode[0]).size(); i++) {
				int[] next = graph.get(curNode[0]).get(i);
                if(!isV[next[0]]) {
					pq.offer(next);
				}
			}
			
		}
		System.out.println(totalCost-max);
		
	}
	
	static int pint(String s) {
		return Integer.parseInt(s);
	}
}

 

https://www.tutorialspoint.com/data_structures_algorithms/graph_data_structure.htm

1. Graph 자료구조

  • Graph란, 정점(node, vertex)과 정점 사이를 잇는 간선(edge, link)으로 구성된 자료구조이다
  • 객체와 객체 사이의 관계를 나타내는데 좋은 자료구조로, 지하철 노선도, 비행기의 항로, 도로 등 다양하게 응용 가능하다

 

2. Graph의 구성

  • 정점 : Graph에 속한 객체 (노선도의 경우 각 역)
  • 간선 : 정점 사이의 관계를 나타내는 선 (두 역 사이에 지하철이 다니면 선이 존재하지만, 다니지 않으면 존재하지 않는다)
  • 차수 : 정점에 연결된 간선의 갯수 (신촌역은 차수가 2이지만, 왕십리역은 차수가 6이다)
  • 입력 차수(진입 차수) : 방향 그래프의 경우, 정점으로 들어오는 간선의 갯수
  • 출력 차수(진출 차수) : 방향 그래프의 경우, 정점에서 나가는 간선의 갯수
  • 사이클 : 어떠한 정점 A에서 간선을 한번씩만 이동했을때, 자기 자신으로 돌아오게 되는 경로, 또는 그러한 그래프
  • 인접 정점 : 어떤 정점 A에서 간선으로 직접 연결된 정점

 

3. Graph의 종류

무방향그래프 / 방향그래프

  • 무방향그래프
    https://mathinsight.org/definition/undirected_graph
    • 간선에 진행 방향이 없는 그래프, 양 방향 이동이 자유롭다
    • A->B 간선은 B->A간선과 동일하다
    • ex) 지하철

 

  • 방향그래프
    https://mathinsight.org/definition/directed_graph
    • 간선에 진행 방향이 있는 그래프, 해당 방향으로만 진행 가능
    • A->B간선만 존재하면, B에서 A로는 이동할 수 없다
    • ex) 일방통행 도로

 

완전그래프 / 부분그래프

  • 완전그래프 
    https://en.wikipedia.org/wiki/Complete_graph
    • 모든 정점 사이에 간선이 연결된 그래프
    • 간선의 갯수는 n(n-1)/2이다 (무방향 기준)

 

  • 부분그래프
    • 완전그래프가 아닌 그래프
    • 간선으로 직접 연결되지 않은 정점들이 존재한다

 

가중치그래프

  • 가중치그래프
    http://www.mathcs.emory.edu/~cheung/Courses/171/Syllabus/11-Graph/weighted.html
    • 간선에 각각 가중치 value가 부여된 그래프
    • ex) 지하철의 요금, 도로의 거리 등

 

4. Graph의 저장 방법

인접 행렬 / 인접 리스트

  • 인접 행렬
    • N개의 정점이 있다면, N*N 2차원 배열으로 연결 정보를 관리한다
    • adj[i][j]는 i->j로 가는 간선의 유/무 혹은 그 갯수를 저장한다
    • 장점 : 특정 노드들 사이의 간선 존재 여부를 O(1)으로 확인 가능하다
    • 간선의 갯수가 많은 밀집 그래프에서 유리하다
    • 단점 : O(N^2) 크기의 배열이 필요하다
  • 인접 리스트
    • 각 정점에 대해 리스트를 만들어, 인접한 정점정보만 저장한다
    • 간선이 없는 정점은 저장하지 않는다
    • 장점 : 실제 간선이 존재하는 경우만 저장하므로, 공간 복잡도가 낮아진다
    • 간선의 갯수가 적은 희소 그래프에서 유리하다
    • 단점 : 특정 노드들 사이의 간선 존재 여부를 확인하려면, 리스트를 탐색해야한다

간선 리스트

  • 간선 리스트
    • 정점이 아닌, 모든 간선의 리스트를 저장한다

 

5. Graph 연관 알고리즘

Graph 탐색 (링크)

  • DFS
  • BFS

최소신장트리(Minimum Spanning Tree, MST) (링크)

  • 그래프의 모든 정점을 잇는 간선의 가중치의 합이 최소가 되는 트리
  • 크루스칼(KRUSKAL) 알고리즘
  • 프림(PRIM) 알고리즘

최단경로 탐색

  • 특정 정점에서 다른 정점들까지 가는 최소 가중치의 경로를 탐색
  • 다익스트라(Dijkstra) 알고리즘
  • 벨만-포드 알고리즘
  • 플로이드-와샬 알고리즘

그래프부터 알아보기

https://vivadifferences.com/difference-between-dfs-and-bfs-in-artificial-intelligence/

 

1. 너비 우선 탐색 (BFS)

  • 현재 탐색 정점과 인접한 노드들을 우선 탐색한다
  • A정점에서 시작했다면, A-B를 탐색한 후 B의 인접노드인 D/E가 아닌 C를 우선 탐색한다
  • 시작 노드로부터 거리가 1인 모든 정점 탐색 - 거리가 2인 모든 정점 탐색 - 거리가 3인....
  • 최단거리 찾기, A-B 사이 거리 찾기 등에 활용 가능하다

 

  • FIFO 자료구조인 큐를 사용하여 구현한다
  • A탐색 - B/C를 offer - B pop, 탐색 - D/E를 offer - C pop, 탐색 - .....

 

 

2. 깊이 우선 탐색 (DFS)

  • 더 이상 깊이 갈수 없을때까지 우선 탐색하며, 탐색할 노드가 없으면 부모 노드로 돌아온다
  • A정점에서 시작했다면, A-B를 탐색한 후 B의 인접노드인 D - D의 인접노드인 H ... 순서로 탐색한다
  • 한 경로로 진행했을때 가능한 모든 경우의 수를 탐색한 후 다른 경로로 진행
  • 경로별 특징이 나타나는 문제에 활용 가능하다

 

  • LIFO 자료구조인 스택을 사용하거나, 재귀를 사용하여 구현한다
  • A탐색 - C/B를 push - B pop, 탐색 - E/D를 push - D pop, 탐색 - .....
  • A탐색 - B로 재귀 - B탐색 - D로 재귀 - .....

 

 

3. 각 방식의 특징

  • DFS BFS 다 방문한 노드 / 방문하지 않는 노드를 구분할 수단이 필요하다
  • 4번의 예시 코드는 별도의 Boolean 배열을 이용했으며, tree와 같은 특수한 형태의 graph는 부모-자식 관계를 이용하기도 한다
  • 재귀를 사용한 DFS는 노드 방문 시점에서 방문처리를 해도 무방하다.
  • 스택을 사용한 DFS / 큐를 사용하는 BFS의 경우 노드 방문 시점에서 방문처리를 하면 스택/큐에 노드가 중복 삽입된다

https://math.stackexchange.com/questions/1548708/finding-dfs-in-undirected-graph

  • ex)A에서 DFS를 한다면 B/D를 스택에 넣게되는데, B를 pop해 DFS를 할 때 D는 방문하지 않은 상태로 중복 삽입된다
  • 때문에 실제 방문하는 시점이 아닌, 방문 예고 시점, 즉 스택/큐에 넣는 시점에서 방문처리를 해야 한다

 

  • 모든 노드를 탐색하는 경우 DFS와 BFS의 차이는 거의 없지만, 다른 문제에서 응용하는 경우 DFS보단 BFS를 자주 사용한다
    • DFS의 경우 해당 경로에 해답이 없고 깊이가 무한일 경우 탈출할 수 없게 된다
    • 또한 다양한 해가 있을 경우 DFS의 경우 최초 발견한 해와 최소 깊이의 해가 같다고 말할 수 없지만,
    • BFS의 경우 최초 발견한 해가 최소 깊이의 해가 된다. (최소 거리 문제에서 사용하는 이유)

 

  • BFS는 특정 노드에 진입한 경로를 확인할 수 없지만
  • (ex. 위 그래프에서 BFS로 e노드에 진입시, 그게 a-b-e인지 a-d-e인지 알 수 없다)
  • DFS는 어떤 경로로 해당 노드에 진입하는지 확인하는게 용이하다
  • (ex. DFS를 진행하며 거쳐온 node를 표시해 두면, 지금까지 지나온 노드 정보를 확인 가능하다)

 

 

4. JAVA 기초 코드 (DFS와BFS)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
	public static void main(String[] args)throws Exception {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		sb = new StringBuilder();
		graph = new ArrayList<ArrayList<Integer>>();
		int N = Integer.parseInt(st.nextToken());
		int M = Integer.parseInt(st.nextToken());
		int V = Integer.parseInt(st.nextToken());
		
		for (int i = 0; i <= N; i++) {
			graph.add(new ArrayList<Integer>());
		}
		//graph 입력
		for (int i = 0; i < M; i++) {
			st = new StringTokenizer(br.readLine());
			int node1 = Integer.parseInt(st.nextToken());
			int node2 = Integer.parseInt(st.nextToken());
			
			graph.get(node1).add(node2);
			graph.get(node2).add(node1);
		}
		//graph 정렬
		for (int i = 0; i <= N; i++) {
			graph.get(i).sort(null);
		}
		
		isChecked = new boolean[N+1];
		DFS(V);
		sb.append("\n");
		 
		isChecked = new boolean[N+1];
		isChecked[V]=true;
		BFS(V);
		sb.append("\n");
		
		System.out.println(sb);
		
	}
	static StringBuilder sb;
	static ArrayList<ArrayList<Integer>> graph;
	static boolean[] isChecked;
	
	static void DFS(int i) {
		sb.append(i).append(" ");
		isChecked[i]=true;//방문처리
		for (int j = 0, len = graph.get(i).size(); j < len; j++) {
			int temp = graph.get(i).get(j);
			if(!isChecked[temp]) {//이미 방문했던 노드는 재방문하지 않는다
				DFS(temp);//재귀로 자식노드 우선처리
			}
		}
	}
	
	static void BFS(int i) {
		Queue<Integer> q = new LinkedList<Integer>();
		q.offer(i);
		
		while(!q.isEmpty()) {
			int next = q.poll();
			sb.append(next).append(" ");
			for (int j = 0, len = graph.get(next).size(); j < len; j++) {
				int temp = graph.get(next).get(j);
				if(!isChecked[temp]) {//이미 방문했던 노드는 재방문하지 않는다
					isChecked[temp]=true;//방문처리
					q.offer(temp);
				}
			}
		}
	}
}

'알고리즘 공부' 카테고리의 다른 글

최소 신장 트리(MST)에 대해  (0) 2021.05.31
그래프(Graph)에 대해  (0) 2021.05.30

+ Recent posts