https://www.acmicpc.net/problem/1535

 

1535번: 안녕

첫째 줄에 사람의 수 N(≤ 20)이 들어온다. 둘째 줄에는 각각의 사람에게 인사를 할 때, 잃는 체력이 1번 사람부터 순서대로 들어오고, 셋째 줄에는 각각의 사람에게 인사를 할 때, 얻는 기쁨이 1번

www.acmicpc.net

코드설명

냅색(배낭, Knapsack) + DP(Dynamic Programming)문제입니다.

 

문제는 냅색 배낭 알고리즘입니다.

N의 범위가 작기때문에 DFS를 활용하여 해결했습니다.

문제에는 DP없이 사용하여도 해결할 수 있지만, N이 커진다면 어떻게 될까 하여 DP를 같이 적용해보았습니다.

 

이 문제에서 어떻게 냅색이 사용가능할지 확인해보겠습니다.

1. 세준이가 인사를 할때 : 이 경우 인사할 사람은 n-1명 남으며, 추가로 인사할 수 있는 최대 체력은 health - L[n]이 됩니다. 세준이가 인사한 후 기쁨은 J[n]만큼 증가하며, n-1명의 사람과 health - L[n]의 체력을 사용할 수 있는 세준이의 원래 문제가 남습니다.

2. 세준이가 인사하지 않을떄 : 이 경우 인사할 사람은 n-1명 남으며, 추가로 인사할 수 있는 체력은 그대로 health 입니다. 세준이가 인사했을 때의 기쁨도 그대로이며, n-1 명의 사람과 health만큼의 인사를 할 수 있는 체력이 있는 원래 문제가 남습니다.

 

즉, 각 상황에서 기쁨이 최대인 사람에게 인사를 하면 되며, 두 경우 모두 원래 문제와 같은 유형의 작은 문제가 남습니다.

 

이걸 재귀로 구현할시 시간복잡도는 O(2^N) 입니다. 각 함수마다 두개씩의 자식 노드를 가지고 있는 재귀 호출 구조라 생각하면 되며, 같은 하위 문제를 반복 계산하는 경우도 찾을 수 있습니다.

 

다이나믹 프로그래밍을 적용하기 위해서, 인사 후 최대 기쁨을 저장하는 방법을 찾는 것 입니다.이를 위해 값을 결정하는 변수들의 차원만큼의 행렬에 대응하는 배열을 사용합니다.

 

이 문제에는 세준이의 체력과 인사할 사람, 두 개의 변수가 있습니다. 각 행이 인사할 사람을 나타내고, 각 열이 체력을 나타내는 행렬을 생각해봅시다. 이 경우 각 행렬의 셀 (r, c)는 사람들 중 r번쨰까지의 사람에게 인사했을 때 체력 c인 상태에서의 최대 기쁨을 저장하면 됩니다. 이 행렬에는 물건이 0개일 때와 체력이 0일떄의 값도 저장해야 하므로, 행렬에 대응하는 배열은 아래와 같이 선언해야합니다.

int cache[N+1][100+1]

셀 (r,c)의 값, 즉 체력의 용량이 100이고 i번쟤 사람에까지의 최대 기쁨은 다음 둘 가운데 큰 값입니다.

1. i번째 사람에게 인사하지 않았을 때의 최대 기쁨 : cache[r - 1][c]  

2. i번째 사람에게 인사했을 때의 최대 기쁨 : J[r-1] + cache[r - 1][c-L[r - 1]]

이 됩니다.

사람 r을 인사하려면, 체력에는 c - L[r-1]의 체력만 들어있어야 하므로 사람 r의 가격에 이미 계산한 cache[r-1][c-L[r-1]]의 값을 더하면 됩니다.

 

이를 활용하여 바텀업 방식의 다이나믹 프로그래밍으로 세준이가 인사할 수 있는 최대의 기쁨을 계산합니다.

package Main;

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

public class Main {
    static int N, M, K;
    static int answer = 0;
    static int[] energy, happy; 
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        N = Integer.parseInt(st.nextToken());
        energy = new int[N];
        happy = new int[N];
        st = new StringTokenizer(br.readLine());
        for(int i=0; i<energy.length; i++) {
        	energy[i] = Integer.parseInt(st.nextToken());
        }
        st = new StringTokenizer(br.readLine());
        for(int i=0; i<energy.length; i++) {
        	happy[i] = Integer.parseInt(st.nextToken());
        }

