상근이는 영화 DVD를 수집하는 열성적인 수집가이다. 그는 자신의 DVD 콜렉션을 탑처럼 쌓아 보관한다. 영화를 보고 싶을 때마다 DVD의 위치를 찾아서, 쌓여 있는 콜렉션이 무너지지 않도록 조심스럽게 해당 DVD를 꺼낸다. 영화를 다 본 후에는 그 DVD를 가장 위에 놓는다.
하지만 상근이가 보유한 DVD의 수가 너무 많아, 원하는 DVD를 찾는 데 시간이 오래 걸린다. 각 DVD의 위치를 쉽게 찾기 위해, 찾으려는 DVD 위에 몇 개의 DVD가 있는지만 알면 된다. 각 DVD는 표지에 붙어 있는 번호로 구별할 수 있다.
이때, 각 DVD의 위치를 기록하는 프로그램을 작성하고자 한다. 상근이가 영화를 한 편 볼 때마다 그 DVD 위에 몇 개의 DVD가 있었는지를 출력해야 한다. 또한, 상근이는 매번 영화를 볼 때마다 본 DVD를 가장 위에 놓는다.
프로그램은 여러 테스트 케이스를 처리해야 하며, 각 테스트 케이스마다 상근이가 가지고 있는 영화의 수 $n$과 보려고 하는 영화의 수 $m$이 주어진다. 이어서 $m$개의 영화 번호가 순서대로 주어진다. 가장 처음에는 영화들이 1번부터 $n$번까지 번호 순서대로 쌓여 있으며, 가장 위에 있는 영화의 번호는 1번이다.
각각의 영화에 대해, 그 영화를 볼 때 해당 DVD 위에 몇 개의 DVD가 있었는지를 구해야 한다.
문제 : https://www.acmicpc.net/problem/3653
![]() |
|---|
학습 목표
이 글을 읽고 나면 다음을 설명할 수 있어야 한다.
- “위에 쌓인 DVD 수"를 위치 슬롯의 구간 합 문제로 바꾸는 이유
- 슬롯을
m + i에서 시작해top--로 내려오며 재활용하는 방식이 왜 정확한지(불변식) - Fenwick Tree와 세그먼트 트리 중 이 문제에 무엇이 더 적합한지, 그리고 전체 복잡도 $O((n+m)\log(n+m))$의 도출
접근 방식
문제의 제약은 테스트 케이스마다 $n, m \le 100{,}000$이다. DVD를 꺼낼 때마다 배열을 선형으로 훑으면 질의 하나에 $O(n)$, 전체로 $O(nm)$이라 최악 $10^{10}$ 번의 연산이 되어 시간 초과가 난다. 필요한 것은 “특정 DVD보다 위쪽에 있는 DVD가 몇 개인가"를 로그 시간에 답하는 구조다.
핵심 발상은 DVD를 옮기는 대신 위치 번호만 새로 배정하는 것이다. 탑의 위쪽을 낮은 번호로 보고, 각 슬롯에 DVD가 있으면 1, 없으면 0을 기록하면 “위에 쌓인 개수"는 1부터 현재 위치 - 1까지의 prefix sum이 된다. 맨 위로 올리는 동작은 기존 슬롯의 값을 0으로 되돌리고, 지금까지 쓰인 어떤 슬롯보다 더 위쪽(더 작은 번호)의 빈 슬롯에 1을 기록하는 것으로 표현된다. 이 점 갱신과 prefix sum 질의를 모두 $O(\log N)$에 처리하는 자료구조가 Fenwick Tree(이진 인덱스 트리)다.
슬롯 배정 규칙은 다음 불변식으로 정리된다. 초기에 DVD i는 슬롯 m + i를 차지하고, 슬롯 1부터 m까지는 비어 있다. k번째 요청(k는 1부터)에서 꺼낸 DVD는 슬롯 m - k + 1로 올라간다. 코드의 top은 처음에 m이고 요청마다 하나씩 줄어드는 “다음에 쓸 가장 위쪽 빈 슬롯"이다. 요청이 m번뿐이므로 top은 1 아래로 내려가지 않고, 이미 사용한 위쪽 슬롯은 항상 그 아래(번호가 큰) 슬롯보다 위에 있으므로 번호 순서가 곧 쌓인 순서와 일치한다.
작은 예로 $n=3, m=2$에서 요청이 3, 1일 때 슬롯 상태를 따라가 보자. 슬롯 번호는 위가 작은 쪽이다.
| 단계 | 슬롯 1 | 슬롯 2 | 슬롯 3 | 슬롯 4 | 슬롯 5 | 출력 |
|---|---|---|---|---|---|---|
| 초기 | 0 | 0 | DVD1 | DVD2 | DVD3 | - |
| 요청 3 | 0 | DVD3 | DVD1 | DVD2 | 0 | 2 |
| 요청 1 | DVD1 | DVD3 | 0 | DVD2 | 0 | 1 |
요청 3에서 DVD3은 슬롯 5에 있고 그 앞(슬롯 1–4)의 합이 2이므로 2를 출력한다. 이후 슬롯 5를 비우고 top = 2에 올린다. 요청 1에서 DVD1은 슬롯 3에 있고 앞의 합은 슬롯 2의 DVD3 하나라서 1이 된다.
대안으로 세그먼트 트리도 같은 점 갱신과 구간 합을 지원한다. 하지만 이 문제는 prefix sum만 필요하고 구간 갱신이 없어서, 구현이 짧고 상수가 작은 Fenwick Tree가 더 적합하다. 오프라인으로 모든 요청을 미리 읽어 처리하는 방법도 있지만 이 문제에서는 온라인 풀이만으로 충분하다.
| 항목 | 값 |
|---|---|
| 초기화 | $O(n \log(n+m))$ (또는 선형 빌드 가능) |
| 요청 하나 | 질의 1회 + 갱신 2회 = $O(\log(n+m))$ |
| 전체 | $O((n+m)\log(n+m))$ |
| 메모리 | $O(n+m)$ |
C++ 코드와 설명
| |
코드 설명
FenwickTree 클래스는 idx & -idx로 최하위 비트를 더하거나 빼며 트리를 오르내리는 표준 구현이다. 크기를 n + m으로 잡는 이유는 초기 슬롯이 m + 1부터 m + n까지 놓이기 때문이다. pos[i] = m + i로 초기 위치를 정하고 해당 슬롯에 1을 더해 DVD의 존재를 표시한다. 요청마다 query(idx - 1)이 그 DVD 위의 개수를 돌려주고, 그 값을 출력한 뒤 update(idx, -1)로 기존 슬롯을 비우고 pos[movie] = top--로 새 슬롯을 배정하여 1을 더한다. 처음 두 줄의 ios::sync_with_stdio(false)와 cin.tie(nullptr)는 테스트 케이스가 많을 때 입출력 시간을 줄이기 위한 것이다.
C++ without library 코드와 설명
| |
코드 설명
로직은 위 클래스 버전과 같고 STL 없이 전역 배열만 쓴다. tree는 200,005칸으로 $n + m \le 200{,}000$을 수용하고, pos는 100,005칸으로 $n \le 100{,}000$을 수용한다. 테스트 케이스마다 tree를 size까지만 0으로 지우므로 케이스가 많아도 초기화 비용이 입력 크기에 비례한다. 입출력은 scanf와 printf를 그대로 사용한다.
Python 코드와 설명
| |
코드 설명
Python 버전도 같은 알고리즘이며 sys.stdin.readline으로 입력을 빠르게 읽고, 결과를 리스트에 모아 ' '.join(res)로 한 번에 출력한다. 함수 호출 비용 때문에 C++보다 느리므로 입력이 최대 크기일 때는 시간 제한에 여유가 적다는 점을 감안해야 한다.
결론
핵심은 DVD를 실제로 옮기지 않고 위치 슬롯의 0/1 배열과 prefix sum으로 “위에 쌓인 개수"를 표현한 것이다. 슬롯을 m칸 비워 두고 top을 줄여 가며 재활용하면 번호 순서가 쌓인 순서와 일치한다는 불변식이 유지되고, Fenwick Tree가 갱신과 질의를 $O(\log(n+m))$에 처리해 전체 $O((n+m)\log(n+m))$로 문제를 해결한다. 같은 발상은 “원소를 옮기면서 앞쪽 개수를 세는” 다른 문제(예: 순서 재배열 후 inversion 개수 계산)에도 이어진다.
Fenwick Tree의 구조와 증명은 CP-Algorithms의 Fenwick Tree 문서에 정리되어 있다. 원 논문은 Peter M. Fenwick, “A New Data Structure for Cumulative Frequency Tables”, Software: Practice and Experience 24(3), 1994이다.
코너 케이스 및 실수 포인트
| 케이스 | 설명 | 처리 방법 |
|---|---|---|
| 같은 영화 연속 요청 | 맨 위에 있는 DVD를 다시 요청하면 답은 0 | 기존 슬롯 제거 후 새 슬롯 추가 순서를 지키면 자연히 0이 나온다 |
| m이 n보다 큰 경우 | 같은 DVD를 여러 번 요청 가능 | 트리 크기를 n이 아닌 n + m으로 잡고 top이 1 아래로 내려가지 않음을 확인 |
| 다중 테스트 케이스 | 이전 케이스의 트리·위치가 남음 | 케이스마다 tree를 다시 0으로 초기화 |
| 오버플로우 | 답은 최대 $n-1$이라 int로 충분 | long long은 필요 없다 |
| 출력 형식 | 케이스마다 한 줄, 값은 공백 구분 | 줄 끝 공백이 허용되는지 채점 환경에 맞춰 점검 |
![Featured image of post [Algorithm] C++/Python 백준 3653번 : 영화 수집](/post/algorithm/2024-09-25-boj-3653/wordcloud_hu_7f22372fc9aad88b.webp)

