코딩테스트/알고리즘 개념정리

그래프 이론(싸이클, 최소신장트리, 위상 정렬)

SK_MOUSE 2020. 11. 25. 17:21
先요약
신장 트리란? Spanning Tree
위상정렬이란? Topological Sort Algorithm
진입차수

先요약

  • 일반적인 그래프를 구성할때는 싸이클이 있는지를 확인한다 : findParent 메소드 이용
  • 두개의 노드를 간선으로 잇는 작업은 unionParent 메소드를 이용한다.
  • 신장트리는 싸이클이 없어야하며, 모든 노드가 연결되어있어야한다 : 크루스칼 알고리즘으로 만든다.
  • 위상정렬은 싸이클이 없고 방향그래프(DAG)인 상황에, 방향성에 거스르지 않도록 순서대로 나열하는 것.

그래프의 기본적인 이론에 대하여 알아보겠다.

아래의 메소드는 그래프 문제를 풀때 구현해놓으면 좋은 메소드이다.

 

1. findParent(int x) 메소드 

: 루트노드가 아니라면, 해당 루트 노드를 찾을때까지 재귀적으로 findParent메소드 재귀호출.

3번노드의 부모노드는 2번노드(루트노드는 1번노드)

ex) findParent(3번노드) -> 재귀 findParent(2번노드) -> 1번노드==부모노드 return.

 

  • 비개선 findParent(int x)  : return findParent(parent[x]);
  • 개선된 findParent(int x) return parent[x] = findParent(parent[x]);

 

2. unionParent(int a, int b) 메소드

: 두 원소가 속한 집합 합치는 작업. 이때, 번호가 큰 노드의 parent 값을 변경해준다.