        int[][] cache = new int[N+1][101];
        for(int r=0; r<N; r++) {
        	cache[r][0] = 0;
        }
        for(int c = 0; c < 101; c ++) {
        	cache[0][c]=0;
        }
        
        for(int i=1; i<=N; i++) {
        	for(int j=1; j<=100; j++) {
        		//i번째 사람에게 인사를 하지 않을경우
        		int ret = cache[i-1][j];
        		int ret2 = 0;
        		if(j - energy[i-1] > 0)
        			ret2 = happy[i-1] + cache[i-1][j - energy[i-1]];
        		cache[i][j] = Math.max(ret, ret2);
        	}
        }
        
        answer = cache[N][100];
        System.out.println(answer);
    }
    
}

위와 같이 접근한다면 시간복잡도는 O(100 * N)으로 계산할 수 있습니다. 

 

만약 역추적할경우

package Main;

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

public class Main {
    static int N, M, K;
    static int answer = 0;
    static int[] energy, happy; 
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        N = Integer.parseInt(st.nextToken());
        energy = new int[N];
        happy = new int[N];
        st = new StringTokenizer(br.readLine());
        for(int i=0; i<energy.length; i++) {
        	energy[i] = Integer.parseInt(st.nextToken());
        }
        st = new StringTokenizer(br.readLine());
        for(int i=0; i<energy.length; i++) {
        	happy[i] = Integer.parseInt(st.nextToken());
        }

        int[][] cache = new int[N+1][101];
        for(int r=0; r<N; r++) {
        	cache[r][0] = 0;
        }
        for(int c = 0; c < 101; c ++) {
        	cache[0][c]=0;
        }
        
        for(int i=1; i<=N; i++) {
        	for(int j=1; j<=100; j++) {
        		//i번째 사람에게 인사를 하지 않을경우
        		int ret = cache[i-1][j];
        		int ret2 = 0;
        		if(j - energy[i-1] > 0)
        			ret2 = happy[i-1] + cache[i-1][j - energy[i-1]];
        		cache[i][j] = Math.max(ret, ret2);
        	}
        }
        
        answer = cache[N][100];
        int maxEnergy = 100;
        for(int i=0; i<N; i++) {
        	if(cache[i][maxEnergy] == cache[i+1][maxEnergy]) {
        		
        	}else {
        		picked.add(i);
        		maxEnergy -= energy[i];
        	}
        }
        
        for(int v : picked) {
        	System.out.print(v+" ");
        }
        System.out.println();
        System.out.println(answer);
    }
    static ArrayList<Integer> picked = new ArrayList<>();
    
}

 

 

만약 위 문제를 그대로 재귀형식으로 바꾼다면,

package Main;

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

public class Main {
    static int N, M, K;
    static int answer = 0;
    static int[] energy, happy; 
    static int[][] cache;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        N = Integer.parseInt(st.nextToken());
        energy = new int[N];
        happy = new int[N];
        st = new StringTokenizer(br.readLine());
        for(int i=0; i<energy.length; i++) {
        	energy[i] = Integer.parseInt(st.nextToken());
        }
        st = new StringTokenizer(br.readLine());
        for(int i=0; i<energy.length; i++) {
        	happy[i] = Integer.parseInt(st.nextToken());
        }
        cache = new int[N+1][101];
        for(int i=0; i<N+1; i++) {
        	Arrays.fill(cache[i], -1);
        }
        
        answer = knapsack(0, 100);
        System.out.println(answer);
    }
    
    static int knapsack(int peopleIdx, int leftEnergy) {
    	if(peopleIdx == N) {
    		return 0;
    	}
    	if(cache[peopleIdx][leftEnergy] != -1) return cache[peopleIdx][leftEnergy];
    	int ret = 0;
    	if(leftEnergy - energy[peopleIdx] > 0) {
    		ret = knapsack(peopleIdx + 1, leftEnergy - energy[peopleIdx]) + happy[peopleIdx];
    	}
    	ret = Math.max(ret, knapsack(peopleIdx + 1, leftEnergy)); 
    	
    	return cache[peopleIdx][leftEnergy] = ret;
    }
}

 

역추적할경우

package Main;

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

