Algorithm

LCA 최소 공통 조상

태인킴 2021. 3. 4. 21:18
반응형


최소 공통 조상 이란 트리(Tree) 구조에서 특정한 두 노드의 공통된 조상 중에서 가장 가까운 조상을 의미 합니다. 최소 공통 조상 알고리즘은 트리에서 두 노드 사이의 거리를 빠르게 구하는 등 다양한 계산에 활용될 수 있다는 특징이 있습니다.

이 알고리즘은 일종의 다이나믹 프로그래밍 입니다.

이 때 트리는 반드시 이진 트리(Binary Tree)가 아니어도 일반적인 형태의 트리라면 적용할 수 있습니다.

위와 같이 트리 형태의 그래프가 주어졌을 때 노드 20과 15의 최소 공통 조상 노드를 구해 봅시다.

두 노드를 거슬러 올라가다 보면 공통 조상인 노드 1을 찾을 수 있습니다.

* 다이나믹 프로그래밍을 활용한 최소 공통 조상 알고리즘

1. 모든 노드에 대한 깊이(Depth)를 구합니다.

2. 모든 노드에 대한 2^i 번째 부모 노드를 구합니다.

3. 최소 공통 조상을 찾을 두 노드를 설정 합니다.

4. 두 노드의 깊이(Depth)가 동일 하도록 거슬러 올라 갑니다.

5. 최상단 노드부터 내려오는 방식으로 두 노드의 공통 부모를 찾아 냅니다.

 

 

1. 모든 노드의 대한 깊이를 구합니다.

dfs를 이용해 쉽게 구현 할 수 있습니다.

// 바로 윗 부모와 연결하는 함수 입니다.
    // 모든 노드의 깊이를 구합니다.
    static void dfs(int x, int depth) {
        c[x] = true;
        d[x] = depth;
        for (int i = 0; i < a[x].size(); i++) {
            int y = a[x].get(i);
            if (c[y]) continue;
            parent[y][0] = x;
            dfs(y, depth + 1);
        }
    }

 

2. 모든 노드에 대한 2^i 번째 부모 노드를 구합니다.

이제 전체 적인 부모 노드 관계를 구합니다.

이 때 부모를 다 구하는 것이 아니라 2^i번째 부모 노드만 구하도록 합니다.

이는 2의 배수씩 점프 하도록 하기 위함이며 시간 복잡도O(logN) 으로 만들어 줍니다.

// 전체 부모 관계를 설정하는 함수 입니다.
    static void setParent() {
        dfs(0, 0); // 루트를 0으로 설정 합니다.
        for (int i = 1; i < LOG; i++) {
            for (int j = 0; j < n; j++) {
                parent[j][i] = parent[parent[j][i - 1]] [i - 1];
            }
        }
    }

노드 20의

2^0 번째 부모 = 17

2^1 번째 부모 = 13

2^2 번째 부모 = 3

.....

이와 같이 구합니다.

 

3. 최소 공통 조상을 찾을 두 노드를 설정 합니다.

 

4. 두 노드의 깊이(Depth)가 동일 하도록 거슬러 올라 갑니다.

노드 15와 20의 깊이가 동일하도록 거슬러 올라가기 위해서 더 낮은 노드인 20을 선택해서 올라갑니다.

이 때 깊이의 차이는 6-4 = 2이므로 2보다 많이 올라가지는 않도록 합니다.

결과적으로 다음 그림과 같이 노드 13까지 올라 갑니다.

 

 

5. 최상단 노드부터 내려오는 방식으로 두 노드의 공통 부모를 찾습니다.

두 노드를 기준으로 가장 멀리 있는 부모 부터 계산해 부모가 동일하지 않는 시기를 찾습니다.

// 최소 공통 조상을 찾는 함수 입니다.
    static int LCA(int x, int y) {
        // y가 더 깊도록 설정 합니다.
        if (d[x] > d[y]) {
            int tmp = y;
            y = x;
            x = tmp;
        }

        // 두 노드의 깊이를 동일하도록 맞춥니다.
        for (int i = LOG - 1; i >= 0; i--) {
            if (d[y] - d[x] >= (1 << i)) {
                y = parent[y][i];
            }
        }

        // 부모가 같은 경우 반환합니다.
        if (x == y) return x;
        
        // 최상단 노드부터 내려오는 방식으로 두 노드의 공통 부모를 찾습니다.
        for (int i = LOG - 1; i >= 0; i--) {
            // 조상을 향해 거슬러 올라 갑니다.
            if (parent[x][i] != parent[y][i]) {
                x = parent[x][i];
                y = parent[y][i];
            }
        }
        // 바로 부모가 찾고자 하는 조상 입니다.
        return parent[x][0];
    }

 

 

