-> 블로그 이전

[Algorithm Week_4] Divide & Conquer 개요 - Recursion Tree

2022. 3. 25. 13:41Major`/알고리즘

Recursion?

Recursion이란 동일한 문제를 Reduction을 통해서 사이즈를 줄여가는 방식이다

당연히 재귀를 멈춰줄 "Base Case"가 필요하다

 

Recursive Algorithms의 "Correctness"induction으로 증명하고, "Time Complexity"점화식을 통해서 구한다

 

 

Divide & Conquer?

Divide & Conquer이란 Recursion을 사용하는 알고리즘 기법이다

  • Merge Sort
  • Quick Sort
  • ....

 

Recursion은 "input Size"를 줄여가는 방식이다

여기서 단순히 줄여가는 방식은 효율이 그렇지 뛰어나지 않기 때문에 아예 "줄인다"라는 개념을 "잘라버린다"의 개념으로 바꾼 것이 Divide & Conquer이다

 

Recursion에서는 Reduction된 subproblem들에 대해서 다시 "Recursion"으로 보내듯이

Divde & Conquer에서도 "자른 2개의 subproblem"들에 대해서 Recursion을 보낸다

 

 

D&C Pattern

1. Divide

'Several Independent Smaller Instance'

- 일단 큰 전체 문제를 "독립적인 작은 instance"로 나누기

  • 동일 문제 & 입력 사이즈 ↓

 

2. Delegate (Conquer)

'Each Smaller Instance to the Recursion Fairy'

- 1)을 통해서 나눠진 각각의 독립적인 instance를 "재귀의 요정"에게 보내서 처리하도록 한다

- 이 단계에서 각각의 독립적 instance들의 solution이 도출된다 - By Recursion

 

3. Combine (Merge)

'The soluitons for the smaller instances into the final solution for the given instance'

- 2)에서 도출된 각각의 독립적 instance의 solution을 우리가 원래 풀고자 했던 문제에서 사용함으로써 문제를 해결한다

  • 각 solution들을 조합/개선/정리해서 최종 문제 풀기

 

 

Example 1) Sum : Iteration / Recursion / D&C

// sum - Iteration
// input : Natural number n
// output : value of (1 + 2 + 3 + .... + n)

----- Pseudo Code -----
sum1(n)
    s <- 0
    for i <- 1 to n
        s <- s + i
    return s
    
----- Java Code -----
static int sum_for(int [] list){
    int sum = 0;
    for(int n : list){
        sum += n;
    }
    return sum;
}

>> O(n)

 

// sum - Recursion
// input : Natural number n
// output : value of (1 + 2 + 3 + .... + n)

----- Pseudo Code -----
sum2(n)
    if n = 1 return 1
    return n + sum2(n-1)
    
----- Java Code -----
static int sum_recursion(int [] list, int N){
    if (N == 0){
        return 1;
    }
    else{
        return sum_recursion += list[N] + sum_recursion(list, N-1);
    }
}

T(n) = T(n-1) + O(1)

>> O(n)

 

// sum - D&C
// input : Natural number n
// output : value of (1 + 2 + 3 + .... + n)

----- Pseudo Code -----
sum3(n)
    if n = 1 return 1
    if n is even 
        then return 2 * sum3(n/2) + (n/2)*(n/2)
    else // n is odd
        then return 2 * sum3((n-1)/2) + ((n-1)/2)*((n-1)/2) + n
    
----- Java Code -----
static int sum_Divide_Conquer(int N){
    if (N == 1){
        return 1;
    }

    if(N%2 == 0){ // list's last value is "even"
        return 2*sum_Divide_Conquer(N/2) + (N/2)*(N/2);
    }
    else{ // list's last value is "odd"
        return 2*sum_Divide_Conquer((N-1)/2) + ((N-1)/2)*((N-1)/2) + N;
    }
}

>> O(log n)

 

Example 2) Max : Iteration / D&C

// max - Iteration
// input : array A[0, 1, 2, ...., n-1]
// output : maximum value of input array

----- Pseudo Code -----
max1(A[0, 1, 2, ...., n-1])
    m <- A[0]
    for i <- 1 to n-1
        if A[i] > m then m <- A[i]
    return m
    
----- Java Code -----
static int max_for(int [] list){
    int max = list[0];

    for(int i=1; i<list.length; i++){
        if(list[i] > max){
            max = list[i];
        }
    }

    return max;
}

>> O(n)

 

// max - D&C
// input : array A[0, 1, 2, ...., n-1]
// output : maximum value of input array

----- Pseudo Code -----
max2(A[0, 1, 2, ....., n-1])
    // "Base Case"
    if n = 1 return A[0]
    
    // 1. Divide
    mid <- n/2
    
    // 2. Delegate (Conquer)
    m1 <- max2(A[0, 1, 2, ...., mid])
    m2 <- max2(A[mid + 1, mid + 2, ..... n-1])
    
    // 3. Combine (Merge)
    if m1 > m2
        return m1
    else
        return m2
    
----- Java Code -----
static int max_Divide_Conquer(int [] list, int start, int end){
    // start : list's first index
    // end : list's last index

    if(end - start == 0){
        // can't divide anymore
        return list[end];
    }

    // 1. Divide
    int mid = (start + end)/2;
    
    // 2. Delegate (Conquer)
    int m1 = max_Divide_Conquer(list, 0, mid);
    int m2 = max_Divide_Conquer(list, mid + 1, end);

    // 3. Combine (Merge)
    return Math.max(m1, m2);
}

>> O(n)

  • max라는 개념은 최소한 해당 list에서 (n-1)번의 비교를 수행해야 하기 때문에 D&C로 구현해도 시간복잡도가 줄어들지 않는다

 


Recursion Tree

- 재귀 호출을 수행한 만큼 자식 node가 생기는 구조

 

"Value of Node"

- 호출된 node상에서 수행한 연산의 개수

>> "Recursive Call"을 제외한 연산의 개수이다

 

"Sum of the values of all nodes"

- 해당 알고리즘의 running time이다

 


일반적인 D&C Algorithm의 Recursion Tree (Master Theorem)

L : Tree Depth

>> 제일 밑단의 노드의 value는 1이여야 한다

 

leaf의 개수

>> leaf의 개수는 곧 leaf의 비용이라고도 할 수 있다

 

 

 

재귀 호출하는 부분 시간 = r * T(n/c)

비재귀 호출하는 부분 시간 = O(f(n))

 

 

Case 1)은 f(n)이 꽤 큰 경우이다.

이 경우는 다시 말해서, f(n)이 leaf노드의 비용보다 더 빨리 증가해서 해당 알고리즘은 결국 f(n)에 비례한다는 의미이다

>> tree에서 밑으로 갈수록 level sum이 작아져서 결국 수렴하는 경우이다

"비 재귀호출 부분에서 시간을 많이 쓴다"

 

 

 

Case 2)f(n)과 leaf노드의 비용이 동등하게 증가하는 것이다

>> tree에서 밑으로 가도 level sum이 동등한 경우이다

 

 

 

 

Case 3)f(n)이 leaf노드의 비용보다 느리게 증가해서 결국 해당 알고리즘은 leaf노드의 비용에 비례한다

이 말은 다시 말해서 leaf노드의 개수에 비례한다

>> tree에서 밑으로 갈수록 level sum이 증가해서 발산하는 경우이다

"비 재귀호출 부분에서 시간을 더 적게 쓴다"