문제 정보
백준 20131: 트리 만들기
- 링크: https://www.acmicpc.net/problem/20131
- 시간 제한: 2초 | 메모리 제한: 1024 MB
- 난이도: 중상 | 분류: 그래프, 트리, 구현
요약:
정점이 \(N\)개인 트리에서 차수 1인 정점(리프)을 번호가 가장 큰 것부터 골라 제거하면서, 그 리프와 인접한 정점 번호를 기록해 얻은 길이 \(N-2\)의 수열이 주어진다. 이 수열이 실제로 어떤 트리에서 나올 수 있는지 판별하고, 가능하다면 유일한 트리를 복원하여 간선을 사전 순으로 출력해야 하며, 트리가 존재하지 않거나 둘 이상 존재하면 -1을 출력한다.
문제 설명
트리에 대해 다음 과정을 수행해서 수열 \(a_1, a_2, \dots, a_{N-2}\)를 만든다.
- 현재 트리에서 차수가 1인 정점들 중 번호가 가장 큰 정점을 하나 고른다. 이를 \(x\)라고 하자.
- 정점 \(x\)와 인접한 정점의 번호를 수열에 넣는다.
- 정점 \(x\)와 인접한 간선을 트리에서 삭제한다.
- 위 과정을 총 \((N-2)\)번 반복한다.
이때 최종적으로 남는 두 정점은 서로 간선으로 연결된 상태가 된다.
입력으로 주어진 수열이 위 과정을 통해 얻어질 수 있는지 확인하고, 가능하다면 원래 트리의 간선 \((N-1)\)개를 조건에 맞게 출력하는 것이 목표다.
입출력 예제
예제 1
입력:
| |
출력:
| |
예제 2
입력:
| |
출력:
| |
접근 방식
핵심 관찰
- 최종 수열은 항상 리프에서 큰 번호부터 제거하는 과정에서 나왔으므로,
이를 역으로 따라가면 원래 트리를 재구성할 수 있다. - 수열의 각 원소 \(a_i\)는, 해당 단계에서 제거된 리프 \(x\)와 연결된 정점의 번호이다.
- 즉, 그 시점의 리프 \(x\)와 \(a_i\) 사이에 간선 \((x, a_i)\) 가 존재해야 한다.
- 리프는 항상 차수 1이므로, 각 정점의 차수는 \[ \text{deg}[v] = \text{(수열에서 등장한 횟수)} + 1 \] 로 결정된다.
- 처음부터 차수 1인 정점들을 모두 모아두고, 최대 힙(우선순위 큐) 로 관리하면
“현재 리프들 중 가장 번호가 큰 정점”을 빠르게 선택할 수 있다.
알고리즘 설계
수열 등장 횟수 세기
- 각 값 \(a_i\)의 등장 횟수를 세어
cnt[v]에 저장한다. deg[v] = cnt[v] + 1로 초기 차수를 설정한다.
- 각 값 \(a_i\)의 등장 횟수를 세어
초기 리프 수집
- 모든 정점 \(v\)에 대해
deg[v] == 1인 정점을 우선순위 큐(최대 힙)에 넣는다.
- 모든 정점 \(v\)에 대해
N-2단계 역 시뮬레이션
- 수열을 앞에서부터 순서대로 보면서, 매 단계마다:
- 우선순위 큐에서 현재 차수 1인 정점들 중 가장 큰 번호 \(x\)를 꺼낸다.
- 단, 과거에 차수가 변해 이미 리프가 아닌 정점이 힙에 남아있을 수 있으므로,
deg[x] == 1인 정점을 만날 때까지 반복해서 꺼낸다. - 만약 큐가 비면, 수열을 만들 수 없는 경우이므로 -1이다.
- 단, 과거에 차수가 변해 이미 리프가 아닌 정점이 힙에 남아있을 수 있으므로,
- 해당 단계의 값 \(a_i\)와 \(x\)를 잇는 간선 \((x, a_i)\)를 저장한다.
deg[x]--(0이 되어 더 이상 사용되지 않음),deg[a_i]--.- 만약
deg[a_i]가 1이 되면, 새로 리프가 되었으므로 우선순위 큐에 넣는다. deg[a_i] < 0이 되는 경우는 불가능한 상태이므로 -1 처리.
- 우선순위 큐에서 현재 차수 1인 정점들 중 가장 큰 번호 \(x\)를 꺼낸다.
- 수열을 앞에서부터 순서대로 보면서, 매 단계마다:
마지막 두 정점 연결
- 모든 단계를 마친 뒤, 남은 정점들 중에서
deg[v] == 1인 정점이 정확히 두 개 있어야 한다. - 이 둘을 \(u, v\)라 할 때, 간선 \((u, v)\)를 추가하면 전체 트리가 완성된다.
- 모든 단계를 마친 뒤, 남은 정점들 중에서
정답 정렬 및 출력
- 모든 간선 \((a, b)\)에 대해 항상
a < b가 되도록 정규화한 뒤, - 간선 리스트를 사전 순(
a오름차순,a같으면b오름차순)으로 정렬해 출력한다. - 중간에 실패 판정을 내렸거나, 마지막에 남은 리프 수가 2가 아니면 -1을 출력한다.
- 모든 간선 \((a, b)\)에 대해 항상
Mermaid 흐름도
flowchart TD
A["입력: N, 수열 a[1..N-2]"] --> B["각 정점 등장 횟수 cnt[v] 계산"]
B --> C["deg[v] = cnt[v] + 1 로 차수 초기화"]
C --> D["deg[v] == 1 인 정점을 모두 최대 힙에 삽입"]
D --> E["i = 1 .. N-2 단계 반복"]
E --> F{"힙에서 deg[x] == 1 인 최대 정점 x 찾기"}
F -- 없음 --> G["불가능: -1 출력 후 종료"]
F -- 존재 --> H["간선 (x, a[i]) 추가 (작은 번호가 앞)"]
H --> I["deg[x]--, deg[a[i]]--"]
I --> J{"deg[a[i]] < 0 ?"}
J -- Yes --> G
J -- No --> K{"deg[a[i]] == 1 ?"}
K -- Yes --> L["a[i]를 힙에 삽입"]
K -- No --> M["다음 i로 진행"]
L --> M
M --> N{"i < N-2 ?"}
N -- Yes --> E
N -- No --> O["deg[v] == 1 인 정점 두 개 u, v 찾기"]
O --> P{"리프가 정확히 두 개인가?"}
P -- No --> G
P -- Yes --> Q["간선 (u, v) 추가 후 전체 간선 정렬"]
Q --> R["사전 순으로 간선 출력"]
복잡도 분석
| 항목 | 복잡도 | 설명 |
|---|---|---|
| 시간 복잡도 | \(O(N \log N)\) | 각 단계마다 최대 한 번의 힙 연산 및 정렬 \(O(N \log N)\) |
| 공간 복잡도 | \(O(N)\) | 차수 배열, 등장 횟수 배열, 간선 리스트, 힙 저장 |
구현
C++ 코드
| |
코너 케이스 및 실수 포인트
| 케이스 | 설명 | 처리 방법 |
|---|---|---|
| \(N = 3\) | 수열 길이가 1인 최소 크기 트리 | 한 번의 단계 후 남은 두 정점을 정확히 연결해야 함 |
| 수열 값 범위 초과 | \(a_i \notin [1, N]\) | 즉시 -1 출력 |
| 리프 부족 | 어떤 단계에서 힙이 비는데 아직 수열 처리가 남은 경우 | 불가능한 수열로 간주하고 -1 출력 |
| 차수 음수 | deg[y]가 0 아래로 떨어지는 경우 | 잘못된 수열이므로 -1 처리 |
| 리프 개수 불일치 | 최종적으로 deg == 1인 정점이 2개가 아닌 경우 | 유효한 트리가 아니므로 -1 출력 |
| 정렬 조건 누락 | \(a < b\) 또는 사전 순 정렬을 빼먹은 경우 | 출력 형식 오류로 오답, 간선 정규화 + 정렬 필수 |
제출 전 점검
-
deg[v] = cnt[v] + 1로 초기 차수를 올바르게 계산했는가? - 힙에서 stale 리프(더 이상 차수 1이 아닌 정점) 를 건너뛰도록 구현했는가?
- 모든 간선에 대해 항상
a < b가 되도록 정규화했는가? - 간선 리스트를 사전 순으로 정렬 후 출력했는가?
- 잘못된 입력(수열 범위, 차수 음수, 리프 개수 불일치)에 대해 -1을 출력하는가?
![Featured image of post [Algorithm] C++ 백준 20131번: 트리 만들기](/post/algorithm/2025-12-11-boj-20131-tree-making-cpp-solution/wordcloud_hu_e489dc28555e8898.png)
![[Algorithm] C++ 백준 11409번: 열혈강호 6](/post/algorithm/2025-12-11-boj-11409-yeolhyeol-gangho-6-min-cost-max-flow-cpp-solution/wordcloud_hu_53b0c1906c1fe868.png)
![[Algorithm] C++ 백준 20131번: 트리 만들기](/post/algorithm/2025-12-11-boj-20131-tree-making-cpp-solution/wordcloud_hu_461e53ca4ea7f406.png)
![[Algorithm] C++ 백준 5920번: Cow Photography](/post/algorithm/2025-12-11-boj-5920-cow-photography-cpp-solution/wordcloud_hu_ab327ea8ee254ac3.png)
![[Algorithm] C++ 백준 11868번: 님 게임 2](/post/algorithm/2025-12-08-boj-11868-nim-game-2-game-theory-cpp-solution/wordcloud_hu_cb52f0251e1b8fbb.png)
![[Algorithm] C++ 백준 16741번: 긴급 탈출](/post/algorithm/2025-12-08-boj-16741-emergency-evacuation-cpp-solution/wordcloud_hu_dacf69a3964cf279.png)
![[Algorithm] C++ 백준 24272번: 루트 노드가 많은 트리일수록 좋은 트리이다](/post/algorithm/2025-08-14-boj-24272-more-root-nodes-better-tree-cpp-solution/wordcloud_hu_41642d1d3a56b905.png)
![[Algorithm] C++ 백준 1763번: 트리 색칠 - 비율 그리디](/post/algorithm/2025-08-14-boj-1763-tree-coloring-cpp-solution/wordcloud_hu_7953fe6dae3d039f.png)
![[Algorithm] C++ 백준 19693번: Safety](/post/algorithm/2025-12-08-boj-19693-safety-stacks-smooth-cpp-solution/wordcloud_hu_e00d069337dc3e1b.png)
![[Algorithm] C++ 백준 15782번: Calculate! 2](/post/algorithm/2025-12-02-boj-15782-calculate-2-tree-segtree-lazy-cpp-solution/wordcloud_hu_c7b151aaf16cbe10.png)