Bell_bear 2021. 1. 28. 21:28
반응형

코세라 수업을 듣고 내가 정리한 내용이다.

 

Union find 알고리즘에 대해서 공부한 내용이다.

목차는 아래와 같이 5개이다.

 

1. dynamic connectivity

2. quick find

3. quick union

4. improvements

#5. applications # Union find 를 활용한 사례

 

 

1. dynamic connectivity

 

먼저 주어지는 N은 노드의 개수로 총 노드의 개수가 주어지고 이 노드들의 연결 정보가 입력값으로 주어진다.

이를 연결해주는 Union이라는 함수와 연결이 되어있는지 검사하는 find,connected 라는 함수를 만들어준다.

 

대략적인 클래스 모양은 아래와 같다.

public class UF // Union Find
    UF( int n ) // n개의 objects를 생성
    void union(int p, int q) // p,q 사이의 연결을 생성
    boolean connected(int p, int q) // p,q가 연결된 컴포넌트 인지 검사
    // int find(int p)
    // int count()

 

public static void main(String[] args)
{
    int N = StdIn.readInt();
    UF uf = new UF(N);
    while (!StdIn.isEmpty())
    {
       int p = StdIn.readInt();
       int q = StdIn.readInt();
       if (!uf.connected(p,q,))
       {
           uf.union(p,q);
           StdOut.println(p+" "+q);
       }
    }

 

2. quick find ( eager algorithm )

예제)

 

가지고 연결의 index 값이 동일하여 같은 배열에 있으면, 동일한 index를 가지고 있다고 판단하여,

 

id[] 0,1,1,3,3,0,0,1,3,3 각 최초 시작하는 index 값을 저장해도면 찾을 수 있다.

 

이 id 배열을 저장하여 두었을 때, 해당 값에 대해서 연결을 물으면 id 값이 다르면 연결성이 없다고 볼 수 있다.

 

하지만 이 접근방법은 숫자가 커진다면, 바꿔야하는 숫자도 많아져서 문제가 생긴다. 결국 제곱시간은 너무 느리다..

( 쉬운 구현이기 때문에 간단하게는 사용 가능 )

 

public class QuickFindUF
{
    private int[] id;
    public QuickFindUF(int N)
    {
        id = new int[N];
        for (int i = 0; i < N; i++)
            id[i] = i;
    }
    public boolean connected(int p, int q)
    { return id[p] == id[q]; }
    public void union(int p, int q)
    {
        int pid = id[p];
        int qid = id[q];
        for (int i = 0; i < id.length; i++)
            if (id[i] == pid) id[i] = qid;
    }
}

 

3. quick union ( lazy algorithm )

 

위의 quick find 알고리즘에서는 union에 많은 시간이 들어간다. ( 거의 무조건  N 만큼은 돌아가야함. )

 

이를 줄여보고자, quick union 알고리즘으로 만들어 본다.

 

id 배열에 연결되는 node의 root 값을 찾아가는 방법이므로, union의 속도가 빨라질수 있다.

 

이 방법은 결국 들어오는 왼쪽의 노드를 오른쪽의 노드 밑에 붙이는 만드는 방법으로 계속 트리가 늘어날 수있다.

public class QuickUnionUF
{
    private int[] id;
    
    public QuickUnionUF( int N )
    {
        id = new int[N];
        for (int i=0;i<N;i++) id[i]=i;
    }
    
    private int root(int i)
    {
        while (i != id[i]) i= id[i];
        return i;
    }
    
    public boolean connected(int p, int q)
    {
        return root(p)==root(q);
    }
    
    public void union(int p, int q)
    {
        int i = root(p);
        int j = root(q);
        id[i]=j;
    }
}

 

이 알고리즘은 quick find 보다는 빠를 수 있지만, quick union도 너무 느린 알고리즘이다.

 

Algorithm initialize union find
quick-find N N 1
quick-union N N N ( worst case )

 

quick-find의 단점은 union 작업시 N 만큼 걸리는 것이고,

quick-union의 경우 find 작업시 최대 N 만큼 걸릴 수 있게 되어 속도가 둘 다 좋지는 않다. ( 아주 긴 tree 모양이 될 때,)

 

그래서 quick-union을 개선시킨 방향을 생각해본다.

 

4. quick-union improvements

 

개선방향으로는 union 함수를 수행할 때, weighting을 비교하여 tree가 길어지지 않게 만드는 방법이다.

 

이렇게 되면, tree는 한쪽으로 치우쳐지지 않고, 분배되는 tree로 만들어지게 된다.

 

public class QuickUnionUF
{
    private int[] id;
    private int[] sz;
    
    public QuickUnionUF( int N )
    {
        id = new int[N];
        for (int i=0;i<N;i++) { id[i]=i; sz[i]=1;}
    }
    
    private int root(int i)
    {
        while (i != id[i]) i= id[i];
        return i;
    }
    
    public boolean connected(int p, int q)
    {
        return root(p)==root(q);
    }
    
    public void union(int p, int q)
    {
        int i = root(p);
        int j = root(q);
        // 추가된 코드
        if (i==j) return;
        if (sz[i]<sz[j]) { id[i] = j; sz[j]+= sz[i];}
        else { id[j] = i; sz[i]+= sz[j];}
    }
}

 

Algorithm initialize union find
quick-find N N 1
quick-union N N N ( worst case )
quick-union improvements N log N log N

 

 

여기서 좀 더 발전적인 방향으로 path compression ( 루트를 바로 상속받기 ) 방법으로 좀 더 줄일 수 있다.

 

현재는 한 비교되는 노드에 자식으로 들어가는데, 각자 최고 root에 대해 자식으로 들어가게 되면

더 평평하게 만들 수 있다. 아래는 root을 찾을 때 추가되는 코드이다.

 

private int root(int i)
{
    while (i!=id[i])
    {
        id[i]=id[id[i]];
        i=id[i]
    }
    return i;
}

 

반응형