public class Main {
    static int N, M, K;
    static int answer = 0;
    static int[] energy, happy; 
    static int[][] cache;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        N = Integer.parseInt(st.nextToken());
        energy = new int[N];
        happy = new int[N];
        st = new StringTokenizer(br.readLine());
        for(int i=0; i<energy.length; i++) {
        	energy[i] = Integer.parseInt(st.nextToken());
        }
        st = new StringTokenizer(br.readLine());
        for(int i=0; i<energy.length; i++) {
        	happy[i] = Integer.parseInt(st.nextToken());
        }
        cache = new int[N+1][101];
        for(int i=0; i<N+1; i++) {
        	Arrays.fill(cache[i], -1);
        }	

        answer = knapsack(0, 100);
        reconstruct(0, 100);
        
        for(int i=0; i<N+1; i++) {
        	for(int j=0; j<=100; j++) {
        		System.out.print(cache[i][j]+" ");
        	}
        	System.out.println();
        }
        System.out.println();
        for(int v : picked) {
        	System.out.print(v+" ");
        }
        System.out.println();
        System.out.println(answer);
    }
    
    static int knapsack(int peopleIdx, int leftEnergy) {
    	if(peopleIdx == N) {
    		return 0;
    	}
    	if(cache[peopleIdx][leftEnergy] != -1) return cache[peopleIdx][leftEnergy];
    	int ret = 0;
    	if(leftEnergy - energy[peopleIdx] > 0) {
    		ret = knapsack(peopleIdx + 1, leftEnergy - energy[peopleIdx]) + happy[peopleIdx];
    	}
    	ret = Math.max(ret, knapsack(peopleIdx + 1, leftEnergy)); 
    	
    	return cache[peopleIdx][leftEnergy] = ret;
    }
    static ArrayList<Integer> picked = new ArrayList<Integer>();
    static void reconstruct(int peopleIdx, int leftEnergy) {
    	//기저사례: 모든 물건을 다 고려했음
    	if(peopleIdx == N) {
    		return ;
    	}
    	
    	if(knapsack(peopleIdx, leftEnergy) == knapsack(peopleIdx + 1, leftEnergy)) {
    		reconstruct(peopleIdx + 1, leftEnergy);
    	}
    	else {
    		picked.add(peopleIdx);
    		reconstruct(peopleIdx + 1, leftEnergy - energy[peopleIdx]);
    	}
    }
}

 

 

 

코드

simulate에 happy값을 넣어서 계산하지 않고, 계산 도중에 happy 값을 처리합니다.

추가로 DP[level][health] : 해당 level, health에서의 최대 행복값을 저장합니다.

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

public class Main {
	public static int N, M;
	public static int answer = 0;
	public static int[][] DP;
	public static int[] L; //체력
	public static int[] J; //기쁨
	public static int health = 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());
    	DP = new int[21][101]; //21은 인사해야할 사람의 숫자, 101 세준이의 체력의 최대값을 의미합니다. 그리고 DP값은 기쁨의 최대입니다.
    	L = new int[N];
    	J = new int[N];
    	
    	st = new StringTokenizer(br.readLine());
    	for(int i=0;i<N;i++) {
    		L[i] = Integer.parseInt(st.nextToken());
    	}
    	
    	st = new StringTokenizer(br.readLine());
    	for(int i=0; i<N;i++) {
    		J[i] = Integer.parseInt(st.nextToken());
    	}
    	answer = simulate(0, 0);
    	System.out.println(answer);
	}
	
	public static int simulate(int level, int healthValue) {
		if(DP[level][healthValue] > 0) return DP[level][healthValue];
		if(level == N) return 0; //범위를 벗어납니다.
		
		int J1 = 0; //기쁨정도
		if(healthValue + L[level] < health) { //만약 최대체력범위 100 미만이라면
			J1 = J[level] + simulate(level + 1, healthValue + L[level]); //해당 즐거움을 더하는경우  
		}
		int J2 = simulate(level + 1, healthValue + 0); //해당 경우를 더하지 않은경우 
		return DP[level][healthValue] = Math.max(J1, J2);
	}
	
}

 

simulate인자에 level과 health 값을 넣어서 계산합니다. 

완전탐색을 통한 로직입니다.

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

