Algorithm

다이나믹 프로그래밍 2

태인킴 2021. 1. 15. 22:15
반응형


 

다이나믹 프로그래밍 1

'다이나믹 프로그래밍' 한글로 '동적 계획법' 이라는 알고리즘은 '리차드 벨만'이 만든 알고리즘 입니다. 리차드 벨만은 최단 경로를 찾는 '벨만 포드 알고리즘'도 만들었습니다. ​ ​ 1. 다이나

coding-food-court.tistory.com

위의 다이나믹 프로그래밍의 개념을 먼저 공부하고 아래의 문제를 다이나믹 프로그래밍을 이용해서 해결해 보겠습니다.

 

 

행렬 경로 문제

1. 정수들이 저장된 N * N 행렬이 있습니다.

2. 이 행렬의 좌상단 부터 우하단 까지 이동 합니다.

3. 오른쪽이나 아래쪽 방향으로만 이동 할 수 있습니다.

4. 어느 길로 가야 정수들의 합이 최소가 되도록 할 수 있을까요?

1. (i, j)에 도달하기 위해서는 (i, j-1) 혹은 (i-1, j)를 거쳐야 합니다.

2. (i, j-1) 혹은 (i-1, j)까지는 최선의 방법으로 이동해야 합니다.

 

 

1. 조건식

 

 

2. sudo 코드

int findMiniVal(int i, int j){
   if(i == 1 && j == 1)
       return m[i][j];
   else if(i == 1)
       return findMiniVal(1, j-1) + m[i][j];
   else if(j == 1)
       return findMiniVal(i-1, 1) + m[i][j];
   else
       return Math.min(findMiniVal(i-1, j), findMiniVal(i, j-1)) + m[i][j];
}

위 findMiniVal()의 호출 스택(Call stack)을 가시적으로 확인해 보겠습니다.

밑에 이미지를 보시면 i = 4, j = 4 임에도 findMiniVal() 함수가 굉장히 많이 호출 되는 것을 확인 할 수 있습니다. 이중에서 중복 되는 연산을 확인 하기 위해 findMiniVal(2, 2) 와 findMiniVal(2, 1)가 얼마나 많이 호출 되는지 체크해 봤습니다. 우리는 memoization 방식으로 이를 해결해야 합니다.

 

 

3. memoization top-down 방식

int findMiniVal(int i, int j){                   // 배열 miniSum은 이미 -1로 초기화
   if(miniSum[i][j] != -1) return miniSum[i][j]; // 기존 배열 miniSum이 -1이 아니라면 캐싱되어 있던 값 리턴
   if(i == 1 && j == 1)
       miniSum[i][j] = m[i][j];
   else if(i == 1)
       miniSum[i][j] = findMiniVal(1, j-1) + m[i][j];
   else if(j == 1)
       miniSum[i][j] = findMiniVal(i-1, 1) + m[i][j];
   else
       miniSum[i][j] = Math.min(findMiniVal(i-1, j), findMiniVal(i, j-1)) + m[i][j];
   return miniSum[i][j];
}

miniSum 이라는 2차원 배열을 만들고 이 2차원 배열은 -1값로 모두 초기화 되있어야 합니다. miniSum[i][j] 은 (i, j)까지 오른쪽이나 아래쪽으로만 이동했을때 m[i][j]의 합이 최소가 되는 값이 저장 되기 위한 공간 입니다.

 

 

4. memoization bottom-up 방식

int findMiniVal(int i, int j){           // 배열 miniSum은 이미 -1로 초기화
   for(int i=1; i<=n; i++){
       for(int j=1; j<=n; j++){
           if(i==1 && j==1)
              miniSum[i][j] = m[1][1];
           else if(i == 1) 
              miniSum[i][j] = m[i][j] + miniSum[i][j-1];
           else if(j == 1)
              miniSum[i][j] = m[i][j] + miniSum[i-1][j];
           else
              miniSum[i][j] = m[i][j] + Math.min(miniSum[i-1][j], miniSum[i][j-1]);
       }
   }
   return miniSum[n][n];
}

이 코드의 시간 복잡도는 두개 for문이 n번씩 반복 되므로 O(n2) 입니다.

 

 

5. Common trick

int findMiniVal(){             // 배열 miniSum은 이미 무한대로(-1이 아니라) 초기화
   for(int i=1; i<=n; i++){
       for(int j=1; j<=n; i++){
           if(i==1 && j==1){
              miniSum[i][j] = m[1][1];
           else
              miniSum[i][j] = m[i][j] + Math.min(miniSum [i-1][j], miniSum [i][j-1]);
       }
   }
   return miniSum[n][n];
}

위 코드는 bottom-up 방식 에서 miniSum을 -1이 아니라 무한대로 초기화 시켜 if(i == 1) 또는 if(j == 1) 조건을 지워 줄수 있습니다. -1이 아닌 무한대로 초기화 시켰으면 Math.min(miniSum[0][j], miniSum[i][j-1])은 Math.min(무한대, miniSum[i][j-1])가 되기 때문에 if(i == 1) 또는 if(j == 1) 조건을 지워 줄수 있습니다.

 

 

6. 최단 경로 구하기

위에 까지의 코드는 최소합을 구하는 방법 이었습니다. 이번에는 최소합을 따라 가는 경로를 출력해 보겠습니다.

int findMiniVal(){             // 배열 miniSum은 이미 무한대로(-1이 아니라) 초기화
   for(int i=1; i<=n; i++){
       for(int j=1; j<=n; i++){
           if(i==1 && j==1){
              miniSum[i][j] = m[1][1];
              path[i][j] = '-';
           else{
                if(miniSum[i-1][j] < miniSum[i][j-1]){
                      miniSum[i][j] = m[i][j] + miniSum[i-1][j];
                      path[i][j] = '^';
                } else {
                      miniSum[i][j] = m[i][j] + miniSum[i][j-1];
                      path[i][j] = '<';
                }
           }
       }
   }
   return miniSum[n][n];
}

 

 

6-1 도착점 부터 경로 출력하기

void printPath(){
    int i = n, j = n;
    while(path[i][j] != '-'){
        print(i + " " + j);
        if(path[i][j] == '<')
            j = j - 1;
        else
            i = i - 1;
   }
   print(i + " " + j);
}

 

 

6-2 출발점 부터 경로 출력 하기

// 위 코드와 반대로 출력 순서가 반대로 출력되고 recrusive 하게 구현
void printPathRecursive(int i, int j){
    if(path[i][j] == '-')
       print(i + " " + j);
    else {
       if(path[i][j] == '<')
          printPathRecursive(i, j-1);
       else
          printPathRecursive(i-1, j);
       print(i + " " + j);
    }
}
반응형