문제 : https://www.acmicpc.net/problem/17831
17831번: 대기업 승범이네
첫 번째 줄에 판매원들의 수 N(2 ≤ N ≤ 200,000)이 주어진다. 판매원들은 1번, 2번, …, N번으로 번호가 매겨지며, 승범이는 항상 1번이다. 두 번째 줄에 2번 판매원부터 N번 판매원의 사수가 순서대
www.acmicpc.net
문제 파악하기
승범이네 회사의 사원들 중 멘토와 멘티를 적절하게 선택해서 최대의 시너지 효과를 구하는 문제입니다. 승범이네 회사 구조는 트리 구조로 되어있으며 사수와 부사수 관계를 가지고 있는 사원을 멘토-멘티로 선정할 수 있습니다. 결국 이 문제는 현재 노드와 자식 노드를 멘토-멘티로 선정할 것인지, 아니면 현재 노드를 제외할지 탐색해야 하는 문제로 바꿀 수 있습니다. 그리고 이런 경우에는 다이나믹 프로그래밍을 사용하면 쉽게 문제를 해결할 수 있습니다.
문제 해결하기
문제를 해결하기 위한 배열 DP를 다음과 같이 정의해봅시다.
DP[idx][used] : 현재(idx) 노드가 사용되었는지(0/1) 여부에 따라 가질 수 있는 시너지의 최댓값
used는 현재 노드의 사용 여부를 의미합니다. 0인 경우에는 idx를 이전에 사용하지 않았다는 의미로, 자식 노드 중 하나를 선택해서 멘토-멘티로 선정할 수 있음을 의미합니다. 1인 경우에는 이미 부모 노드와 멘토-멘티 관계를 가지고 있다는 의미로, 어떤 자식 노드와도 멘토-멘티를 선정할 수 없음을 의미합니다. 이렇게 used의 값을 설정하면 사실 알고리즘은 이미 나왔습니다. used의 값에 따라 자식 노드와 멘토-멘티를 선정하거나 선정하지 않는 값 중 최댓값을 DP[idx][used]에 저장하면 됩니다! 이렇게 배열의 의미를 명확하게 정의하면 알고리즘 또한 쉽게 유추할 수 있습니다.
그렇다면 이제 알고리즘을 만들어봅시다. 우선, used의 값과 상관 없이 현재 노드(idx)를 선택하지 않는 경우는 항상 탐색하게 됩니다. 그렇기에 DP 배열에 자식 노드로 넘어가는 값을 모두 더해 초깃값을 만들어봅시다.
DP[idx][used] = sum( sv(child1, 0), sum(child2, 0), ... ,sum(childN, 0) );
현재 노드(idx)의 모든 자식 노드를 선택하지 않았을 때의 값을 모두 더하면, 우리가 원하는 현재 노드(idx)를 선택하지 않았을 때 가질 수 있는 자식 노드 사이의 시너지의 최댓값을 구할 수 있습니다. 그럼 이번에는 현재 노드(idx)를 선택한 경우에 대해 알아봅시다. 단, 현재 노드를 선택할 수 있는 경우는 항상 used의 값이 0인 경우입니다. 현재 노드(idx)를 선택하여 멘토-멘티를 만들었을 때 중요한 점은 선택하지 않은 자식 노드들의 시너지 값 역시 더해주어야 한다는 점입니다. 따라서, 이 경우에는 다음과 같이 수식을 사용하면 우리가 원하는 결괏값을 얻을 수 있습니다.
DP[idx][used] = max( DP[idx][used], (tot-sv(childT, 0)) + sv(childT, 1)+pf[idx]*pf[childT] );
tot는 이전에 구한 DP[idx][used]의 값, 즉 모든 자식 노드들의 시너지 값의 합을 의미합니다. 그리고 pf배열은 사원 개개인의 실력을 의미합니다. 위의 수식은 결국 현재 노드(idx)와 자식 노드(childT)를 멘토-멘티로 만들어 시너지(pf[idx]*pf[childT])를 만들었을 때의 최댓값을 저장하는 수식입니다. 이렇게 현재 노드(idx)가 가질 수 있는 모든 경우를 메모이제이션을 활용하여 효과적으로 탐색하면 문제를 해결할 수 있습니다.
소스코드
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
|
#include <stdio.h>
#include <string.h>
#include <vector>
#include <algorithm>
#define NMAX 200010
using namespace std;
int N, p;
int pf[NMAX];
vector< int > tree[NMAX];
int dp[NMAX][2];
int sv(int idx, int used) {
if(dp[idx][used] >= 0) return dp[idx][used];
int tmp, ret=0;
// idx 노드를 사용하지 않는 경우
for(int child:tree[idx]) ret += sv(child, 0);
// idx 노드를 사용하는 경우
if(!used) {
tmp = ret;
for(int child: tree[idx]) {
ret = max(ret, (tmp-sv(child, 0)) + sv(child, 1) + pf[idx]*pf[child]);
}
}
return dp[idx][used] = ret;
}
int main() {
// 입력
scanf("%d", &N);
for(int i=2;i<=N;i++) {
scanf("%d", &p);
tree[p].push_back(i);
}
for(int i=1;i<=N;i++) scanf("%d", &pf[i]);
// 탐색
memset(dp, -1, sizeof(dp));
printf("%d", max(sv(1, 0), sv(1, 1)));
}
|
cs |
후기
트리를 사용하는 다이나믹 프로그래밍 중 기초적인 문제입니다. 다만, 다른 쉬운 문제들과는 사뭇 다르게 모든 자식 노드의 값을 더해주어야 한다는 점만 유의한다면 어렵지 않게 풀 수 있습니다. 트리를 사용한 구조 및 다이나믹 프로그래밍을 연습할 때 추천하고 싶은 문제입니다.
'문제 노트 > 백준' 카테고리의 다른 글
불만 정렬( BOJ 5012 ) (0) | 2022.03.01 |
---|---|
숨바꼭질 5( BOJ 17071 ) (0) | 2022.02.26 |
Degree Bounded Minimum Spanning Tree( BOJ 20927 ) (0) | 2022.02.11 |
Optimization is Freaky Fun( BOJ 17417 ) (0) | 2022.02.05 |
Celebrity( BOJ 23259 ) (0) | 2022.01.28 |