반응형

* 핵심

숫자를 보는게 아닌 각 입력값의 idx를 순서를 구분해서 뽑는 과정이다.

1234~1432, 2134~2431 ... 순서를 전부 확인하는 Backtracking 코드를 구현한다.

* 문제

문제

상근이는 카드 n(4 ≤ n ≤ 10)장을 바닥에 나란히 놓고 놀고있다. 각 카드에는 1이상 99이하의 정수가 적혀져 있다. 상근이는 이 카드 중에서 k(2 ≤ k ≤ 4)장을 선택하고, 가로로 나란히 정수를 만들기로 했다. 상근이가 만들 수 있는 정수는 모두 몇 가지일까?

예를 들어, 카드가 5장 있고, 카드에 쓰여 있는 수가 1, 2, 3, 13, 21라고 하자. 여기서 3장을 선택해서 정수를 만들려고 한다. 2, 1, 13을 순서대로 나열하면 정수 2113을 만들 수 있다. 또, 21, 1, 3을 순서대로 나열하면 2113을 만들 수 있다. 이렇게 한 정수를 만드는 조합이 여러 가지 일 수 있다.

n장의 카드에 적힌 숫자가 주어졌을 때, 그 중에서 k개를 선택해서 만들 수 있는 정수의 개수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 n이, 둘째 줄에 k가 주어진다. 셋째 줄부터 n개 줄에는 카드에 적혀있는 수가 주어진다.

출력

첫째 줄에 상근이가 만들 수 있는 정수의 개수를 출력한다.

* 소스 코드

import java.io.*;
import java.util.*;
public class Main {
public static StringTokenizer stk;
public static StringBuffer sb = new StringBuffer();
public static HashSet<String> hs = new HashSet<>();
public static boolean[] visited;
public static String[] card;
public static int cnt = 0, n, k;
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
n = Integer.parseInt(br.readLine());
k = Integer.parseInt(br.readLine());
card = new String[n + 1];
visited = new boolean[n + 1];
for (int i = 1; i <= n; i++) {
card[i] = br.readLine();
}
dfs(k, 1, "");
System.out.println(hs.size());
}
public static void dfs(int k, int idx, String s) {
if (k == 0) {
hs.add(s);
return;
}
if (idx > n) {
return;
}
for (int i = 1; i <= n; i++) {
if (!visited[i]) {
visited[i] = true;
dfs(k - 1, i, s + card[i]);
visited[i] = false;
}
}
}
}


반응형
반응형

* 핵심

BinaryTree Class를 만들어서 left, right에 자식 이진 트리를 만들어 붙여 트리를 만든다.

* 문제

문제

이진 검색 트리는 다음과 같은 세 가지 조건을 만족하는 이진 트리이다.

  • 노드의 왼쪽 서브트리에 있는 모든 노드의 키는 노드의 키보다 작다.
  • 노드의 오른쪽 서브트리에 있는 모든 노드의 키는 노드의 키보다 크다.
  • 왼쪽, 오른쪽 서브트리도 이진 검색 트리이다.

전위 순회 (루트-왼쪽-오른쪽)은 루트를 방문하고, 왼쪽 서브트리, 오른쪽 서브 트리를 순서대로 방문하면서 노드의 키를 출력한다. 후위 순회 (왼쪽-오른쪽-루트)는 왼쪽 서브트리, 오른쪽 서브트리, 루트 노드 순서대로 키를 출력한다. 예를 들어, 위의 이진 검색 트리의 전위 순회 결과는 50 30 24 5 28 45 98 52 60 이고, 후위 순회 결과는 5 28 24 45 30 60 52 98 50 이다.

이진 검색 트리를 전위 순회한 결과가 주어졌을 때, 이 트리를 후위 순회한 결과를 구하는 프로그램을 작성하시오.

입력

트리를 전위 순회한 결과가 주어진다. 노드에 들어있는 키의 값은 106보다 작은 양의 정수이다. 모든 값은 한 줄에 하나씩 주어지며, 노드의 수는 10,000개 이하이다. 같은 키를 가지는 노드는 없다.

출력

입력으로 주어진 이진 검색 트리를 후위 순회한 결과를 한 줄에 하나씩 출력한다.

* 소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.io.*;
import java.util.*;
 
public class Main {
    public static StringBuilder sb = new StringBuilder();
 
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BinaryTree tree = new BinaryTree(Integer.parseInt(br.readLine()));
        String s = "";
        while ((s = br.readLine()) != null && s.length() != 0) {    //EOF까지 입력받음
            tree = tree.addTree(tree, Integer.parseInt(s));
        }
        postorder(tree);
        System.out.println(sb);
    }
 
    public static void postorder(BinaryTree tree) {
        if (tree.left != null) postorder(tree.left);
        if (tree.right != null) postorder(tree.right);
        sb.append(tree.data + "\n");
    }
}
 
