1. 문제 정보
- 문제 번호: 18870번
- 문제 이름: 좌표 압축
- 문제 요약: 수직선 위의 N개 좌표에 좌표 압축을 적용한다. X_i를 좌표 압축한 결과는 X_i > X_j를 만족하는 서로 다른 좌표 X_j의 개수와 같아야 한다.
2. 문제풀이 과정
값의 크기가 아니라 값의 위치가 중요하기 때문에, find 함수를 썼지만 find함수의 선형탐색 특징 때문에 시간 초과가 발생했다.
해결방법: 정렬된 상태에서 이진 탐색을 수행하는 lower_bound를 사용하면 입력값이 커도 적은 연산으로 수행할 수 있다.
**lower_bound는 왜 lower_bound라는 이름을 가지고 있을까??
lower_bound는 하한선이라는 뜻이다.
그래서!! "내가 찾는 값 X 가 나타나는 가장 첫 번째(가장 낮은 위치)"를 찾기 때문에 붙여진 이름이다.
예시:
{1, 2, 3, 3, 3, 4, 6}에서 3을 찾는다면:
- lower_bound는 첫 번째 3의 위치를 알려준다. (3이 시작되는 하한선)
- upper_bound는 4의 위치를 알려준다. (3이 끝난 바로 다음 상한선)
3. unique와 erase 문법의 정체
C++ STL의 unique는 이름과 달리 요소를 실제로 지우지 않는다. 이 부분이 가장 헷갈리는 포인트이다.
① std::unique(first, last)
이 함수는 연속된 중복 원소를 뒤로 밀어버리고, 중복되지 않은 원소들이 끝나는 지점의 다음 이터레이터를 반환한다.
- 예: {1, 2, 2, 3, 3, 4} → {1, 2, 3, 4, (쓰레기 값), (쓰레기 값)}
- 이때 반환값은 첫 번째 '쓰레기 값'의 위치이다.
② v.erase(unique_it, v.end())
unique가 반환한 이터레이터부터 벡터의 실제 끝(v.end())까지를 삭제하여 물리적으로 크기를 줄인다.
결론: v.erase(unique(v.begin(), v.end()), v.end()); 이 한 줄은 정렬된 벡터에서 중복을 완전히 제거하는 공식과도 같은 문법이다.
4. 코드 구현
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
// 입출력 최적화
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
vector<long long> v(N);
vector<long long> sorted_v;
for (int i = 0; i < N; i++) {
cin >> v[i];
sorted_v.push_back(v[i]);
}
// 1. 정렬
sort(sorted_v.begin(), sorted_v.end());
// 2. 중복 제거 (unique + erase 공식)
sorted_v.erase(unique(sorted_v.begin(), sorted_v.end()), sorted_v.end());
// 3. 각 원소의 인덱스 찾기 (lower_bound)
for (int i = 0; i < N; i++) {
// lower_bound는 이터레이터를 반환하므로 begin()을 빼서 인덱스로 변환한다.
int idx = lower_bound(sorted_v.begin(), sorted_v.end(), v[i]) - sorted_v.begin();
cout << idx << " ";
}
return 0;
}
5. 배운 점 & 회고 (TIL)
① lower_bound와 이터레이터 연산
std::lower_bound가 반환하는 값은 단순한 정수가 아니라 해당 위치를 가리키는 이터레이터(Iterator)이다. 이터레이터끼리의 뺄셈(it - v.begin())을 통해 해당 원소가 몇 번째 인덱스인지를 O(1)만에 계산할 수 있음을 배웠다.
② set과 distance의 함정
중복 제거를 위해 std::set을 쓸 수도 있지만, set의 이터레이터는 양방향 이터레이터(Bidirectional Iterator)이다. 따라서 두 이터레이터 사이의 거리를 구하는 std::distance를 쓰면 내부적으로 하나씩 세며 이동하므로 O(N)이 걸린다. 결국 정렬된 벡터 + lower_bound 조합이 가장 빠르고 정석적인 방법이다.
**양방향 이터레이터란?
이터레이터(Iterator)는 포인터처럼 컨테이너의 원소를 가리키는 도구인데, 컨테이너의 구조에 따라 "움직일 수 있는 능력"이 다르다.
1 - 양방향 이터레이터 (예: std::list, std::map, std::set)
말 그대로 앞(++)과 뒤(--)로만 한 칸씩 움직일 수 있는 이터레이터이다.
- 장점: 뒤로 돌아갈 수 있다.
- 단점: "여기서 10번째 칸으로 바로 점프해!"가 안 된다. 10번을 ++ 해서 가야 한다.
2 - 임의 접근 이터레이터 (Random Access Iterator) (예: std::vector, std::deque)
포인터와 똑같은 능력을 가졌다. ++, --는 물론이고 it + 5나 it[3]처럼 한 번에 멀리 점프할 수 있다.
③ 효율적인 중복 제거 프로세스
unique 함수가 요소를 물리적으로 지우는 것이 아니라 뒤로 보내기만 한다는 점이 흥미롭다. 반환된 이터레이터를 erase의 시작점으로 활용하는 유기적인 문법 구조를 확실히 익히게 되었다.
④ 시간 복잡도의 중요성
N이 1,000,000일 때 O(N^2)은 불가능하며, 반드시 O(N log N)이하의 알고리즘을 설계해야 한다. 이번 문제는 정렬(O(N log N))과 N번의 이진 탐색(O(N log N))으로 문제를 해결한 전형적인 사례이다.
'알고리즘 문제풀이 > C++' 카테고리의 다른 글
| C++ - References and Pointers (0) | 2026.07.31 |
|---|---|
| C++ 개념 학습 - 전처리, 리터럴 상수, 심볼릭 상수 (0) | 2026.07.28 |
| [C++ STL] map 컨테이너 완벽 정리: 구조부터 활용까지 (0) | 2026.03.25 |
| [백준/C++] 11724번: 연결 요소의 개수 - DFS와 인접 리스트의 정석 (1) | 2026.03.23 |
| [백준/C++] 2805번: 나무 자르기 - 이분 탐색과 매개변수 탐색 (0) | 2026.03.19 |