• [백준 알고리즘] (DP&DC) 1912번 Java 풀이

    2020. 4. 3.

    by. SDev

    728x90

    DP - 1463, 11726, 11727, 9095, 10844, 11057, 2193, 9465, 2156, 11053, 11055, 11722, 11054, 1912, 2579, 1699, 2133, 9461, 2225, 2011, 1105

    출처: https://plzrun.tistory.com/entry/알고리즘-문제풀이PS-시작하기 [plzrun's algorithm]

     

    백준 알고리즘 1912번

     

    문제를 접하고 바로 저번 학기에 수강했던 알고리즘 수업 내용이 떠올랐고, 대략적으로 풀이 내용이 기억났었는데 구현 방식이 구체적으로 떠오르지 않아 교재였던 Introduction to Algorithms(3rd Edition) - Cormen을 다시 살펴봤다.

    교재에서는 DP파트에 들어있지 않고, 이 문제와 동일하지만 조금 더 요구사항이 있는(교재에서는 부분도 함께 출력하라고 함) 문제로 분할정복(Divide&Conquer) 파트에서 다뤄져 있다.

    먼저 교재에서는 두 가지 풀이 방법이 제시되어있는데, 1) 주먹구구식 2)분할계획법 풀이이다.
    (엄밀히 말하면 주먹구구식 풀이는 그냥 간단하다고 언급만 되어있고 구현은 슈도코드도 안나와있다..)

    어쨌든 지금은 공부하는 과정이니까... 그 두 가지 풀이를 모두 풀어보고 한 번 더 DP로도 풀어봤다. 
    결과적으로 주먹구구식 풀이는 역시나 메모리초과가 발생하고 나머지 풀이는 정답 결과를 받았다!

     

    <첫 번째 풀이(주먹구구식)> - 메모리 초과

    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
    package dp;
     
    import java.util.Scanner;
     
    public class N1912_RuleofThumbs {
     
        public static void main(String[] args) {
            Scanner sc = new Scanner(System.in);
            int n =sc.nextInt();
            int arr[] = new int[n];
            
            for(int i=0; i<n; i++) {
                arr[i] = sc.nextInt();
            }
            int max=-9999999;
            int sum[][] = new int[n][n];
            
            for(int i=0; i<n; i++) {
                for(int j=0; j<n; j++) {
                    if(i>j) {
                        sum[i][j] = -99999999;
                    }
                    else if(i == j) {
                        sum[i][j] = arr[j];
                    }
                    else {
                        sum[i][j] = sum[i][j-1]+arr[j];
                    }
                }
            }
            for(int i=0; i<n; i++) {
                for(int j=i; j<n; j++) {
                    max = Math.max(max, sum[i][j]);
                }
            }
            System.out.println(max);
        }
     
    }
     
    Colored by Color Scripter
     

     

    첫 번째 풀이는 접근 방식이 아주 심플하다. 그냥 발생할 수 있는 모든 부분 배열을 구하고 전부 비교하는 것이다.
    책에서는 간단하다고 적혀있는데 사실 구현까지 그렇게 간단하게 느껴지지는 않았다...ㅋㅋㅋㅋㅋㅋ

    아무리 모든 부분 배열을 구한다쳐도 이를 저장하기 위해 2차원 배열을 생성해서 값을 받으면서
    그래도 너무 메모리 낭비는 피하기 위해 2차원 배열을 대각선으로 쪼개고,
    대각선 왼편에는 모두 -9999999값을 저장한뒤, 후에 최댓값을 구할 때도 대각선 오른쪽 방향만 고려하며 값을 구했다.

    그래도 시간, 공간복잡도 모두 n^2이다.
    결과적으로 메모리 초과!

    <두 번째 풀이(분할정복)>

    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
    package dp;
     
    import java.util.Scanner;
     
    public class N1912_DC{
     
        static int arr[];
        public static void main(String[] args) {
            Scanner sc = new Scanner(System.in);
            int n =sc.nextInt();
            arr = new int[n];
            
            for(int i=0; i<n; i++) {
                arr[i] = sc.nextInt();
            }
            
            int max = MaxPartialCal(0, n-1);
            System.out.println(max);
        }
        
        public static int CrossingCal(int low, int mid, int high) {
            int lmax=-999999;
            int lsum=0;
            for(int i=mid;i>=low;i--) {
                lsum = lsum + arr[i];
                if(lsum > lmax) {
                    lmax = lsum;
                }
            }
            int rmax =-999999;
            int rsum =0;
            for(int i=mid+1;i<=high;i++) {
                rsum = rsum + arr[i];
                if(rsum > rmax) {
                    rmax = rsum;
                }
            }
            return rmax + lmax;
        }
        
        public static int MaxPartialCal(int low, int high) {
            if(low ==high) {
                return arr[low];  //basecase : only one element 
            }
            else {
                int mid = (low+high)/2;
                int lmax = MaxPartialCal(low, mid);
                int rmax = MaxPartialCal(mid+1,high);
                int cmax = CrossingCal(low,mid,high);
                
                if(lmax >= rmax && lmax>=cmax) {
                    return lmax;
                }
                else if(rmax >= lmax && rmax >=cmax){
                    return rmax;
                }
                else {
                    return cmax;
                }
            }
        }
     
    }
    Colored by Color Scripter
     

    이 풀이는 교과서에 슈도 코드도 나와있었는데, 완전히 베끼지 않고 조금씩 참고하려다보니 구현하는게 많이 어려웠다.

    분할정복법으로 접근할 때 키 포인트는 이 문제를 3가지 유형으로 나누는 것이다.
    전체 배열의 중간점을 찍으면, 발생할 수 있는 최대 배열의 위치는 3가지가 존재한다.

    1) 중간점 왼쪽에 완벽히 포함된 경우
    2) 배열이 중간점을 포함하고 걸쳐있는 경우
    3) 중간점 오른쪽에 완벽히 포함된 경우

    따라서 세 가지 경우를 모두 비교해 최대값을 반환하는 것이 핵심 아이디어다.

    2)경우에서는 최대부분배열이 중간점을 걸치고 있는 경우이기에,
    먼저 중간점을 기준으로 왼쪽으로 쭉 읽어나가면서 값이 더 커지면 왼쪽 방향으로의 max값을 갱신하고,
    이후 오른쪽으로 똑같이 쭉 읽어나가면서 max값을 갱신한 뒤,
    마지막에 그 양쪽 max값을 더해주면서 최대 배열 길이를 반환하는 방식이다.

    1)경우와 3)경우는 재귀적으로 중간점을 찍어 계속 나눠버리면
    결국 언젠가는 배열 길이가 1이나 2)경우로 귀결된다.

    재귀적으로 계속해서 1), 2), 3) 경우들의 최대값을 반환하고 비교해서
    전체 배열의 최대 부분 배열의 합을 출력한다.

    이 풀이 방법의 복잡도는 nlog(n)으로 1)풀이보다 우수하다.

    low = 가장 왼쪽 index
    high = 가장 오른쪽 index
    mid = 중간 Index
    lsum = 왼쪽 합 
    rsum = 오른쪽 합
    lmax = 왼쪽 최대값
    rmax = 오른쪽 최대값

    <세 번째 풀이(DP)>

    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
    package dp;
     
    import java.util.Scanner;
     
    public class N1912{
        public static void main(String args[]) {
            Scanner sc = new Scanner(System.in);
            
            int n = sc.nextInt();
            
            int arr[] = new int[n];
            int dp[] = new int[n];
            
            for(int i=0; i<n; i++) {
                arr[i] = sc.nextInt();
            }
            sc.close();
            
            dp[0= arr[0];
            
            for(int i=1; i<n; i++) {
                dp[i] = Math.max(arr[i], dp[i-1]+arr[i]);
            }
            
            int max = -9999999;
            for(int i=0; i<n; i++) {
                max = Math.max(dp[i], max);
            }
            System.out.println(max);
        }
    }
     
    Colored by Color Scripter
     

    역시 알고리즘 분류에 다이나믹 프로그래밍으로 되어 있는 이유가 있다.
    다른 풀이에 비해 너무 명쾌하다. 접근 방식을 떠올리기가 어려워서 그렇지..

    DP방식으로의 핵심적인 접근법은
    최대값 = (이전까지의 최대값 + 지금 값) vs (지금 값)
    두 가지 값을 비교하면서 그 위치의 최대 값에 입력해주고,
    나중에 각 위치의 최대값을 비교해서 가장 큰 값을 출력하는 것이다.

    이전까지의 최대값에 현재 값을 더했을 때, 현재 값보다 더 낮다면,
    1) 현재 값이 음수이거나, 2)이전까지의 최대값이 음수이면서, 현재 값보다 더 작다라는 뜻이기 때문이다.

    2)의 경우에는 더 고려할 필요없이 이전까지의 값을 버리고 현재 값부터 counting해주면 된다.
    문제는 1)의 경우인데, 현재 값이 음수여도 뒤의 값에 따라 가져가는게 이득일 수도 있다.
    그런데 이전까지의 최대값과 지금 값을 더하고, 지금 값과 비교하기 때문에
    결국 지금 값이 음수여도 이전까지의 값이 양수라면 가져가게되고, 뒤에서 버릴지말지 판단하게된다. 
    그러므로 1)의 경우에도 문제가 없다.

    dp[] = 최대값을 담을 배열
    dp[i] = Math.max(arr[i], dp[i-1]+arr[i]); -> 최대값 = (이전까지의 최대값 + 지금 값) vs (지금 값)

     

     

    Github: https://github.com/jaeuk9407/AlgorithmBOJ

     

    jaeuk9407/AlgorithmBOJ

    BaekJoon Online Judge Problems. Contribute to jaeuk9407/AlgorithmBOJ development by creating an account on GitHub.

    github.com

     

    댓글