public class Main {
	public static int N, M;
	public static Node[] node;
	public static int answer = 0;
	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());
		node = new Node[N];
		for(int i=0;i<N;i++) {
			node[i] = new Node();
		}
		
		st = new StringTokenizer(br.readLine());
		for(int i=0;i<N;i++) {
			int input = Integer.parseInt(st.nextToken());
			node[i].health = input;
		}
		
		st = new StringTokenizer(br.readLine());
		for(int i=0;i<N;i++) {
			int input = Integer.parseInt(st.nextToken());
			node[i].happy = input;
		}
		
		answer = simulate(0, 100);
		System.out.println(answer);
	}
	
	public static int simulate(int level, int health) {
		if(level >= N) {
			return 0;
		}
		
		int resultA = 0, resultB = 0;
		Node temp = node[level];
		
		if(health - temp.health > 0) {
			resultA = temp.happy + simulate(level + 1, health - temp.health);
		}
		resultB = simulate(level + 1, health);
		return Math.max(resultA,  resultB);
	}
}

class Node{
	int health;
	int happy;
	public Node(int health, int happy) {
		this.health = health;
		this.happy = happy;
	}
	public Node() {
		
	}
}

 

 

아래의 코드에 DP를 추가해보려고 한 코드이지만, DP[level][health][happy] : 현재 Level에서 해당 Health를 가지고 있을떄 가질 수 있는 최대 행복값을 구해야합니다. (만약 위의 코드와 같이 level, healthValue가 함수의 인자로 들어가 있다면, DP[level][health] 로 처리해야합니다.)

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

public class Main {
	public static int N, M;
	public static Node[] node;
	public static int answer = 0;
	public static int[][][] dp;
	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());
		node = new Node[N];
		dp = new int[21][100 + 1][20*100 + 1];
		for(int i=0;i<N;i++) {
			node[i] = new Node();
		}
		
		st = new StringTokenizer(br.readLine());
		for(int i=0;i<N;i++) {
			int input = Integer.parseInt(st.nextToken());
			node[i].health = input;
		}
		
		st = new StringTokenizer(br.readLine());
		for(int i=0;i<N;i++) {
			int input = Integer.parseInt(st.nextToken());
			node[i].happy = input;
		}
		
		answer = simulate(0, 100, 0);
		System.out.println(answer);
	}
	
	public static int simulate(int level, int health, int happy) {
		if(dp[level][health][happy] > 0) {
			return dp[level][health][happy];
		}
		if(level >= N || health < 0 ) {
			return happy;
		}
		
		int resultA = 0, resultB = 0;
		Node temp = node[level];
		
		//해당 level의 사람에게 인사하는 경우. ( 체력이 남아있어야한다. 처음에 0 인경우까지 포함했는데 체력이 0 이면 인사를 못합니다. )
		if(health - temp.health > 0) {
			resultA = simulate(level + 1, health - temp.health, happy + temp.happy);	
		}
		
		//해당 level의 사람에게 인사하지 않는 경우
		resultB =  simulate(level + 1, health, happy);
		
		return dp[level][health][happy] = Math.max(resultA, resultB);
	}
}

class Node{
	int health;
	int happy;
	public Node(int health, int happy) {
		this.health = health;
		this.happy = happy;
	}
	public Node() {
		
	}
}

 

단순히 완전탐색을 통해서 해결한 코드입니다.

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

public class Main {
	public static int N, M;
	public static Node[] node;
	public static int answer = 0;
	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());
		node = new Node[N];
		for(int i=0;i<N;i++) {
			node[i] = new Node();
		}
		
		st = new StringTokenizer(br.readLine());
		for(int i=0;i<N;i++) {
			int input = Integer.parseInt(st.nextToken());
			node[i].health = input;
		}
		
		st = new StringTokenizer(br.readLine());
		for(int i=0;i<N;i++) {
			int input = Integer.parseInt(st.nextToken());
			node[i].happy = input;
		}
		
		answer = simulate(0, 100, 0);
		System.out.println(answer);
	}
	
	public static int simulate(int level, int health, int happy) {
		if(level >= N || health < 0 ) {
			return happy;
		}
		
		int resultA = 0, resultB = 0;
		Node temp = node[level];
		//해당 level의 사람에게 인사하는 경우. ( 체력이 남아있어야한다. )
		if(health - temp.health > 0) {
			resultA = simulate(level + 1, health - temp.health, happy + temp.happy);	
		}
		
		//해당 level의 사람에게 인사하지 않는 경우
		resultB =  simulate(level + 1, health, happy);
		
		return Math.max(resultA, resultB);
	}
}

class Node{
	int health;
	int happy;
	public Node(int health, int happy) {
		this.health = health;
		this.happy = happy;
	}
	public Node() {
		
	}
}

 

