Sweep Line
Sweep Line
Sweep Line은 이벤트를 한 축 위에 정렬해 두고, 한 방향으로 훑으면서 상태를 갱신하는 기법이다.
한 줄로 요약하면 다음과 같다.
시작점과 끝점을 이벤트로 바꿔 정렬한 뒤 차례대로 처리한다
1. 언제 쓰는가
문제에서 아래 표현이 보이면 sweep line을 떠올릴 수 있다.
- 구간 시작 / 종료
- 겹치는 구간 수
- 동시에 열려 있는 회의 수
- 선분 교차 여부의 단순화 버전
- 좌표축 위 이벤트 처리
- 시간 순으로 상태 변화를 추적
대표 문제:
- 최대 동시 접속자 수
- 필요한 최소 회의실 수
- 겹치는 구간 개수
- 직사각형/구간 커버 여부
2. 핵심 아이디어
구간 [l, r]이 있으면 이것을:
- 시작 이벤트
+1 - 종료 이벤트
-1
로 바꾼다.
그 다음 모든 이벤트를 좌표순으로 정렬해서 보면서 현재 활성 구간 수를 관리한다.
flowchart LR
A["구간 입력"] --> B["시작/끝 이벤트로 변환"]
B --> C["이벤트 정렬"]
C --> D["왼쪽에서 오른쪽으로 스캔"]
D --> E["현재 활성 개수 / 최대값 갱신"]
3. 가장 기본 예시: 최대 겹침 수
문제:
구간들이 주어질 때 동시에 겹치는 구간의 최대 개수는?
예:
[1,4], [2,5], [4,6]
이벤트로 바꾸면:
(1, 시작)
(4, 끝)
(2, 시작)
(5, 끝)
(4, 시작)
(6, 끝)
정렬 후 순서대로 보면서 현재 개수를 갱신하면 된다.
손으로 따라가기
반열린 구간 [l, r)라고 하자.
그러면 같은 좌표에서는 끝 이벤트를 먼저 처리한다.
정렬된 이벤트는:
(1, +1)
(2, +1)
(4, -1)
(4, +1)
(5, -1)
(6, -1)
이제 왼쪽에서 오른쪽으로 훑어 보자.
x=1: active = 1
x=2: active = 2
x=4: active = 1 // [1,4) 종료
x=4: active = 2 // [4,6) 시작
x=5: active = 1
x=6: active = 0
따라서 최대 겹침 수는 2다.
timeline
title Sweep Line Example
1 : [1,4) 시작
2 : [2,5) 시작
4 : [1,4) 종료 / [4,6) 시작
5 : [2,5) 종료
6 : [4,6) 종료
이 예시에서 x = 4가 가장 중요하다.
끝을 먼저 처리하느냐, 시작을 먼저 처리하느냐에 따라
4에서의 active 값이 달라질 수 있기 때문이다.
4. 끝점 포함 규칙이 가장 중요하다
Sweep line에서 가장 자주 틀리는 부분은 같은 좌표에서 시작과 끝이 동시에 있을 때의 처리 순서다.
예를 들어 구간을:
- 닫힌 구간
[l, r] - 반열린 구간
[l, r)
중 무엇으로 해석하는지에 따라 정렬 기준이 달라진다.
보통 코테에서는 다음처럼 많이 처리한다.
1) [l, r)로 볼 때
같은 좌표라면 끝 이벤트를 먼저 처리한다.
이 경우:
[1,4) 와 [4,6)
는 겹치지 않는다.
2) [l, r]로 볼 때
같은 좌표라면 시작 이벤트를 먼저 처리해야 끝점도 겹침으로 셀 수 있다.
즉 문제의 “겹친다” 정의를 먼저 확인해야 한다.
같은 예시를 닫힌 구간으로 보면
만약 [1,4], [4,6]을 닫힌 구간으로 보면
좌표 4에서 실제로 겹친다.
그래서 이 경우에는:
같은 x에서는 시작 이벤트를 먼저 처리
해야 x = 4에서 active가 2가 된다.
즉 sweep line은 자료구조보다도 문제의 포함 규칙을 코드 정렬 기준으로 옮기는 작업이 핵심이다.
5. 최대 동시 구간 수 구현
아래 코드는 반열린 구간 [start, end) 기준이다.
따라서 같은 좌표에서는 끝 이벤트를 먼저 처리한다.
import java.util.*;
class SweepLine {
static class Event {
int x;
int type; // -1: end, +1: start
Event(int x, int type) {
this.x = x;
this.type = type;
}
}
int maxOverlap(int[][] intervals) {
List<Event> events = new ArrayList<>();
for (int[] interval : intervals) {
int start = interval[0];
int end = interval[1];
events.add(new Event(start, +1));
events.add(new Event(end, -1));
}
events.sort((a, b) -> {
if (a.x != b.x) return Integer.compare(a.x, b.x);
return Integer.compare(a.type, b.type); // end(-1) 먼저
});
int active = 0;
int answer = 0;
for (Event e : events) {
active += e.type;
answer = Math.max(answer, active);
}
return answer;
}
}
왜 이 코드가 맞는가
이 코드는 어떤 좌표 x를 지날 때마다:
- 시작 이벤트면 활성 구간 수를 1 증가
- 종료 이벤트면 활성 구간 수를 1 감소
시키고 있다.
즉 active는 “현재 sweep line이 지나가고 있는 지점에서 살아 있는 구간 수”를 의미한다.
flowchart TD
A["이벤트 정렬"] --> B["이벤트 하나 꺼냄"]
B --> C{"start or end?"}
C -->|start| D["active += 1"]
C -->|end| E["active -= 1"]
D --> F["answer = max(answer, active)"]
E --> F
F --> G{"다음 이벤트 있음?"}
G -->|Yes| B
G -->|No| H["종료"]
이 관점으로 보면 sweep line은 “이벤트 시점에서 상태가 어떻게 변하는가”를 구현한 것뿐이다.
6. 회의실 배정과의 관계
“최소 몇 개의 회의실이 필요한가?”도 사실 같은 문제다.
아이디어:
- 회의 시작 -> 방 하나 필요
- 회의 종료 -> 방 하나 반환
즉 최대 동시 활성 회의 수가 곧 정답이다.
이 문제는 다음 두 방법 모두 가능하다.
- sweep line 이벤트 정렬
- 시작 시간 정렬 + 종료 시간 최소 힙
둘 다 정답은 같고, 구현 취향 차이다.
회의실 예시
회의가 다음과 같다고 하자.
[10,20), [15,25), [18,30), [30,40)
활성 회의 수를 세면:
10직후: 1개15직후: 2개18직후: 3개20직후: 2개25직후: 1개30직후: 1개 ([18,30)종료 후[30,40)시작)
최대 동시 개수는 3이고,
필요한 최소 회의실 수도 3이다.
7. 차분 배열과의 관계
좌표 범위가 작고 정수 축이면 sweep line을 차분 배열로 바꿀 수도 있다.
예:
diff[start] += 1
diff[end] -= 1
그리고 누적합을 취하면 각 좌표의 활성 개수를 알 수 있다.
즉:
- 좌표 범위가 작다 -> 차분 배열
- 좌표가 크거나 실수다 -> 이벤트 정렬 sweep line
처럼 생각하면 된다.
즉 차분 배열은 sweep line을 “모든 좌표를 직접 순회하는 버전”이라고 볼 수 있다.
좌표가 크면 정렬 또는 좌표 압축으로 간다
예를 들어 좌표가 1부터 10^9까지 가능하지만
실제로 등장하는 시작/끝점은 2 * N개뿐일 수 있다.
이때는 배열을 직접 만들면 비효율적이다.
- 좌표축을 그대로 다 순회해야 한다 -> 차분 배열 부적합
- 이벤트 좌표만 처리하면 된다 -> 정렬 기반 sweep line 적합
- 정수 좌표이고 구간 쿼리까지 섞인다 -> 좌표 압축 후 세그먼트 트리/Fenwick Tree 검토
즉 sweep line은 단순히 “정렬해서 본다”가 아니라 실제로 변화가 일어나는 좌표만 본다는 관점으로 이해하면 좋다.
8. 2차원에서도 쓰이는가
쓴다.
대표적으로:
- 직사각형 넓이
- 교차 개수
- 최근접 이벤트 갱신
같은 문제에서 x축으로 sweep 하면서, 현재 y축 상태를 세그먼트 트리나 Fenwick Tree로 관리하기도 한다.
다만 코테에서는 보통 1차원 이벤트 정렬 형태가 더 자주 나온다.
그래서 학습 순서는 보통:
- 1차원 구간 이벤트
- 차분 배열과 연결
- 필요하면 2차원 sweep
으로 잡는 편이 좋다.
단순 개수만 세는지, 활성 집합 자체가 필요한지도 구분한다
지금 문서의 예시처럼 “현재 몇 개가 겹치는가”만 묻는다면
active 정수 하나면 충분하다.
하지만 문제에 따라서는 현재 활성인 구간들의 더 많은 정보가 필요하다.
- 가장 먼저 끝나는 구간이 필요하다 -> 최소 힙
- 현재 활성 구간의 최솟값/최댓값이 필요하다 -> TreeSet, 힙, 세그먼트 트리
- 활성 구간들의 총 길이나 가중치가 필요하다 -> 더 복잡한 자료구조
즉 sweep line의 본질은 “정렬된 이벤트를 순서대로 처리”하는 것이고, 그 안에서 유지하는 상태는 문제마다 달라진다.
9. 자주 하는 실수
- 시작/끝이 같은 좌표일 때 tie-break를 잘못 둠
- 닫힌 구간과 반열린 구간 해석을 섞음
- 이벤트 정렬 후 갱신 순서를 잘못 둠
- 좌표가 큰데 배열로 직접 처리하려고 함
active는 맞는데 최대값 갱신 시점을 틀림
10. 시험장용 최소 암기 버전
Sweep Line:
구간을 시작/끝 이벤트로 바꿔 정렬 후 스캔
핵심:
현재 활성 개수 관리
최대/최소/변화 시점 계산
주의:
같은 좌표에서 시작과 끝 처리 순서
구간 포함 규칙 [l,r] / [l,r)
11. 최종 요약
Sweep line은 다음 문장으로 정리할 수 있다.
시간이나 좌표축 위의 변화를 이벤트로 바꿔
정렬 한 번과 선형 스캔으로 상태를 관리하는 기법
Comments
Sign in with GitHub to join the discussion.