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

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

SK_MOUSE 2020. 11. 25. 17:21
반응형

先요약

  • 일반적인 그래프를 구성할때는 싸이클이 있는지를 확인한다 : 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

 

반응형