메모이제이션을 적용하여 가장 빠른 코드입니다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;
 
public class Main {
	private static int N, K, M;
    private static int answer = 0;
    private static int[] arr, L, J;
    private static int[][] cache;
    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());
        L = new int[N]; J = new int[N];
        st = new StringTokenizer(br.readLine());
        for(int i=0;i<N; i++) {
        	L[i] = Integer.parseInt(st.nextToken());
        }
        st = new StringTokenizer(br.readLine());
        for(int i=0;i<N; i++) {
        	J[i] = Integer.parseInt(st.nextToken());
        }
        
        cache = new int[101][N];
        for(int i=0;i<101; i++) {
        	Arrays.fill(cache[i], -1);
        }
        System.out.println(getBestHappy(100, 0));
    }
    
    public static int getBestHappy(int energy, int personIdx) {
    	if(personIdx == N) {
    		return 0;
    	}
    	if(cache[energy][personIdx] != -1) return cache[energy][personIdx];
    	
    	int ret = 0;
    	ret = getBestHappy(energy, personIdx + 1);
    	if(energy - L[personIdx] > 0) {
    		ret = Math.max(ret, getBestHappy(energy - L[personIdx], personIdx + 1) + J[personIdx]);
    	}
    	return cache[energy][personIdx] = ret;
    }
    
    
}

 

만약, 내가 인사한 사람들의 최대 선호도와 함께, 인사한 사람들의 실제 목록이 궁금할경우의 코드입니다.

reconstruct 함수를 통해서 실제 어떤 사람들에게 인사하였는지 출력할 수 있습니다.

package Main;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.StringTokenizer;
 
public class Main {
	private static int N, K, M;
    private static int answer = 0;
    private static int[] arr, L, J;
    private static int[][] cache;
    private static ArrayList<Integer> picked = new ArrayList<>();
    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());
        L = new int[N]; J = new int[N];
        st = new StringTokenizer(br.readLine());
        for(int i=0;i<N; i++) {
        	L[i] = Integer.parseInt(st.nextToken());
        }
        st = new StringTokenizer(br.readLine());
        for(int i=0;i<N; i++) {
        	J[i] = Integer.parseInt(st.nextToken());
        }
        
        cache = new int[101][N];
        for(int i=0;i<101; i++) {
        	Arrays.fill(cache[i], -1);
        }

        System.out.println(getBestHappy(100, 0));
        reconstruct(100, 0);
        for(int v : picked) {
        	System.out.print(v+" ");
        }
    }
    
    public static int getBestHappy(int energy, int personIdx) {
    	if(personIdx == N) {
    		return 0;
    	}
    	if(cache[energy][personIdx] != -1) return cache[energy][personIdx];
    	
    	int ret = 0;
    	ret = getBestHappy(energy, personIdx + 1);
    	if(energy - L[personIdx] > 0) {
    		ret = Math.max(ret, getBestHappy(energy - L[personIdx], personIdx + 1) + J[personIdx]);
    	}
    	return cache[energy][personIdx] = ret;
    }
    
    public static void reconstruct(int energy, int personIdx) {
    	//기저사례 : 모든 사람들을 다 고려했음.
    	if(personIdx == N) return ;
    	
    	//두개의 선택지밖에 없기에 아래와 같이 단순하게 처리할 수 있습니다.
    	//만약 현재 energy와 personIdx와 energy,personIdx+1 때의 행복도가 같다면, 
    	//해당 사람에게는 인사하지 않았다는 의미입니다. ( 인사한 경우라면 [energy][personIdx]의 값은 달라졌어야하기 때문입니다. )
    	//만약 둘이 같다면, 인사를 하지 않아서 같습니다. 그러므로 다음 재귀함수로 넘어갑니다.
    	if(getBestHappy(energy, personIdx) == getBestHappy(energy, personIdx + 1)) {
    		reconstruct(energy, personIdx + 1);
    	}
    	//위의 경우에서 다른 사람에게 인사한 값이 최대값인걸 알았다면, 
    	//해당 사람에게는 인사한 것으로 처리하고, 다음 재귀함수를 계속해서 실행합니다.
    	else if(energy - L[personIdx] > 0) {
    		picked.add(personIdx);
    		reconstruct(energy - L[personIdx], personIdx + 1);
    	}
    }
    
    
}

 

 

+ Recent posts