Programmers - 매출 하락 최소화
Programmers 의 매출 하락 최소화 문제 풀이
문제
트리 구조에서 자식 노드를 가진 팀장 과 부모 노드를 가진 팀원 으로 나뉜다. 각 팀에서 한 명씩 불러야 할 때, 불러낸 각 노드의 매출합을 최소화 하는 문제입니다. 두 팀에 속하는 노드를 부를 경우 그 노드만으로 두 팀에서 각출한 것으로 치는 조건이 있습니다.
보통 트리가 등장하는 문제는 root 나 leaf 에서 어떻게 풀어야 할 지 고민하다 보면 해답이 보이는데, 이 문제는 쉽게 구상이 되지 않았습니다.(그러고보니 Level 4 문제는 처음 풀어보기도 했고…)
이 문제가 어려웠던 이유는 단순한 트리 문제처럼 모든 노드를 단순 순회하는 것으로 문제가 풀리지 않기 때문입니다. 이번 문제는 각 노드를 순회하되, 그 노드를 선택할지 말지도 고민됩니다. 어떤 노드를 선택하게 되면 전체적인 결과에도 영향을 미치기 때문에, 경우의 수를 나누고 그 경우를 기록해둬야 했습니다.
경우의 수를 기록하는 기법을 메모이제이션 (memoization), 타뷸레이션 (tabulation) 혹은 이런 개념을 포함하는 동적 계획법 (dynamic programming) 이라고 합니다. 이제 중요한 건 어떤 것을 기록해야 하는지 와 기록한 내용을 토대로 어떻게 구현해야 할 지가 되겠습니다.
해답 (2차원 배열 메모)
동적 계획법으로 풀어야 한다는 것을 알고 나서, 메모이제이션을 활용하기로 했습니다. 그러면 뭘 메모해둬야 할까요?
이 문제의 트리를 단순한 경우만 가지고 생각해보면, 어떤 subtree 에서의 정답이 루트 노드가 포함 되냐 마냐에 따라서만, 전체 트리일 때 정답에 영향을 미치는 것을 알 수 있습니다. 팀장 노드에서 팀원 노드 전부가 참여하지 않는 경우엔 반드시 팀장 노드가 참여해야 한다는 조건이 있기 때문입니다. (생각해보면 이외의 특별한 조건은 없다는 것을 알 수 있습니다.)
따라서 메모를 두 가지 합니다. (따라서 메모를 위한 데이터는 2차원 배열로 합니다.)
- 해당 노드가 참여했을 때, subtree의 정답
- 해당 노드가 불참했을 때, subtree의 정답
다시 말하지만 이 두가지 외엔 상위트리가 고려할 내용이 없다는 것이 중요합니다. 무엇을 메모해야 하는지가 정해지면 해답이 자연스럽게 보이기 때문입니다. 이를 이용한 코드는 아래와 같습니다.
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
import java.util.ArrayList;
class Solution {
static ArrayList<Integer>[] edges;
static int[] nodes;
public int solution(int[] sales, int[][] links) {
nodes = new int[sales.length + 1]; // node list
for(int i = 1; i < nodes.length; i++){
nodes[i] = sales[i-1];
}
edges = new ArrayList[sales.length + 1];
for(int i = 0; i < nodes.length; i++){
edges[i] = new ArrayList<Integer>();
}
for(int[] arr : links){
edges[arr[0]].add(arr[1]);
}
dp = new int[nodes.length][2];
calc(1);
int answer = Math.min(dp[1][0], dp[1][1]); // CEO 포함인 경우 vs 미포함인 경우
return answer;
}
static int[][] dp;
// case = 0 팀장(자신) 선택
// case = 1 팀원 선택
private void calc(int node){
for(int child : edges[node]){
calc(child);
}
// postorder
int childSum = 0;
int absenceSum = 0;
for(int child : edges[node]){
childSum += Math.min(dp[child][0], dp[child][1]); // 일단 작은거 더하는데...
absenceSum += dp[child][1];
// 둘 중에 작은 것 더하면 되는 거 아니야?
// 그러다가 child 가 전부 선택 안되면 안되거든. (최소 하나는 선택 되어야 함.)
}
dp[node][0] += nodes[node] + childSum; // 팀장이 선택되면 전부 선택 안되도 상관 없음.
if(childSum == absenceSum && edges[node].size() != 0) { // 팀장이 선택 안되는 경우 혹시 child 가 전부 선택이 안되는 케이스를 위해
childSum = Integer.MAX_VALUE;
for(int presentChild : edges[node]){
int tmpSum = 0;
for(int child : edges[node]){
if(presentChild == child) tmpSum += dp[presentChild][0];
else tmpSum += dp[child][1];
}
childSum = (childSum > tmpSum)? tmpSum : childSum;
}
}
dp[node][1] += childSum;
}
}
이 코드로 푸는데 성공 했지만, 1차원 메모로도 풀 수 있다는 것을 발견해서, 추가로 써봅니다.
해답 (1차원 메모)
Programmers 에 제출된 Java 해답 중 맨 위의 답안이 1차원 메모만 쓰고 있었습니다. 제가 위에 코드를 짤 때 생각하기론 아래 두가지 경우를 상위 트리가 고려해줘야 했습니다.
- 해당 노드가 참여했을 때, subtree의 정답
- 해당 노드가 불참했을 때, subtree의 정답
그래서 한 노드당 두가지 정보를 가지는 2차원 트리를 선택했는데, 1차원 메모만 쓰는 답안이 있어서 깜짝 놀랐어요. 코드를 곰곰히 살펴보니, 메모는 한가지만 고려하고 있습니다. 그렇다는 건…
- (노드 선택 했든 말든) subtree의 정답
이것만 고려해도 괜찮았다는 건데, 코드를 보면서 고민을 좀 해보니 그렇긴 했습니다. 사실 이 풀이에서도 노드를 선택하는 경우와 하지않는 경우를 고려하긴 했습니다. 상위 트리에서, 다음 두 가지 상황을 고려하고 있습니다.
- 팀장을 선택하지 않았을 때, 팀원 중 하나를 반드시 선택하고, 그 팀원의 하위 팀원에서 메모를 가져온다.
- 팀장을 선택한 경우엔 따로 신경 쓸 조건이 없으므로, 단순하게 팀원들의 메모를 가져온다.
이렇게 짜 놓으면 앞서 설명한 것 처럼 상위 트리가 팀원의 메모 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
import java.util.*;
class Solution {
int memos[];
public int solution(int[] sales, int[][] links) {
Node nodes[] = new Node[sales.length+1];
for(int i = 0; i < sales.length; i++)
nodes[i+1] = new Node(sales[i], i+1);
for(int[] link : links)
nodes[link[0]].add(nodes[link[1]]);
memos = new int[sales.length+1];
Arrays.fill(memos, -1);
return recursion(nodes[1]);
}
int recursion(Node node){
if(node == null) return 0;
if(memos[node.num] != -1)
return memos[node.num];
if(node.childs.size() == 0) return 0;
int min = Integer.MAX_VALUE>>1;
int sum = 0;
for(int i = 0; i < node.childs.size(); i++){
sum = 0;
for(int j = 0; j < node.childs.size(); j++){
if(i == j) continue;
sum += recursion(node.childs.get(j));
}
for(Node child : node.childs.get(i).childs) {
sum += recursion(child);
}
min = Math.min(min, sum+node.childs.get(i).sales);
}
sum = 0;
for(Node child : node.childs)
sum += recursion(child);
min = Math.min(min, sum+node.sales);
memos[node.num] = min;
return min;
}
class Node{
int sales;
int num;
List<Node> childs = new ArrayList<>();
Node(int sales, int num){
this.sales = sales;
this.num = num;
}
void add(Node node){
childs.add(node);
}
}
}