import java.util.*; public class Main { ​​​​// 노드의 개수(V)와 간선(Union 연산)의 개수(E) ​​​​// 노드의 개수는 최대 100,000개라고 가정 ​​​​public static int v, e; ​​​​public static int[] parent = new int[100001]; // 부모 테이블 초기화하기 ​​​​// 특정 원소가 속한 집합을 찾기 ​​​​public static int findParent(int x) { ​​​​​​​​// 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출 ​​​​​​​​if (x == parent[x]) return x; ​​​​​​​​ ​​​​​​​​//개선된 경로 압축!! parent[x]에 재귀된 값을 업데이트 한다. ​​​​​​​​return parent[x] = findParent(parent[x]);// ​​​​} ​​​​// 두 원소가 속한 집합을 합치기 ​​​​public static void unionParent(int a, int b) { ​​​​​​​​a = findParent(a); ​​​​​​​​b = findParent(b); ​​​​​​​​if (a < b) parent[b] = a; ​​​​​​​​else parent[a] = b; ​​​​} ​​​​public static void main(String[] args) { ​​​​​​​​Scanner sc = new Scanner(System.in); ​​​​​​​​v = sc.nextInt(); ​​​​​​​​e = sc.nextInt(); ​​​​​​​​// 부모 테이블상에서, 부모를 자기 자신으로 초기화 ​​​​​​​​for (int i = 1; i <= v; i++) { ​​​​​​​​​​​​parent[i] = i; ​​​​​​​​} ​​​​​​​​// Union 연산을 각각 수행 ​​​​​​​​for (int i = 0; i < e; i++) { ​​​​​​​​​​​​int a = sc.nextInt(); ​​​​​​​​​​​​int b = sc.nextInt(); ​​​​​​​​​​​​unionParent(a, b); ​​​​​​​​} /* // 각 원소가 속한 집합 출력하기 System.out.print("각 원소가 속한 집합: "); for (int i = 1; i <= v; i++) { System.out.print(findParent(i) + " "); } System.out.println(); // 부모 테이블 내용 출력하기 System.out.print("부모 테이블: "); for (int i = 1; i <= v; i++) { System.out.print(parent[i] + " "); } System.out.println(); */ ​​​​} }

 

3. 싸이클 확인하기 : boolean cycle = false;

위와 같이 cycle 변수를 선언 및 초기화한다.

무방향 그래프 내에서의 사이클을 판별할때 서로소 집합을 활용하여 사이클을 판별한다.

참고로 방향그래프에서의 사이클 여부는 DFS를 이용하여 판별 가능하다.(code0xff.tistory.com/39)

 

사이클 찾기: DFS

유향 그래프에서 사이클이 있는지 판단하고, 사이클에 포함되는 노드의 개수가 몇 개인지 세는 알고리즘으로 백준 알고리즘 저지의 9466번: 텀 프로젝트 문제가 이에 해당한다. 9466번: 텀 프로젝

code0xff.tistory.com

 

아래는 무방향 그래프에서의 싸이클을 찾을때 main문에서 

findParent값이 동일하면 cycle로 판별하여 cycle =true; break;을 반환한다.

​​​​public static void main(String[] args) { ​​​​​​​​Scanner sc = new Scanner(System.in); ​​​​​​​​v = sc.nextInt(); ​​​​​​​​e = sc.nextInt(); ​​​​​​​​// 부모 테이블상에서, 부모를 자기 자신으로 초기화 ​​​​​​​​for (int i = 1; i <= v; i++) { ​​​​​​​​​​​​parent[i] = i; ​​​​​​​​} ​​​​​​​​boolean cycle = false; // 사이클 발생 여부 ​​​​​​​​for (int i = 0; i < e; i++) { ​​​​​​​​​​​​int a = sc.nextInt(); ​​​​​​​​​​​​int b = sc.nextInt(); ​​​​​​​​​​​​// 사이클이 발생한 경우 종료 ​​​​​​​​​​​​if (findParent(a) == findParent(b)) { ​​​​​​​​​​​​​​​​cycle = true; ​​​​​​​​​​​​​​​​break; ​​​​​​​​​​​​} ​​​​​​​​​​​​// 사이클이 발생하지 않았다면 합집합(Union) 연산 수행 ​​​​​​​​​​​​else { ​​​​​​​​​​​​​​​​unionParent(a, b); ​​​​​​​​​​​​} ​​​​​​​​} ​​​​​​​​if (cycle) { ​​​​​​​​​​​​System.out.println("사이클이 발생했습니다."); ​​​​​​​​} ​​​​​​​​else { ​​​​​​​​​​​​System.out.println("사이클이 발생하지 않았습니다."); ​​​​​​​​} ​​​​} }

신장 트리란? Spanning Tree

신장트리 설명

1. 사이클이 존재하지 않음

2. 모든 노드가 연결 되어 있어야한다.

=> 몇개의 간선이 없어도 되는지! => 최소한의 비용으로 구성하는 신장트리를 찾는 문제

=> 최소신장트리Minimum Spanning Tree(모든것이 최소비용으로 연결된 최소사이클이 없는 그래프 구조)

=> 대표적인 최소신장트리 구현 알고리즘 : 크루스칼 알고리즘(Kruskal's Algorithm)

 

크루스칼 알고리즘

 

class Edge implements Comparable<Edge> 

=> get메소드 Distance, NodeA, NodeB

=> compareTo 메소드 : distance가 작은것이 더 높은 우선순위를 가짐.

 

크루스칼 알고리즘 전체 코드는 아래와 같다.

import java.util.*; class Edge implements Comparable<Edge> { ​​​​private int distance; ​​​​private int nodeA; ​​​​private int nodeB; ​​​​public Edge(int distance, int nodeA, int nodeB) { ​​​​​​​​this.distance = distance; ​​​​​​​​this.nodeA = nodeA; ​​​​​​​​this.nodeB = nodeB; ​​​​} ​​​​public int getDistance() { ​​​​​​​​return this.distance; ​​​​} ​​​​public int getNodeA() { ​​​​​​​​return this.nodeA; ​​​​} ​​​​public int getNodeB() { ​​​​​​​​return this.nodeB; ​​​​} ​​​​// 거리(비용)가 짧은 것이 높은 우선순위를 가지도록 설정 ​​​​@Override ​​​​public int compareTo(Edge other) { ​​​​​​​​if (this.distance < other.distance) { ​​​​​​​​​​​​return -1; ​​​​​​​​} ​​​​​​​​return 1; ​​​​} } public class Main { ​​​​// 노드의 개수(V)와 간선(Union 연산)의 개수(E) ​​​​// 노드의 개수는 최대 100,000개라고 가정 ​​​​public static int v, e; ​​​​public static int[] parent = new int[100001]; // 부모 테이블 초기화하기 ​​​​// 모든 간선을 담을 리스트와, 최종 비용을 담을 변수 ​​​​public static ArrayList<Edge> edges = new ArrayList<>(); ​​​​public static int result = 0; ​​​​// 특정 원소가 속한 집합을 찾기 ​​​​public static int findParent(int x) { ​​​​​​​​// 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출 ​​​​​​​​if (x == parent[x]) return x; ​​​​​​​​return parent[x] = findParent(parent[x]); ​​​​} ​​​​// 두 원소가 속한 집합을 합치기 ​​​​public static void unionParent(int a, int b) { ​​​​​​​​a = findParent(a); ​​​​​​​​b = findParent(b); ​​​​​​​​if (a < b) parent[b] = a; ​​​​​​​​else parent[a] = b; ​​​​} ​​​​public static void main(String[] args) { ​​​​​​​​Scanner sc = new Scanner(System.in); ​​​​​​​​v = sc.nextInt(); ​​​​​​​​e = sc.nextInt(); ​​​​​​​​// 부모 테이블상에서, 부모를 자기 자신으로 초기화 ​​​​​​​​for (int i = 1; i <= v; i++) { ​​​​​​​​​​​​parent[i] = i; ​​​​​​​​} ​​​​​​​​// 모든 간선에 대한 정보를 입력 받기 ​​​​​​​​for (int i = 0; i < e; i++) { ​​​​​​​​​​​​int a = sc.nextInt(); ​​​​​​​​​​​​int b = sc.nextInt(); ​​​​​​​​​​​​int cost = sc.nextInt(); ​​​​​​​​​​​​edges.add(new Edge(cost, a, b)); ​​​​​​​​} ​​​​​​​​// 간선을 비용순으로 정렬 ​​​​​​​​Collections.sort(edges); ​​​​​​​​// 간선을 하나씩 확인하며 ​​​​​​​​for (int i = 0; i < edges.size(); i++) { ​​​​​​​​​​​​int cost = edges.get(i).getDistance(); ​​​​​​​​​​​​int a = edges.get(i).getNodeA(); ​​​​​​​​​​​​int b = edges.get(i).getNodeB(); ​​​​​​​​​​​​// 사이클이 발생하지 않는 경우에만 집합에 포함 ​​​​​​​​​​​​if (findParent(a) != findParent(b)) { ​​​​​​​​​​​​​​​​unionParent(a, b); ​​​​​​​​​​​​​​​​result += cost; ​​​​​​​​​​​​} ​​​​​​​​} ​​​​​​​​System.out.println(result); ​​​​} }

간선을 확인하며, cost 및 a, b 사이 사이클이 발생안하면 result에 cost값을 더해

"최소비용(result)"에 값을 추가한다.

 

p.s. 겁먹지말고 원리를 순서대로 차근차근 읽어보자.

 

 


위상정렬이란? Topological Sort Algorithm

: 사이클이 없는 방향 그래프의 모든 노드를 방향성에 거스르지 않도록 순서대로 나열하는 것

 

아래의 예시를 보겠다.

 

위상정렬의 예시.

고급 알고리즘을 듣고 알고리즘을 듣는 것은 불가능하다.

따라서 <자료구조-알고리즘-고급알고리즘>

혹은, <자료구조-고급알고리즘>

수업은 이러한 방식으로 수업을 들어야한다.

but, 모두 듣기위한 방법은 위의 한가지 방법뿐이다.

 

위상정렬은 큐를 이용하여 동작한다.

 

위상정렬은 사이클이 없는 그래프에서 가능하다.

"진입차수" 란?

: 해당 노드로 들어오는 다른 노드의 개수.

 

첫번째 단계. 1번노드에서 시작.

1번 노드에서 가는 2번/5번 노드를 큐에 넣고 해당 노드들의 진입차수를 -1씩 한다.

 

두번째 단계. 큐에서 2번노드를 꺼내서 진행한다.

이러한 방식으로 큐에서 차례대로 꺼내가면서 진행한다.

위상정렬 예시 결과


코드출처 : github.com/ndb796/python-for-coding-test/blob/master/10/6.java

 

ndb796/python-for-coding-test

[한빛미디어] "이것이 취업을 위한 코딩 테스트다 with 파이썬" 전체 소스코드 저장소입니다. - ndb796/python-for-coding-test

github.com

개념출처 : 나동빈 유튜브 www.youtube.com/watch?v=aOhhNFTIeFI&list=PLRx0vPvlEmdAghTr5mXQxGpHjWqSz0dgC&index=8

 

반응형