알고리즘/배낭문제(DP)

BOJ 백준 5444 시리얼 넘버

kyjdummy 2025. 4. 7. 17:01

문제 : https://www.acmicpc.net/problem/5444

 

[ 문제 요약 ]

- 기타 시리얼 넘버 값이 주어질 때, 시리얼 넘버의 합이 M의 배수가 되는 것들 중 기타 개수의 최대치를 출력하는 것

 

[ 테스트 케이스 설명 ]

2	// 테스트 케이스
3 5	// 기타개수(1<=500), 기준 값(1<=100,000)
1 8 6	// 시리얼 넘버(0<=100,000)
6 9	// 테케 반복
8 6 4 1 2 3

 

 

[ 문제 풀이 ]

 

시간 복잡도를 생각하지 않고 단순 dp를 선언한다면 아래와 같이 선언할 수 있습니다.

 

dp[기타 시리얼 넘버의 합] = 최대 기타 개수

 

하지만 위처럼 만들면 시간 초과입니다.

 

이유는 기타 시리얼 넘버의 합이 최대 5천만이고, 기타가 최대 500개 있을 수 있어서

 

2중 for 문으로 만들면 아래와 같은 코드가 되고 최악의 경우 250억 번 반복합니다.

 

 

[ 시간 초과 코드 ]

 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

class Main{
	public static void main(String[] args)throws Exception{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringBuilder sb = new StringBuilder();
		int T = Integer.parseInt(br.readLine());	// 테스트케이스
		while(T-->0)
		{
			StringTokenizer st = new StringTokenizer(br.readLine());
			int N	= Integer.parseInt(st.nextToken());	// 기타개수(1<=500)
			int M	= Integer.parseInt(st.nextToken());	// 기준 값(1<=100,000)
			int S[] = new int[N + 1];					// 기타의 시리얼 넘버(0<=100,000)
			int total = 0;
			
			st = new StringTokenizer(br.readLine());
			for(int i=1; i<=N; i++)
				total += S[i] = Integer.parseInt(st.nextToken());
			
			int dp[] = new int[total + 1];
			
			dp[0] = 1;
			// 최악의 경우 25,000,000,000
			for(int i=1; i<=N; i++)					// 최대 500
				for(int j=total; j>=S[i]; j--)		// 최대 50,000,000
					if(dp[j - S[i]] != 0)
						dp[j] = Math.max(dp[j - S[i]] + 1, dp[j]);
			
			int max = 0;
			for(int m=M; m<=total; m+= M)
				max = Math.max(max, dp[m]);
			
			sb.append(max - 1).append('\n');
		}
		System.out.print(sb);
	}
}

 

 

시간을 줄이기 위해서 시리얼 넘버의 합을 M 초과로 두지 말고, M으로 나눈 나머지로 하면 문제 해결이 가능합니다. 

 

dp[시리얼 넘버 합의 나머지] = 기타 최대 개수

 

그리고 이 문제에 대해서 알아둬야 할 것은 시간 복잡도뿐만 아니라 공간 복잡도도 생각해야 합니다. 

 

무작정 2차원 배열 선언 시 메모리 초과입니다.

 

아래 코드처럼 만들어야 합니다. 설명은 주석으로 대신합니다.

 

[ 정답 코드 ]

 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

class Main{
	public static void main(String[] args)throws Exception{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringBuilder sb = new StringBuilder();
		int T = Integer.parseInt(br.readLine());	// 테스트 케이스
		while(T-->0)
		{
			StringTokenizer st = new StringTokenizer(br.readLine());
			int N	= Integer.parseInt(st.nextToken());	// 기타 개수(1<=500)
			int M	= Integer.parseInt(st.nextToken());	// 기준 값(1<=100,000)
			int S[] = new int[N];						// 기타 시리얼 넘버(0<=100,000)
			
			st = new StringTokenizer(br.readLine());
			for(int i=0; i<N; i++)
				S[i] = Integer.parseInt(st.nextToken());
			
			// 현재 연산 기준이 될 dp와, dp배열을 통해 만들어갈 nextDp선언
			int dp[]	= new int[M];
			int nextDp[]= new int[M];
			
			// 두 배열의 값을 처음에 모두 -1을 넣어 유효하지 않은 값으로 만들어 줍니다.
			for(int i=0; i<M; i++)
				dp[i] = nextDp[i] = -1;
			
			// dp[0] 을 0으로 초기화 하여 유효한 값으로 만들어줌
			dp[0] = 0;
			// 기타 반복
			for(int s : S)
			{
				// 시리얼 넘버의 합의 나머지 값을 반복
				for(int j=0; j<M; j++)
				{
					if(dp[j] < 0)	// 유효하지 않은 값이면 스킵
						continue;
					
					// 현재 나머지 값에서 s를 선택했을때 나머지 값
					int next = (j + s) % M;
					// 현재 dp를 통해 nextDp에 값을 갱신해나감 
					nextDp[next] = Math.max(nextDp[next], dp[j] + 1);
				}
				// 만들어진 nextDp를 dp값에 복사하여 연산반복
				for(int i=0; i<M; i++)
					dp[i] = nextDp[i];
			}
			// M의 배수이므로 dp[0] 값이 정답
			sb.append(dp[0])
				.append('\n');
		}
		System.out.print(sb);
	}
}