이제 전체 소스 코드 입니다.

package idea;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class LowestCommonAncestor {
    /**
     * @참고 https://blog.naver.com/ndb796/221282478466
     */
    static final int MAX = 1001;
    static final int LOG = 11; // 전체 노드의 갯수는 2^10개 이하 입니다.
    static int n;
    static int[][] parent = new int[MAX][LOG];
    static int[] d = new int[MAX];
    static boolean[] c = new boolean[MAX];
    static List<Integer>[] a = new ArrayList[MAX];

    public static void main(String[] args) {
        n = 21;
        a = Stream.of(a).map(value -> value = new ArrayList<>()).toArray(ArrayList[]::new);
        // 0과 1을 연결 합니다.
        a[0].add(1);
        a[1].add(0);
        // 0과 2를 연결 합니다.
        a[0].add(2);
        a[2].add(0);
        // 1과 3를 연결 합니다.
        a[1].add(3);
        a[3].add(1);
        // 1과 4를 연결 합니다.
        a[1].add(4);
        a[4].add(1);
        // 2과 5를 연결 합니다.
        a[2].add(5);
        a[5].add(2);
        // 2과 6를 연결 합니다.
        a[2].add(6);
        a[6].add(2);
        // 3과 7를 연결 합니다.
        a[3].add(7);
        a[7].add(3);
        // 3과 8를 연결 합니다.
        a[3].add(8);
        a[8].add(3);
        // 4과 9를 연결 합니다.
        a[4].add(9);
        a[9].add(4);
        // 4과10를 연결 합니다.
        a[4].add(10);
        a[10].add(4);
        // 4과 11를 연결 합니다.
        a[4].add(11);
        a[11].add(4);
        // 8과 12를 연결 합니다.
        a[8].add(12);
        a[12].add(8);
        // 8과 13를 연결 합니다.
        a[8].add(13);
        a[13].add(8);
        // 9과 14를 연결 합니다.
        a[9].add(14);
        a[14].add(9);
        // 10과 15를 연결 합니다.
        a[10].add(15);
        a[15].add(10);
        // 13과 16를 연결 합니다.
        a[13].add(16);
        a[16].add(13);
        // 13과 17를 연결 합니다.
        a[13].add(17);
        a[17].add(13);
        // 14과 18를 연결 합니다.
        a[14].add(18);
        a[18].add(14);
        // 15과 19를 연결 합니다.
        a[15].add(19);
        a[19].add(15);
        // 17과 20를 연결 합니다.
        a[17].add(20);
        a[20].add(17);
        setParent();
//        System.out.printf("5와 7의 LCA: %d \n", LCA(5, 7));
        System.out.printf("15와 20의 LCA: %d \n", LCA(15, 20));
//        System.out.printf("16와 17의 LCA: %d \n", LCA(16, 17));
//        System.out.printf("10와 9의 LCA: %d \n", LCA(10, 9));
//        System.out.printf("3와 17의 LCA: %d \n", LCA(3, 17));
    }

    // 전체 부모 관계를 설정하는 함수 입니다.
    static void setParent() {
        dfs(0, 0); // 루트를 0으로 설정 합니다.
        for (int i = 1; i < LOG; i++) {
            for (int j = 0; j < n; j++) {
                parent[j][i] = parent[parent[j][i - 1]] [i - 1];
            }
        }
    }

    // 바로 윗 부모와 연결하는 함수 입니다.
    // 모든 노드의 깊이를 구합니다.
    static void dfs(int x, int depth) {
        c[x] = true;
        d[x] = depth;
        for (int i = 0; i < a[x].size(); i++) {
            int y = a[x].get(i);
            if (c[y]) continue;
            parent[y][0] = x;
            dfs(y, depth + 1);
        }
    }

    // 최소 공통 조상을 찾는 함수 입니다.
    static int LCA(int x, int y) {
        // y가 더 깊도록 설정 합니다.
        if (d[x] > d[y]) {
            int tmp = y;
            y = x;
            x = tmp;
        }

        // 두 노드의 깊이를 동일하도록 맞춥니다.
        for (int i = LOG - 1; i >= 0; i--) {
            if (d[y] - d[x] >= (1 << i)) {
                y = parent[y][i];
            }
        }

        // 부모가 같은 경우 반환합니다.
        if (x == y) return x;

        // 최상단 노드부터 내려오는 방식으로 두 노드의 공통 부모를 찾습니다.
        for (int i = LOG - 1; i >= 0; i--) {
            // 조상을 향해 거슬러 올라 갑니다.
            if (parent[x][i] != parent[y][i]) {
                x = parent[x][i];
                y = parent[y][i];
            }
        }
        // 바로 부모가 찾고자 하는 조상 입니다.
        return parent[x][0];
    }
}
반응형