class BinaryTree {
    int data;
    BinaryTree left;
    BinaryTree right;
 
    public BinaryTree(int data) {
        this.data = data;
        this.left = null;
        this.right = null;
    }
 
    public BinaryTree addTree(BinaryTree tree, int val) {
        BinaryTree curTree = null;
        if (tree == nullreturn new BinaryTree(val);
        if (tree.data > val) {
            curTree = addTree(tree.left, val);  //새로운 하위 이진 트리 생성
            tree.left = curTree;                //부모 노드의 왼쪽에 붙인다
        } else if (tree.data < val) {
            curTree = addTree(tree.right, val); //새로운 하위 이진 트리 생성
            tree.right = curTree;               //부모 노드의 오른쪽에 붙인다
        }
        return tree;                            //새로 붙인 트리 return
    }
}
 
cs


반응형
반응형

* 문제 핵심

세그먼트 트리를 이용하여 sum-tree 구현 및 값 변경

* 문제

문제

모두 알다시피 동호는 커피숍의 마담이다. (마담이 무엇인지는 본인에게 물어보도록 하자.)

어느날 커피숍의 손님 A씨가 동호에게 게임을 하자고 했다.

그 게임은 다음과 같은 규칙을 갖는다.

N개의 정수가 있으면, 동호는 다음과 같이 말한다. “3~7번째 수의 합은 무엇이죠?” 그러면 상대방은 “그 답은 000입니다. 그리고 8번째 수를 2로 고치도록 하죠” 그러면 동호는 “네 알겠습니다.”라고 한 뒤에 다시 상대방이 동호가 했던 것처럼 “8~9번째 수의 합은 무엇이죠?”라고 묻게된다. 이 것을 번갈아 가면서 반복하는 게임이다.

당신은 이 게임의 심판 역을 맡았다. 요컨대, 질문에 대한 답들을 미리 알아야 한다는 것이다.

당신의 머리가 출중하다면 10만개 가량 되는 정수와 10만턴 정도 되는 게임을 기억할 수 있을 것이다. 몇판 이 게임을 즐기던 동호는 많은 사람들이 이 게임을 하기를 바라게 되었고, 당신에게 심판 프로그램을 구현해달라고 요청했다.

입력

첫째 줄에 수의 개수 N과 턴의 개수 Q가 주어진다.(1 ≤ N, Q ≤ 100,000) 둘째 줄에는 처음 배열에 들어가 있는 정수 N개가 주어진다. 세 번째 줄에서 Q+2번째 줄까지는 x y a b의 형식으로 x~y까지의 합을 구하여라, a번째 수를 b로 바꾸어라 라는 뜻의 데이터가 주어진다.

입력되는 모든 수는 32비트 부호있는 정수이다.

출력

한 턴마다 구한 합을 한 줄마다 한 개씩 출력한다.

* 소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import java.io.*;
import java.util.*;
 
public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer stk = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(stk.nextToken());
        int q = Integer.parseInt(stk.nextToken());
        stk = new StringTokenizer(br.readLine());
        SegmentTree st = new SegmentTree(n);
        for (int i = 1; i <= n; i++) {
            int v = Integer.parseInt(stk.nextToken());
            st.update(i, v);
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < q; i++) {
            stk = new StringTokenizer(br.readLine());
            int x = Integer.parseInt(stk.nextToken());
            int y = Integer.parseInt(stk.nextToken());
            int max = Math.max(x, y);
            int min = Math.min(x, y);
            x = min;
            y = max;
            int a = Integer.parseInt(stk.nextToken());
            int b = Integer.parseInt(stk.nextToken());
            sb.append(st.getSum(x, y) + "\n");
            st.update(a, b);
        }
        System.out.println(sb);
    }
}
 
class SegmentTree {
    long[] tree;
    int s;
 
    public SegmentTree(int n) {
        for (s = 1; s < n; s *= 2)
            ;
        tree = new long[s * 2];
        for (int i = 1; i < s + s; i++)
            tree[i] = 0l;
    }
 
