본문 바로가기

Dot Algo∙ DS/PS

[BOJ] 백준 2228번 구간 나누기 (Java)

    #2228 구간 나누기

    난이도 : 골드 5

    유형 : DP / 누적합

     

    2228번: 구간 나누기

    N(1≤N≤100)개의 수로 이루어진 1차원 배열이 있다. 이 배열에서 M(1≤M≤⌈(N/2)⌉)개의 구간을 선택해서, 구간에 속한 수들의 총 합이 최대가 되도록 하려 한다. 단, 다음의 조건들이 만족되어야

    www.acmicpc.net

    ▸ 문제

    N(1≤N≤100)개의 수로 이루어진 1차원 배열이 있다. 이 배열에서 M(1≤M≤⌈(N/2)⌉)개의 구간을 선택해서, 구간에 속한 수들의 총 합이 최대가 되도록 하려 한다. 단, 다음의 조건들이 만족되어야 한다.

    1. 각 구간은 한 개 이상의 연속된 수들로 이루어진다.
    2. 서로 다른 두 구간끼리 겹쳐있거나 인접해 있어서는 안 된다.
    3. 정확히 M개의 구간이 있어야 한다. M개 미만이어서는 안 된다.

    N개의 수들이 주어졌을 때, 답을 구하는 프로그램을 작성하시오.

     

     입력

    첫째 줄에 두 정수 N, M이 주어진다. 다음 N개의 줄에는 배열을 이루는 수들이 차례로 주어진다. 배열을 이루는 수들은 -32768 이상 32767 이하의 정수이다.

     

     출력

    첫째 줄에 답을 출력한다.

     

    문제 풀이 

    N개의 수로 이루어진 배열에서 M개 구간을 나눠 구간합의 최댓값을 구하는 문제이다. 

     

    📚 조건

    • N ( 1 <= N <= 100)
    • M ( 1 <= M <=⌈(N/2)⌉
    • 배열에 들어가는 수의 범위 ~32768 ~32767
    • 구간이 겹쳐있거나 인접해있어서는 안된다.

     

    하향식 풀이로 구간이 M개를 나누어지는 순간 최대의 합을 가지는 구간합을 구하면 된다.

    누적합을 통해 구간 합 구하기

    1. 주어진 구간 데이터를 누적 합으로 저장

    dp배열로 다룰 변수로는 배열 인덱스 idx, 구간 수 section으로 지정하고 입력받은 배열 값들을 누적합 배열로 저장한다.

    sum = new int[n+1];
    for(int i=1; i<n+1; i++) {
    	sum[i] = sum[i-1] + Integer.parseInt(br.readLine());	
    }

     

    2. dp배열 초기값 설정

    배열에 들어가는 수의 범위가 -32768 ~32767이기때문에 처음으로, Init값을 -33333으로 잡아버린 나는 당연히 음수 누적합이 -33333이 넘을 수 있다는 사실을 간과해버리고 있었다. 그러면 누적합의 값이 더 작을 수 있으므로 초기화 값이 될 수 없다. 

     

    그래서 최솟값 -32768과 n의 최대 크기 100을 곱하여 Init = -32768*100; 로 초기값을 설정해줬다.

     

    3. 구간 합의 최댓값 구하기

    완전탐색이 진짜 다른 조건들 하나도 생각안하고 그냥 top-down 재귀를 사용하면 되니깐 너무 편하다.

    아 그런데, 누적합이라서 재귀 위에 for문으로 감싸주었던 부분은 살짝 고민이 필요한 부분이었다.

    1. idx가 구간 section에 포함되지 않은 경우 idx-1의 최댓값과 동일하다.
      1. dp[idx][section] = solve(idx-1, section); 
    2. idx가 구간 section에 포함되는 경우 이제 다음 구간을 for문을 돌려 탐색하면 된다.
      1. 구간은 겹쳐있거나 인접해있어서는 안된다. 그래서 다음 재귀함수는 한 칸 뛰어서 i-2부터 탐색해주면 된다. 
      2. dp[idx][section] = max (dp[idx][section], max(dp[i-2][section-1]) + sum(arr[i] ~ arr[n]));

    dp[idx][section] = solve(idx-1, section);
    for(int i = idx; i>0; i--) {
    	dp[idx][section] = Math.max(dp[idx][section], solve(i-2, section-1)+(sum[idx]-sum[i-1]));
    }

     

    3-1. 방문 여부 체크

    마지막으로, 음수로 뒤덮인 누적합 배열에서 방문을 했는지 안했는지 int값으로 판단하는 것은 거의 불가능했다.

    if(dp[idx][section] ≠ Init) return dp[idx][section];  이런 조건들은 그냥 가볍게 무시되고 시간초과가 발생하였다.

     

    그래서 결국 그래프 탐색할 때 처럼, boolean함수로 처리하였다.

    if(checked[idx][section]) return dp[idx][section];

     

    이와 같은 풀이로 예제를 돌리면 dp배열의 값은 다음과 같다.

    배열 arr -1 3 1 2 4 -1
    누적합 sum -1 2 3 5 9 8

     

    구간 개수 \ 배열 idx 1 2 3 4 5 6
    1 -1 3 4 6 Init Init
    2 Init Init 0 5 9 9

     

    풀이 코드 

    import java.io.*;
    import java.util.Arrays;
    import java.util.StringTokenizer;
    
    public class Main {
    
    	static int n,m;
    	static int[] sum;
    	static int[][] dp;
    	static boolean[][] checked;
    	static int Init = -32768*100;
    	public static void main(String[] args) throws IOException{
    		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    		StringTokenizer st = new StringTokenizer(br.readLine());
    		
    		n = Integer.parseInt(st.nextToken());
    		m = Integer.parseInt(st.nextToken());
    		
    		sum = new int[n+1];
    		for(int i=1; i<n+1; i++) {
    			sum[i] = sum[i-1] + Integer.parseInt(br.readLine());	
    		}
    
    		
    		// - 32768 ~ 32767
    		dp = new int[n+1][m+1];
    		checked = new boolean[n+1][m+1];
    		for(int i=0; i<n+1; i++) {
    			Arrays.fill(dp[i], Init);			
    		}
    		
    		System.out.println(solve(n,m));
    
    	}
    	
    	static int solve(int idx, int section) {
    		if(section == 0 ) return 0;		
    		if(idx < 0 ) return Init;
    		
    		if(checked[idx][section]) return dp[idx][section];
    		
    		checked[idx][section] = true;
    		dp[idx][section] = solve(idx-1, section);
    		for(int i = idx; i>0; i--) {
    			dp[idx][section] = Math.max(dp[idx][section], solve(i-2, section-1)+(sum[idx]-sum[i-1]));
    		}
    		
    		return dp[idx][section];
    		
    	}
    }