![[Algorithm] C++/Python 백준 16565번 : N포커](/post/algorithm/2024-10-10-boj-16565/wordcloud_hu_7823a4e0fd1c291c.webp)
![[Algorithm] C++/Python 백준 2357번 : 최솟값과 최댓값](/post/algorithm/2024-10-10-boj-2357/wordcloud_hu_2932bb9886e69a3a.webp)
![[Algorithm] C++/Python 백준 3653번 : 영화 수집](/post/algorithm/2024-09-25-boj-3653/wordcloud_hu_9c7e86fbd0974d56.webp)
![[Algorithm] C++/Python 백준 3679번 : 단순 다각형](/post/algorithm/2024-09-25-boj-3679/wordcloud_hu_6b3b2fbdb74918fe.webp)
![[Algorithm] C++/Python 백준 5670번: 휴대폰 자판](/post/algorithm/2024-09-25-boj-5670/wordcloud_hu_520ec3c3904044ff.webp)
![[Algorithm] C++/Python 백준 11505번 : 구간 곱 구하기](/post/algorithm/2024-10-10-boj-11505/wordcloud_hu_83182463fd8f24b4.webp)
![[Algorithm] C++ 백준 17353번: 하늘에서 떨어지는 1, 2, ..., R-L+1개의 별](/post/algorithm/2025-08-14-boj-17353-falling-stars-r-l-plus-1-cpp-solution/wordcloud_hu_3d7c3c42240f3074.webp)
![[Algorithm] C++/Python 백준 16975번 : 수열과 쿼리 21](/post/algorithm/2025-02-07-boj-16975/index_hu_e6256d91c1b80bf0.webp)
![[Algorithm] C++/Python 백준 16978번 : 수열과 쿼리 22](/post/algorithm/2025-02-08-boj-16978/index_hu_cd9c0b20945c8464.webp)
![[Algorithm] C++/Python 백준 8217번 : 유성](/post/algorithm/2025-02-03-boj-8217/index_hu_a1dcbfc06599702d.webp)