    void update(int x, long v) {
        int l = x + s - 1;
        tree[l] = v;
        l /= 2;
        while (l >= 1) {
            tree[l] = tree[l * 2+ tree[l * 2 + 1];
            l /= 2;
        }
    }
 
    long getMin(int Left, int Right) {
        int l = Left + s - 1, r = Right + s - 1;
        long rval = Long.MAX_VALUE;
        while (l <= r) {
            if (l % 2 == 0)
                l /= 2;
            else {
                rval = Math.min(rval, tree[l]);
                l = (l / 2+ 1;
            }
            if (r % 2 == 1)
                r /= 2;
            else {
                rval = Math.min(rval, tree[r]);
                r = (r / 2- 1;
            }
        }
        return rval;
    }
 
    long getSum(int Left, int Right) {
        int l = Left + s - 1, r = Right + s - 1;
        long rval = 0;
        while (l <= r) {
            if (l % 2 == 0)
                l /= 2;
            else {
                rval += tree[l];
                l = (l / 2+ 1;
            }
            if (r % 2 == 1)
                r /= 2;
            else {
                rval += tree[r];
                r = (r / 2- 1;
            }
        }
        return rval;
    }
}
cs


반응형
반응형

* 문제 핵심

세그먼트 트리를 이용하여 sum-tree 구현 및 값 변경

* 문제

문제

어떤 N개의 수가 주어져 있다. 그런데 중간에 수의 변경이 빈번히 일어나고 그 중간에 어떤 부분의 합을 구하려 한다. 만약에 1,2,3,4,5 라는 수가 있고, 3번째 수를 6으로 바꾸고 2번째부터 5번째까지 합을 구하라고 한다면 17을 출력하면 되는 것이다. 그리고 그 상태에서 다섯 번째 수를 2로 바꾸고 3번째부터 5번째까지 합을 구하라고 한다면 12가 될 것이다.

입력

첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000,000)과 M(1 ≤ M ≤ 10,000), K(1 ≤ K ≤ 10,000) 가 주어진다. M은 수의 변경이 일어나는 회수이고, K는 구간의 합을 구하는 회수이다. 그리고 둘째 줄부터 N+1번째 줄까지 N개의 수가 주어진다. 그리고 N+2번째 줄부터 N+M+K+1번째 줄까지 세 개의 정수 a, b, c가 주어지는데, a가 1인 경우 b번째 수를 c로 바꾸고 a가 2인 경우에는 b번째 수부터 c번째 수까지의 합을 구하여 출력하면 된다.

a가 1인 경우 c는 long long 범위를 넘지 않는다.

출력

첫째 줄부터 K줄에 걸쳐 구한 구간의 합을 출력한다. 단, 정답은 long long 범위를 넘지 않는다.

*  소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import java.io.*;
import java.util.*;
 
public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer stk = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(stk.nextToken());
        int m = Integer.parseInt(stk.nextToken());
        int k = Integer.parseInt(stk.nextToken());
        SegmentTree st = new SegmentTree(n);
        for (int i = 1; i <= n; i++) {
            long v = Long.parseLong(br.readLine());
            st.update(i, v);
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < m + k; i++) {
            stk = new StringTokenizer(br.readLine());
            int type = Integer.parseInt(stk.nextToken());
            if (type == 1) {
                st.update(Integer.parseInt(stk.nextToken()), Long.parseLong(stk.nextToken()));
            } else {
                sb.append(st.getSum(Integer.parseInt(stk.nextToken()), Integer.parseInt(stk.nextToken()))).append('\n');
            }
        }
        System.out.print(sb);
    }
 
}
 
class SegmentTree {
    long[] tree;
    int s;
 
    public SegmentTree(int n) {
        for (s = 1; s < n; s *= 2)
            ;
        tree = new long[s * 2];
        for (int i = 1; i < s + s; i++)
            tree[i] = 0l;
    }
 
    void update(int x, long v) {
        int l = x + s - 1;
        tree[l] = v;
        l /= 2;
        while (l >= 1) {
            tree[l] = tree[l * 2+ tree[l * 2 + 1];
            l /= 2;
        }
    }
 
    long getMin(int Left, int Right) {
        int l = Left + s - 1, r = Right + s - 1;
        long rval = Long.MAX_VALUE;
        while (l <= r) {
            if (l % 2 == 0)
                l /= 2;
            else {
                rval = Math.min(rval, tree[l]);
                l = (l / 2+ 1;
            }
            if (r % 2 == 1)
                r /= 2;
            else {
                rval = Math.min(rval, tree[r]);
                r = (r / 2- 1;
            }
        }
        return rval;
    }
 
    long getSum(int Left, int Right) {
        int l = Left + s - 1, r = Right + s - 1;
        long rval = 0;
        while (l <= r) {
            if (l % 2 == 0)
                l /= 2;
            else {
                rval += tree[l];
                l = (l / 2+ 1;
            }
            if (r % 2 == 1)
                r /= 2;
            else {
                rval += tree[r];
                r = (r / 2- 1;
            }
        }
        return rval;
    }
}
cs


반응형
반응형

* 세그먼트 트리

배열이 주어졌을 때 해당 배열을 완전 이진 트리 Leaf Node에 넣고 Leaf Node의 부모 노드에 합/곱/Max/Min을 올려가는 방식이다.

A 배열의 부분 합을 구할때 배열의 값이 변하거나 테스트 케이스가 많을 경우(쿼리가 많을 경우) 시간 복잡도가 한 쿼리당 O(logN)으로 계산할 수 있다.

* 세그먼트 트리에서 값을 저장하는 방식

부모 노드가 n 번째 노드라면 n*2, n*2+1 번째 노드를 자식노드로 가진다는 특성을 이용하여 1차원 배열에 저장한다.

처음 입력받는 값들은 Leaf Node이기 때문에 뒤쪽 배열에 값을 저장하고,

자식노드가 n 번째 노드라면 n/2 번째 부모 노드에 트리방식에 따라 합/곱/Max/Min을 저장한다. 이 때의 시간복잡도는 O(logN)이다.

Leaf Node의 값을 변경하게 된다면 트리방식에 따라 부모 노드들을 수정하면 된다. 이때 시간 복잡도는 O(logN)이 된다.


* 소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class SegmentTree {
    long[] tree;
    int s;
 
    public SegmentTree(int n) {
        for (s = 1; s < n; s *= 2) ;
        tree = new long[s * 2];
        for (int i = 1; i < s + s; i++)
            tree[i] = 0l;
    }
 
    void update(int x, long v) {
        int l = x + s - 1;
        tree[l] = v;
        l /= 2;
        while (l >= 1) {
            tree[l] = tree[l * 2+ tree[l * 2 + 1];
            l /= 2;
        }
    }
 
    long getMin(int Left, int Right) {
        int l = Left + s - 1, r = Right + s - 1;
        long rval = Long.MAX_VALUE;
        while (l <= r) {
            if (l % 2 == 0) l /= 2;
            else {
                rval = Math.min(rval, tree[l]);
                l = (l / 2+ 1;
            }
            if (r % 2 == 1) r /= 2;
            else {
                rval = Math.min(rval, tree[r]);
                r = (r / 2- 1;
            }
        }
        return rval;
    }
 
    long getSum(int Left, int Right) {
        int l = Left + s - 1, r = Right + s - 1;
        long rval = 0;
        while (l <= r) {
            if (l % 2 == 0) l /= 2;
            else {
                rval += tree[l];
                l = (l / 2+ 1;
            }
            if (r % 2 == 1) r /= 2;
            else {
                rval += tree[r];
                r = (r / 2- 1;
            }
        }
        return rval;
    }
}
cs

출처 : https://www.codeground.org/


* 참고 문제

백준/2042 :: 구간 합 구하기

백준/1275 :: 커피숍2

반응형
반응형

* 무분별한 if-else 줄이는 코딩 방법(Callback Hell 탈출)

if-else는 프로그래밍을 하면서 필수적으로 사용된다.

하지만 무분별한 if-else문 남용은 가독성을 떨어트려 이해하기 어려운 코드가 된다.

특히, JS에서 많이 사용되는 Callback Hell이라고 불리는 문제를 해결하는 방법을 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Callback Hell
void getConnection() {
    Server server = getServer();
    if (server != null) {
        Client client = server.getClient();
        if (client != null) {
            Connection con = con.getConnection();
            if (con != null) {
                // 실제 처리할 로직
            }
        }
    }
}
 
cs

위 코드는 Callback은 아니지만, 무분별한 if문 남발로 가독성이 떨어지는 코드이다.

이를 해결할 수 있는 방법은 if문에서 예외 처리해야할 부분을 잡아서 처리하는 방법이 좋다.

예외 처리를 미리 해주면 단순 프로그래밍시만 아니라 Backtracking이나 DFS같은 재귀함수를 쓸 때도 무한루프에 빠지지 않을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void getConnection() {
    Server server = getServer();
    if (server == null) {
        return;
    }
    Client client = server.getClient();
    if (client == null) {
        return;
    }
    Connection con = con.getConnection();
    if (con == null) {
        return;
    }
    // 실제 처리할 로직
}
cs

위와 같이 if문으로 예외 처리를 통해 바로 return으로 함수를 종료하면 가독성이 높아진다.

또한, if문 조건에는 주로 간단한 내용을 조건으로 주로 걸고, 복잡한 내용은 else문에 작성하는 것이 일반적이다.

1
2
3
4
5
6
7
8
9
10
11
public int checkNumber(int inputNumber) {
    if (inputNumber == 1) {
        //처리    
    } else if (inputNumber == 2) {
        //처리
    } else if (inputNumber == 3) {
        //처리
    } else if (inputNumber == 4) {
        //처리
    }
}
cs

위 코드 역시 inputNumber가 1인 경우에도 끝까지 확인을 해야한다는 단점이 있다.

이러한 경우 역시 if문으로 조건을 만족하면 return 시키는 방법이 효율적이라고 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int checkNumber(int inputNumber) {
    if (inputNumber == 1) {
        //처리
        return ret;
    }
    if (inputNumber == 2) {
        //처리
        return ret;
    }
    if (inputNumber == 3) {
        //처리
        return ret;
    }
    if (inputNumber == 4) {
        //처리
        return ret;
    }
}
cs


* 참고

Java 성능 좋은 분기문을 쓰는 방법, if문에서 연산 순서 (if문에서 좋은 퍼포먼스를 만들기, && || 연산자 우선 순위)

if문은 가장 필수 적인 요소 하지만 골치 아픈 녀석(if 안쓰는법)

반응형
반응형

* Graph (그래프)

그래프란 어떤 상태 혹은 객체 간의 관계를 나타내는 자료구조입니다. 그래프는 정점(Vertex)과 간선(Edge)으로 구성됩니다.
정점이란 어떠한 상태 혹은 객체를 나타냅니다. 간선은 그러한 정점 간의 관계, 그중에서도 연결성을 표현하는 요소입니다.

아래 그림은 그래프의 개념이 퍼지기 시작한 '쾨니히스베르크의 다리 건너기 문제'의 쾨니히스베르크의 다리를 도식화한 두 그림입니다.
A, B, C, D로 나눠진 4개의 구역과 각 구역을 잇는 a, b, c, d, e, f, g 7개의 다리가 있습니다. 왼쪽이 일반적으로 사용되는 그림이며, 이를 그래프 형태로 나타낸 것이 오른쪽의 그림입니다.
여기서 원으로 표현된 A, B, C, D가 정점이며 이 정점들을 연결하는 실선 a, b, c, d, e, f, g가 두 지역을 오고 갈 수 있음을 의미하는 간선이 됩니다.

<쾨니히스베르크의 다리 1>

 

<쾨니히스베르크의 다리 2>

이를 수학적으로 표현하면 아래와 같습니다.

전체그래프 G = (V, E)
정점 집합 V(G) = {A, B, C, D}
간선 집합 E(G) = {a, b, c, d, e, f, g}

이러한 정점과 간선의 관계를 표현하는 말로 인접(adjacent)과 부속(incident)이 있습니다. 정점 u, v가 있고 이 두 정점을 잇는 간선 e가 있다고 가정합시다. 이때 정점 u, v는 e로 인해 서로 인접합니다. 같은 상황에서 간선 e는 정점 u, v에 부속합니다.

그래프에는 크게 무방향 그래프(undirected graph, 무향 그래프)와 단방향 그래프(directed graph, 유향 그래프)가 있습니다.
무방향 그래프란 위의 쾨니히스베르크의 다리와 같이 모든 간선이 양방향으로 연결된 그래프입니다. 정점 u, v에 대해 무방향 그래프에서는 u->v라면 v->u이며, u->v가 불가능하다면 v->u 역시 불가능합니다.
반면 단방향 그래프는 각 간선이 방향을 가집니다. 따라서 정점 u에서 정점 v로 가는 간선 e가 있다면 이는 u->v를 의미할 뿐, v->u를 의미하는 것은 아닙니다.
단방향 그래프에서 u->v를 연결하는 간선이 있을 때 u는 v에 인접한다, v는 u로부터 인접하다 라고 표현할 수 있습니다.

<무방향 그래프>

 

<단방향 그래프>

간선은 따로 이름을 주지 않더라도 부속한 두 정점으로 표현할 수 있습니다. 무방향 그래프의 경우 u와 v를 연결하는 간선은 (u, v)와 같이 표현합니다. 단방향 그래프의 경우 u에서 나가 v로 들어오는 간선을 로 표현합니다.
따라서 위의 무방향 그래프 G1과 단방향 그래프 G2를 아래와 같이 표현할 수 있습니다.

G1 = (V, E)
V(G1) = {A, B, C}
E(G1) = {(A, B), (B, C)}

 

G2 = (V, E)
V(G2) = {A, B, C}
E(G2) = {<A, B>, <B, C>}

그래프에서 쓰이는 또 다른 개념으로는 차수(degree)가 있습니다. 차수란 각 정점에 부속된 간선의 수로, 무방향 그래프에서 어떤 정점 v의 차수란 v에 부속된 간선의 개수입니다. 단방향 그래프에서 정점의 차수는 입력차수(in-degree)와 출력차수(out-degree)로 나누어집니다. 정점 v의 입력차수는 v로 들어오는 간선의 수이며 출력차수는 v에서 나가는 간선의 수와 같습니다.

이러한 그래프를 코드로 표현하는 데에는 크게 두 가지 방법이 사용됩니다. 첫 번째는 인접 행렬(adjacency matrix)을 이용한 표현입니다. 인접 행렬이란 각 정점이 다른 정점과 연결된 상태에 관한 정보를 나타내는 정사각행렬입니다. 예를 들어, 정점 u에서 나가 정점 v로 들어오는 간선이 있다면 1, 아니면 0으로 인접 행렬을 채웁니다. 무방향 그래프의 경우, u->v간선이 v->u 간선과 동일하므로 대각선(diagonal)을 중심으로 대칭(symmetric)됩니다. 아래는 정점이 4개인 단방향 그래프의 예시입니다.

<인접 행렬을 이용한 단방향 그래프의 표현>

두 번째 방법은 인접 리스트(adjacency list)를 이용한 표현입니다. 인접 리스트란 한 정점과 인접한 정점들을 리스트로 표현하는 형태입니다.
아래는 정점이 4개인 단방향 그래프에서 정점 u에서 나가 v로 들어오는 간선들의 정보를 인접 리스트로 나타낸 예시입니다.

<인접 리스트를 이용한 단방향 그래프의 표현>

그렇다면 두 개의 표현법은 어떻게 다를까요?
2차원 인접 행렬의 이름을 adj_matrix, 인접 리스트의 이름을 adj_list라고 할 때 두 표현법은 아래와 같은 장단점을 가집니다. 
두 정점 u, v가 인접한지를 확인하는 연산을 생각해봅시다. 인접 행렬의 경우 이 연산은 adj_matrix[u][v]의 값을 참조하기만 하면 됩니다. 즉 O(1)의 시간복잡도를 가집니다. 
하지만 인접 리스트는 adj_list[u]에 연결된 원소들을 순회하며 v가 존재하는지 확인해야 합니다. 최악의 경우 이 연산은 O(|V|)의 시간복잡도를 가집니다. 
반면 정점 u에 연결된 모든 정점을 탐색하는 상황을 생각해봅시다. 인접 행렬의 경우 어떤 정점들이 연결되어있는지 알기 위해서 모든 adj_matrix[u][i]를 참조해야 합니다. 따라서 O(|V|)의 시간이 걸립니다. 
하지만 인접 리스트의 경우에는 adj_list[u]에 연결된 정점의 개수만큼만 걸립니다. 물론 최악의 경우 O(|V|)지만 이 탐색이 전체 그래프로 확장된다면 달라집니다. 
인접 행렬의 경우에는 O(|V|^2)을 모두 돌아야 하지만, 인접 리스트의 경우 O(|E| + |V|)가 됩니다. 이는 각 표현에 사용된 모든 값을 참조하기 때문인데, 인접 행렬의 공간복잡도는 O(|V|^2)이며 인접 리스트의 공간복잡도는 O(|E|)입니다. 
따라서 각 정점 간의 연결관계 참조가 잦다면 인접 행렬 표현이, 순회가 잦거나 |V|가 큰 그래프에서는 인접 리스트의 표현이 유리합니다.


* 출처 & 참고 링크

Samsung CodeGround

BFS & DFS

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

반응형
반응형

* Tree (트리)

트리는 자식과 부모의 관계로 이루어진 계층적인 구조입니다. 필요에 따라 다양한 종류로 나뉘게 되는데 이번 문서에서는 제일 간단한 트리인 이진 트리에 대해서 설명하려고 합니다. 먼저 이진 트리에서 사용하는 용어들을 정리해보면 다음과 같습니다.

Root : 트리에서 가장 최상위에 존재하는 노드

Child : 어떠한 노드의 자식 노드

Parent : 어떠한 노드의 부모 노드

Siblings : 같은 부모를 갖는 형제 노드

Leaf/Terminal : 자식 노드를 갖지 않는 노드

Branch/Internal : 자식 노드를 적어도 1개 이상 갖는 노드

Degree : 노드가 가지고 있는 자식 노드의 개수

Height : 해당 노드부터 Leaf 노드까지의 가장 긴 거리

Level : 트리 각 층의 단계 (루트 노드의 경우 1)

위에서 트리에서 사용하는 용어들을 알아보았습니다. 여러분의 이해를 돕기 위해 아래의 그림을 통해서 용어들을 설명하도록 하겠습니다.

<그림 1> 이진 트리

1번 노드를 self 현재 노드라고 한다면, 0은 1번 노드의 Parent입니다.( 0은 트리의 최상단에 위치하기 때문에 root 노드입니다). 그리고 3번과 4번 노드는 1의 자식 노드들 입니다. 2번 노드는 1번 노드의 부모인 0번 노드의 자식 노드이므로 sibling입니다.(자매 노드). 또한 자식이 하나도 존재하지 않으면 Leaf/Terminal 노드입니다. 이러한 노드들은 그림에서 연두색으로 표시하였습니다. 만약 자식이 1명이라도 존재한다면 Branch/ Internal 노드입니다. 이러한 노드들은 그림에서 파랑색으로 표현했습니다. 1번 노드의 Degree는 자식 노드의 수인 2입니다. Height는 1번 노드보다 하위 노드들 중 가장 아래에 있는 노드 7번 노드와의 거리인 2입니다. 1번 노드의 level은 2입니다.(루트 노드는 Level이 1)

이제 위에서 정리했던 용어들의 설명이 다 끝났습니다.

다음으로 트리의 탐색 방법에 대해서 설명을 하도록 하겠습니다. 트리 노드들의 값들을 확인하기 위해선 어떤 방법을 사용할까요? 만약 트리가 선형구조로 되어 있다면 그냥 포인터를 하나 두고 한 칸 씩 뒤로 옮기면서 확인하면 되었겠지만 아쉽게도 트리는 비선형 구조입니다. 따라서 일반적인 선형탐색 방법으로는 모든 노드들을 확인할 수 없습니다. 트리에서는 이러한 탐색과정을 Traversal(순회)이라고 합니다. 순회의 방법에는 여러가지가 있지만 대표적으로 사용하는 방법은 3가지가 있습니다. 그 방법은 Pre-Order traversal(전위 순회), In-Order traversal(중위 순회), Post-Order traversal(후위 순회) 입니다. 이 3가지 순회의 차이점은 방문 시 수행할 기능을 어느 시점에 호출하는가 입니다.

먼저 Pre-Order의 수행 방법입니다. (방문 시 수행할 기능은 값 출력이라고 하겠습니다.)

Pre-Order(현재 트리 노드의 위치 cur){
  
  Print(cur’s value)
  
  If(cur’s left exist){
	Pre-Order(cur->left)
  }
  If(cur’s right exist){
	Pre-Order(cur->right)
  }
}
	

두 번째로 In-Order의 수행 방법입니다.

In-Order(현재 트리 노드의 위치 cur){
  If(cur’s left exist){
	In-Order(cur->left)
  }
  
  Print(cur’s value)
  
  If(cur’s right exist){
	In-Order(cur->right)
  }
}
	

마지막으로 Post-Order의 수행 방법입니다.

Post-Order(현재 트리 노드의 위치 cur){
  If(cur’s left exist){
	Post-Order(cur->left)
  }
  If(cur’s right exist){
	Post-Order(cur->right)
  }
  
  Print(cur’s value)
  
}
	

위의 3가지 트리 순회를 보면 Print, 즉 노드를 방문했을 때 수행할 기능의 순서만 변화하는 것을 알 수 있습니다. 그러면 위에서 사용했던 예시인 <그림 1>을 이용하겠습니다.

트리의 순회는 root에서부터 시작합니다.

먼저, Pre-Order(0 : root)을 실행했을 때 출력 결과에 대해서 말씀드리겠습니다. Pre-Order는 자기 자신-왼쪽-오른쪽 순서로 작업을 수행하는 방식입니다.

(1) 루트인 0번 노드에서 출발하므로 0을 출력합니다. 그리고 왼쪽 노드가 존재하므로 Pre-Order(1)을 호출합니다.

(2) 1번 노드에서 1을 출력하고 마찬가지로 왼쪽 노드가 존재하므로 Pre-Order(3)을 호출합니다.

(3) 3번 노드에서 3을 출력하고, 왼쪽 노드가 존재하므로 Pre-Order(7)을 호출합니다.

(4) 7번 노드에서 7을 출력하게 됩니다. 7번 노드는 왼쪽 노드가 없습니다. 따라서 오른쪽 노드를 확인합니다. 하지만 오른쪽 노드 역시 존재하지 않으므로 Pre-Order(7)은 종료됩니다.

(5) 다시 Pre-Order(3)으로 돌아갑니다.(재귀 함수) 방금 Pre-Order(3)에서 왼쪽 노드를 확인했기 때문에 오른쪽 노드를 확인합니다. 하지만 오른쪽 노드가 존재하지 않으므로 Pre-Order(3) 역시 종료됩니다.

(6) Pre-Order(1)로 돌아가게 됩니다. 마찬가지로 왼쪽 노드를 확인했으므로 오른쪽 노드를 확인하는데 4번 노드가 오른쪽 노드이므로, Pre-Order(4)를 호출합니다.

(7) 4번 노드에서 4가 출력됩니다. 4번 노드도 7번과 같이 Leaf/Terminal 노드이므로 그냥 종료됩니다.

(8) Pre-Order(1)로 돌아가고 Pre-Order(1)도 종료되어 Pre-Order(0) 으로 돌아갑니다. 이러한 방식으로 방문하게 되면 출력결과는 0-1-3-7-4-2-5-6으로 출력하게 됩니다.

이번에는 In-Order를 실행했을 때 출력 결과에 대해서 말씀드리겠습니다. In-Order는 왼쪽-자기 자신-오른쪽 순서로 작업을 수행하는 방식입니다.

(1) 우선 루트인 0번 노드에서 출발합니다. 하지만 Pre-Order와는 다르게 이때 0을 출력하지 않고 왼쪽 자식 노드의 유무를 확인합니다. 이때 왼쪽 노드가 존재하므로 In-Order(1)을 호출합니다.

(2) 1번 노드에서도 1을 바로 출력하지 않고, 왼쪽 노드가 존재하므로 In-Order(3)을 호출합니다.

(3) 3번 노드에서도 3에서도 3을 바로 출력하지 않고, 왼쪽 노드가 존재하므로 In-Order(7)을 호출하게 됩니다.

(4) 7번 노드는 Leaf/Terminal 노드이므로 왼쪽 노드가 없어 7을 출력하고 오른쪽 노드의 유무를 하지만 오른쪽 노드도 없으므로 종료됩니다.

(5) 다시 In-Order(3)으로 돌아가서 3을 출력하고 오른쪽 노드를 확인하지만 존재하지 않으므로 종료됩니다.

(6) 다시 In-Order(1)로 돌아가서 1을 출력하고 오른쪽 노드로 4가 존재하므로 In-Order(4)를 호출합니다.

(7) 4번 노드 역시 Leaf/Terminal 노드이므로 4를 출력하고 종료됩니다.

(8) 다시 In-Order(1)로 돌아가서 종료되고, In-Order(0)으로 돌아가서 0을 출력합니다.

이러한 방식으로 방문하게 되면 출력값은 7-3-1-4-0-5-2-6이 됩니다. 이것은 x좌표 순으로 정렬했을 때의 순서와 동일합니다. 따라서 자신보다 작은 값은 왼쪽 서브 트리, 큰 값은 오른쪽 서브 트리에 저장하는 이진 탐색 트리라면 In-Order Traversal 방식을 이용했을 때 정렬된 값이 나오게 됩니다.

마지막으로 Post-Order를 설명하겠습니다. 이 방식은 왼쪽-오른쪽-자기자신 순서로 수행하는 방식입니다.

(1) 우선 루트인 0번 노드에서 시작하고, 왼쪽 노드가 존재하므로 Post-Order(1)을 호출합니다.

(2) 1번 노드에서도 왼쪽 노드가 존재하므로 Post-Order(3)을 호출합니다.

(3) 3번 노드 역시 왼쪽 노드가 존재하므로 Post-Order(7)을 호출합니다.

(4) 7번 노드는 Leaf/Terminal 노드이므로 7을 출력하고 종료됩니다.

(4) 다시 Post-Order(3)으로 돌아가서 3번 노드는 오른쪽 노드가 없으므로 3을 출력하고 종료됩니다.

(5) 1번 노드는 오른쪽 노드가 존재하므로 Post-Order(1)로 돌아가서 오른쪽 노드인 Post-Order(4)을 호출합니다.

(4) 4번 노드는 Leaf/Terminal 노드이므로 4를 출력하고 종료됩니다.

(5) 다시 Post-Order(1)로 돌아가서 1을 출력하고 종료됩니다.

이와 같은 방식으로 방문하면 출력값은 7-3-4-1-5-6-2-0이 됩니다.

이러한 트리의 순회 방법들로 트리에서 원하는 값을 탐색할 수 있습니다. 그러면 다음으로는 트리의 삽입에 대해서 알아보도록 하겠습니다.

이진 트리로 설명을 하자면, 각 노드의 왼쪽 오른쪽 노드를 저장하기 위해 아래와 같은 2차원 배열을 만들어줍니다.

Child[Node_Num][LR] : Node_Num은 해당 노드의 번호, LR은 0일 경우 왼쪽 자식 노드 1일 경우 오른쪽 자식 노드를 의미합니다. 따라서 0번 노드의 왼쪽 노드가 1번 노드라는 것을 표현하고 싶다면 Child[0][0] = 1과 같이 쓰면 되고, 0번 노드의 오른쪽 노드가 3번 노드라는 것을 표현하고 싶다면 Child[0][1] = 3과 같이 쓰면 됩니다.

그리고 만약 트리 노드마다 값이 존재한다면 1차원 배열인 Value를 만들어주어 각 노드의 값을 표현해주면 됩니다.


* 출처 & 참고 링크

Samsung CodeGround

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

반응형

'∙Data Structure' 카테고리의 다른 글

[Data Structure] 자료구조별 시간복잡도  (0) 2019.02.01
[Data Structure] Graph (그래프)  (0) 2019.01.17
[Data Structure] Hashing (해싱)  (0) 2019.01.17
[Data Structure] Heap (힙)  (0) 2019.01.17