※ 주의!! 이 문제는 사람의 인내심과 정신력을 시험합니다!! 어쭙잖은 각오로 이 문제를 도전하지 마세요.
문제
문제가 상당히 길다. 전문을 보고 싶은 사람은 코드트리에서 확인하자.
코드트리 | 코딩테스트 준비를 위한 알고리즘 정석
국가대표가 만든 코딩 공부의 가이드북 코딩 왕초보부터 꿈의 직장 코테 합격까지, 국가대표가 엄선한 커리큘럼으로 준비해보세요.
www.codetree.ai
요약하자면 아래와 같다. 총 다섯 가지 동작을 수행해야 한다.
(1) 사내 메신저 준비 : 이진트리 구조의 사내 메신저를 초기화합니다.
(2) 알림망 설정 ON / OFF : 처음 모든 채팅방의 알림망 설정은 켜져있습니다. 이 기능이 작동되면 번 채팅방의 알림망 설정이 ON 상태라면 OFF로 바꿔주고, OFF 상태라면 ON으로 바꿔줍니다. 알림망 설정이 OFF 상태가 되면 자기 자신을 포함하여 아래에서 올라온 모든 알림을 더 이상 위로 올려 보내지 않습니다.
(3) 권한 세기 변경 : 번 채팅방의 권한 세기를 로 변경합니다.
(4) 부모 채팅방 교환 : 번 채팅방과 번 채팅방의 부모를 서로 바꿉니다. 이때 번 채팅방과 번 채팅방은 트리 내에서 같은 depth상에 있음을 가정해도 좋습니다.
(5) 알림을 받을 수 있는 채팅방 수 조회 : 메세지를 보냈을 때 번 채팅방까지 알림이 도달할 수 있는 서로 다른 채팅방의 수를 출력
입력
첫 번째 줄에 채팅방의 수 과 명령의 수 가 공백을 사이에 두고 주어집니다.
두 번째 줄부터는 개의 줄에 걸쳐 명령의 정보가 주어집니다. 각 명령에 따른 형태는 다음과 같습니다.
- 사내 메신저 준비
- 100 p1 p2 ... pN a1 a2 .... aN 형태로 공백을 사이에 두고 주어집니다. 1번부터 번까지 각 채팅방의 부모 채팅방 번호가 순서대로 부터 까지고, 초기 권한 세기가 각각 부터 까지임을 뜻합니다. 이 명령은 항상 첫 번째 명령으로만 주어지며, 항상 주어집니다. 0번 채팅방의 값과 값은 주어지지 않음에 유의합니다.
- 알림망 설정 ON / OFF
- 200 c 형태로 공백을 사이에 두고 주어집니다. 이는 번 채팅방의 알림망 설정이 ON 상태라면 OFF로 바꿔주고, OFF 상태라면 ON 상태로 바꿔줘야 함을 의미합니다.
- 권한 세기 변경
- 300 c power 형태로 공백을 사이에 두고 주어집니다. 이는 번 채팅방의 권한 세기를 로 변경해야 함을 의미합니다.
- 부모 채팅방 교환
- 400 c1 c2 형태로 공백을 사이에 두고 주어집니다. 이는 번 채팅방과 번 채팅방의 부모를 변경해야 함을 의미합니다. 이때 번 채팅방과 번 채팅방은 트리 내에서 같은 depth상에 있음을 가정해도 좋습니다.
- 알림을 받을 수 있는 채팅방 수 조회
- 500 c 형태로 공백을 사이에 두고 주어집니다. 이 경우 메세지를 보냈을 때 번 채팅방까지 알림이 도달할 수 있는 서로 다른 채팅방의 수를 출력합니다. 단, 이때 번 채팅방을 제외한 채팅방의 수를 세어야 함에 유의합니다.
- 1 ≤ ≤ 100,000
- 1 ≤ ≤ 100,000
- 1 ≤ 주어지는 이진트리의 최대 깊이() ≤ 20
- 0 ≤ ≤
- 1 ≤ ≤
출력
500 명령어에 대해 번 채팅방까지 알림이 도달할 수 있는 서로 다른 채팅방의 수를 한 줄에 하나씩 출력합니다.
문제 풀이
시간 제한의 늪에 빠져서 5시간을 허우적거린 문제였다. 결국 푸는데 성공했지만... 5시간 걸려서 풀었으면 성공이라고 얘기할 수 있나..? 왜 시간 제한에 걸렸는지도 해설이 끝나고 설명해보겠다. 풀이를 시작해보자.
자료구조 정의 및 입출력
static class Node {
int id;
int authority;
boolean alram = true;
int parentId;
Node parent;
// 현재 노드가 전달할 수 있는 알람 수
// arrOfAlram[3] = 4 라면 위로 3개의 노드에 전달할 수 있는 알람이 4
int[] arrOfAlram = new int[21];
Node rightChild;
int rightMaxAuthority;
Node leftChild;
int leftMaxAuthority;
Node(int id, int authority, int parentId) {
this.id = id;
this.authority = authority;
this.parentId = parentId;
}
}
static Map<Integer, Node> nodeMap = new HashMap<>();
문제에서도 친절히 설명한대로 자료 구조는 이진 트리이다. 따라서 Node 클래스를 선언하고 rightChild와 leftChild를 받아 이진 트리를 구성하자. 트리의 구현을 연습했다면 이정도 자료구조를 생각하는 것은 상당히 간단할 것이다.
이 문제의 핵심은 현재 노드가 상위 노드로 전달할 수 있는 알람 수를 저장하는 것이다. 이유는 후술한다. 따라서 Node 클래스의 멤버 변수로 트리의 최대 뎁스(20) 크기의 arrOfAlram이라는 int형 배열을 선언한다.
N = Integer.parseInt(st.nextToken());
Q = Integer.parseInt(st.nextToken());
int c1 = 0;
int power = 0;
int c2 = 0;
while(Q-- > 0) {
st = new StringTokenizer(br.readLine());
int command = Integer.parseInt(st.nextToken());
switch (command) {
case 100:
init(st);
break;
case 200 :
c1 = Integer.parseInt(st.nextToken());
turn_alram(c1);
break;
case 300 :
c1 = Integer.parseInt(st.nextToken());
power = Integer.parseInt(st.nextToken());
change_authority(c1, power);
break;
case 400 :
c1 = Integer.parseInt(st.nextToken());
c2 = Integer.parseInt(st.nextToken());
change_parent(c1, c2);
break;
case 500:
c1 = Integer.parseInt(st.nextToken());
int count = getArlam(c1);
sb.append(count).append("\n");
}
}
System.out.println(sb);
각 명령(command)에 대해서 동작을 수행할 함수를 작성한다. 구조를 다 잡았으니 하나하나 구현해보자.
command 100 : 이진 트리 초기화 _ init()
static private void init(StringTokenizer st) {
// root 생성
nodeMap.put(0, new Node(0, 0, -1));
Node root = nodeMap.get(0);
// 다른 노드 생성만
for(int i= 1; i <= N; i++) {
nodeMap.put(i, new Node(i, 0, -1));
}
// 노드 연결
for(int i = 1; i <= N; i++) {
int parentId = Integer.parseInt(st.nextToken());
Node parent = nodeMap.get(parentId);
Node child = nodeMap.get(i);
if(parent.leftChild == null) {
parent.leftChild = child;
child.parent = parent;
child.parentId = parentId;
} else {
parent.rightChild = child;
child.parent = parent;
child.parentId = parentId;
}
}
// 권한 설정
for(int i = 1; i <= N; i++) {
int authority = Integer.parseInt(st.nextToken());
Node node = nodeMap.get(i);
node.authority = authority;
}
// ArrOfAlram 초기화
alramInit(root);
}
단순히 입력 받은 값에 이진트리를 만드는 과정이므로 어렵지 않다. 특별히 설명할 것은 없으므로 arrOfAlram 배열을 초기화하는 함수만 언급하고 넘어가겠다.
arrOfAlram 배열 초기화 _ alramInit()
static private void alramInit(Node now) {
if(now == null) {
return;
}
now.arrOfAlram = new int[21];
for(int i = 0; i <= now.authority && i <= 20; i++) {
now.arrOfAlram[i]++;
}
alramInit(now.leftChild);
alramInit(now.rightChild);
if(now.leftChild != null) {
// lefChilde의 n(1~20)번 알람 수가 현재 노드의 n - 1번 알람 수값에 반영
for(int i = 1; i <= 20; i++) {
now.arrOfAlram[i - 1] += now.leftChild.arrOfAlram[i];
}
}
if(now.rightChild != null) {
for(int i = 1; i <= 20; i++) {
now.arrOfAlram[i - 1] += now.rightChild.arrOfAlram[i];
}
}
}
혹시라도 파라미터로 넘어온 node가 null일 수 있으므로 가장 먼저 Null 체크를 해준다.
그 후 본인의 권한으로 상위 노드로 올려보낼 수 있는 만큼 배열에 1을 더해준다.
위 배열에서 id:8 노드의 arrOfAlram[2]의 값은 8노드 기준 두번째 상위 노드까지 본인의 알람을 전달할 수 있다는 의미가 된다.
id:4 노드 기준으로 다시 생각해보자.
본인의 권한에 의해서 4의 arrOfAlram은 위와같이 초기화된다. 이 때, 4는 7과 8에서 알람을 받게 될 것이다. 다음 과정을 반드시 이해하고 넘어가자.
4는 본인의 자식 노드인 7과 8번 노드에서 알람을 넘겨 받게 된다. 따라서, 4번 노드는 본인의 알람을 포함하여 총 3개의 알람이 울리고(arrOfAlram[0] == 3), 본인의 바로 한 단계 상위 노드인 1번 노드로는 2개의 알람을 전송하게 된다(arrOfAlram[1] == 2).
마지막으로 1번 노드를 확인하고 원리를 마무리하자.
1의 권한은 1이므로 본인의 arrOfAlram 배열을 다음과 같이 초기화한다.
이제 자식 노드로부터 알람의 수를 넘겨받자.
따라서 1번 노드는 본인의 알람을 포함하여 4개의 알람을 받고(arrOfAlram[0] == 4) 바로 상위 노드인 루트 노드로 3개의 알람을 보내게 된다(arrOfAlram[1] == 3).
command 500 : 받을 수 있는 알람 수는?
Node에 이렇게 상태를 저장하고 있는 arrOfAlram 배열이 있다면 500번 명령어의 연산은 단순히 O(1)로 끝날 수 있다.
static private int getArlam(int c) {
Node node = nodeMap.get(c);
return node.arrOfAlram[0] - 1;
}
본인의 알람은 답에서 제외하므로 1을 빼고 리턴하자.
command 200 : 알람 활성화/비활성화 _ turn_alram()
static private void turn_alram(int c) {
Node node = nodeMap.get(c);
node.alram = !node.alram;
// node부터 root까지 거슬러 올라가면서 arrOfAlram 재계산
recal(c);
}
특정 노드에 대해서 알람망 기능을 끄게 된다면 해당 노드를 루트로 하는 서브 트리의 알람은 기존의 이진 트리에 영향을 끼칠 수 없다. "단, 분리된 서브 트리 내에서는 새로운 알람 네트워크가 형성된 것에 유의하자."
이를 반영하여 arrOfAlram을 재계산할 수 있도록 새로운 함수를 작성하자.
변경된 상황을 반영하는 함수 _ recal()
static private void recal(int id) {
Node now = nodeMap.get(id);
while(now != null && now.id != 0) {
Arrays.fill(now.arrOfAlram, 0);
for(int i = 0; i <= now.authority && i <= 20; i++) {
now.arrOfAlram[i]++;
}
if(now.leftChild != null && now.leftChild.alram) {
// leftChild의 n(1~20)번 알람 수가 현재 노드의 n - 1번 알람 수값에 반영
for(int i = 1; i <= 20 ; i++) {
now.arrOfAlram[i - 1] += now.leftChild.arrOfAlram[i];
}
}
if(now.rightChild != null && now.rightChild.alram) {
for(int i = 1; i <= 20; i++) {
now.arrOfAlram[i - 1] += now.rightChild.arrOfAlram[i];
}
}
now = now.parent;
}
}
변경이 생긴 노드부터 재계산을 시작하여 상위 노드로 거슬러 올라간다. 연산은 노드가 루트에 도달했을 때 마무리한다(혹시 몰라서 널 체크도 추가했다).
단, 재계산 함수에서는 자식 노드의 알람 기능을 확인해야 한다. 자식 노드의 알람 기능이 켜져있을 때(alram == true)만 현재 노드로 결과를 반영하도록 설계하자.
command 300 : 권한 세기 변경 _ change_authority()
static private void change_authority(int c, int power) {
Node node = nodeMap.get(c);
node.authority = power;
recal(c);
}
recal함수를 정의해 두어서 300 명령어에 대해서는 크게 해줄 일이 없다.
command 400 : 부모 변경 _ change_parent()
static private void change_parent(int c1, int c2) {
Node node1 = nodeMap.get(c1);
Node node2 = nodeMap.get(c2);
Node parent1 = nodeMap.get(node1.parentId);
Node parent2 = nodeMap.get(node2.parentId);
if(parent1.leftChild == node1) {
parent1.leftChild = node2;
} else if(parent1.rightChild == node1) {
parent1.rightChild = node2;
}
node2.parentId = parent1.id;
node2.parent = parent1;
if(parent2.leftChild == node2) {
parent2.leftChild = node1;
} else if(parent2.rightChild == node2) {
parent2.rightChild = node1;
}
node1.parentId = parent2.id;
node1.parent = parent2;
recal(node1.id);
recal(node2.id);
}
연결리스트 swap과 유사한 구조의 코드이다. 이 또한, 이 문제에 도전할 정도의 수리논술력이면 이해하기 어려울 거라고 생각하지 않는다.
이 경우도 recal 함수를 호출하여 변경된 상황을 다시 반영한다.
전체 코드
import java.io.*;
import java.util.*;
public class Main {
static class Node {
int id;
int authority;
boolean alram = true;
int parentId;
Node parent;
// 현재 노드가 전달할 수 있는 알람 수
// arrOfArlam[3] = 4 라면 위로 3개의 노드에 전달할 수 있는 알람이 4
int[] arrOfAlram = new int[21];
Node rightChild;
int rightMaxAuthority;
Node leftChild;
int leftMaxAuthority;
Node(int id, int authority, int parentId) {
this.id = id;
this.authority = authority;
this.parentId = parentId;
}
}
static Map<Integer, Node> nodeMap = new HashMap<>();
static int N, Q;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;
StringBuilder sb = new StringBuilder();
st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken());
Q = Integer.parseInt(st.nextToken());
int c1 = 0;
int power = 0;
int c2 = 0;
while(Q-- > 0) {
st = new StringTokenizer(br.readLine());
int command = Integer.parseInt(st.nextToken());
switch (command) {
case 100:
init(st);
break;
case 200 :
c1 = Integer.parseInt(st.nextToken());
turn_alram(c1);
break;
case 300 :
c1 = Integer.parseInt(st.nextToken());
power = Integer.parseInt(st.nextToken());
change_authority(c1, power);
break;
case 400 :
c1 = Integer.parseInt(st.nextToken());
c2 = Integer.parseInt(st.nextToken());
change_parent(c1, c2);
break;
case 500:
c1 = Integer.parseInt(st.nextToken());
int count = getArlam(c1);
sb.append(count).append("\n");
}
}
System.out.println(sb);
}
static private void init(StringTokenizer st) {
// root 생성
nodeMap.put(0, new Node(0, 0, -1));
Node root = nodeMap.get(0);
// 다른 노드 생성만
for(int i= 1; i <= N; i++) {
nodeMap.put(i, new Node(i, 0, -1));
}
// 노드 연결
for(int i = 1; i <= N; i++) {
int parentId = Integer.parseInt(st.nextToken());
Node parent = nodeMap.get(parentId);
Node child = nodeMap.get(i);
if(parent.leftChild == null) {
parent.leftChild = child;
child.parent = parent;
child.parentId = parentId;
} else {
parent.rightChild = child;
child.parent = parent;
child.parentId = parentId;
}
}
// 권한 설정
for(int i = 1; i <= N; i++) {
int authority = Integer.parseInt(st.nextToken());
Node node = nodeMap.get(i);
node.authority = authority;
}
// ArrOfAlram 초기화
alramInit(root);
}
static private void alramInit(Node now) {
if(now == null) {
return;
}
now.arrOfAlram = new int[21];
for(int i = 0; i <= now.authority && i <= 20; i++) {
now.arrOfAlram[i]++;
}
alramInit(now.leftChild);
alramInit(now.rightChild);
if(now.leftChild != null) {
// lefChilde의 n(1~20)번 알람 수가 현재 노드의 n - 1번 알람 수값에 반영
for(int i = 1; i <= 20; i++) {
now.arrOfAlram[i - 1] += now.leftChild.arrOfAlram[i];
}
}
if(now.rightChild != null) {
for(int i = 1; i <= 20; i++) {
now.arrOfAlram[i - 1] += now.rightChild.arrOfAlram[i];
}
}
}
static private void recal(int id) {
Node now = nodeMap.get(id);
while(now != null && now.id != 0) {
Arrays.fill(now.arrOfAlram, 0);
for(int i = 0; i <= now.authority && i <= 20; i++) {
now.arrOfAlram[i]++;
}
if(now.leftChild != null && now.leftChild.alram) {
// leftChild의 n(1~20)번 알람 수가 현재 노드의 n - 1번 알람 수값에 반영
for(int i = 1; i <= 20 ; i++) {
now.arrOfAlram[i - 1] += now.leftChild.arrOfAlram[i];
}
}
if(now.rightChild != null && now.rightChild.alram) {
for(int i = 1; i <= 20; i++) {
now.arrOfAlram[i - 1] += now.rightChild.arrOfAlram[i];
}
}
now = now.parent;
}
}
static private void turn_alram(int c) {
Node node = nodeMap.get(c);
node.alram = !node.alram;
// node부터 root까지 거슬러 올라가면서 arrOfAlram 재계산
recal(c);
}
static private void change_authority(int c, int power) {
Node node = nodeMap.get(c);
node.authority = power;
recal(c);
}
static private void change_parent(int c1, int c2) {
Node node1 = nodeMap.get(c1);
Node node2 = nodeMap.get(c2);
Node parent1 = nodeMap.get(node1.parentId);
Node parent2 = nodeMap.get(node2.parentId);
if(parent1.leftChild == node1) {
parent1.leftChild = node2;
} else if(parent1.rightChild == node1) {
parent1.rightChild = node2;
}
node2.parentId = parent1.id;
node2.parent = parent1;
if(parent2.leftChild == node2) {
parent2.leftChild = node1;
} else if(parent2.rightChild == node2) {
parent2.rightChild = node1;
}
node1.parentId = parent2.id;
node1.parent = parent2;
recal(node1.id);
recal(node2.id);
}
static private int getArlam(int c) {
Node node = nodeMap.get(c);
return node.arrOfAlram[0] - 1;
}
}
문제 풀이 소요 시간 : 5시간 이상
상당히 난이도가 있는 문제였다. 몇가지 더 언급하고 마무리해보자. 왜 내 원래 코드는 시간 초과가 났을까? 기존의 코드에서는 500번 명령어에서 트리의 순회로 직접 depth와 authority를 비교하는 계산을 수행하였다. 자, 인풋 사이즈를 보자.
트리의 순회로 모든 노드를 다 탐색한다고 가정하면 500번 명령어의 시간 복잡도는 O(N)이다. 만약 최대 인풋 사이즈에서 O(N) 함수를 Q의 최대인 10만 번 호출한다고 가정하면 전체 함수의 시간복잡도는 O(N x Q)로 100억번의 연산을 수행해야 한다. 시간 초과는 당연했다.
500번 명령어의 시간복잡도를 O(1)로 낮추는 것이 이 문제의 핵심이다.
따라서 이진트리의 최대 깊이가 20이라는 점을 이용하기로 생각했더니 풀이가 보였다. 각 노드마다 이진트리의 최대 깊이만큼의 배열을 가지고 여기에 정보를 저장한다면 변경이 생겨 매번 재계산 해줘도 O(D x Q)로 계산수를 획기적으로 줄일 수 있다.
'알고리즘' 카테고리의 다른 글
[알고리즘, BOJ] 17071 숨바꼭질 5 - java (2) | 2024.10.17 |
---|---|
[알고리즘, 코드트리] 왕실의 기사 대결 - java (4) | 2024.10.11 |
[알고리즘, 코드트리] 루돌프의 반란 - java (4) | 2024.10.10 |
[알고리즘, 코드트리] 코드트리 오마카세 - java (3) | 2024.10.09 |
[알고리즘, 코드트리] 고대 문명 유적 탐사 - java (1) | 2024.10.08 |
댓글