겹쳐 붙인 홍보 보드의 전체 넓이
자바스크립트 코딩테스트 문제로 geometry-sweep 주제를 연습해보세요. 난이도는 hard이며, 브라우저에서 바로 JavaScript로 풀이를 실행할 수 있습니다.
직사각형 홍보 보드 여러 장이 겹쳐 붙어 있을 때, 중복 없이 실제로 덮인 전체 넓이를 구하는 문제입니다.
문제 설명
축에 평행한 직사각형 목록 rectangles가 주어집니다.
각 직사각형은 [x1, y1, x2, y2] 형태이며,
- 왼쪽 아래 꼭짓점이
(x1, y1) - 오른쪽 위 꼭짓점이
(x2, y2)를 뜻합니다.
여러 직사각형이 서로 겹칠 수 있고, 일부는 완전히 다른 직사각형 안에 들어 있을 수도 있습니다.
이때 적어도 하나의 직사각형에 덮이는 영역의 총 넓이를 반환하는 unionAreaOfPromoBoards 함수를 작성하세요.
단, 변이나 꼭짓점만 맞닿는 것은 넓이 겹침으로 보지 않습니다.
제한사항
1 <= rectangles.length <= 50000rectangles[i] = [x1, y1, x2, y2]-1000000 <= x1 < x2 <= 1000000-1000000 <= y1 < y2 <= 1000000- 반환값은 JavaScript의
Number로 안전하게 표현되는 범위 안에 들어옵니다. - 가능한 한
O(n log n)수준으로 해결해 보세요.
예시
- 입력:
rectangles = [[-1, 0, 2, 3], [1, 1, 4, 4]]→ 출력:16 - 입력:
rectangles = [[0, 0, 2, 2], [2, 0, 5, 2], [1, 2, 4, 4]]→ 출력:16 - 입력:
rectangles = [[1, 1, 5, 5], [2, 2, 3, 3], [0, 0, 6, 2]]→ 출력:24
첫 번째 예시에서
- 첫 보드 넓이:
9 - 둘째 보드 넓이:
9 - 겹치는 부분 넓이:
1
겹치는 부분은 [1, 2) × [1, 3)이라 넓이가 2이고,
따라서 전체 넓이는 9 + 9 - 2 = 16입니다.
두 번째 예시에서는 첫째와 둘째 보드는 x = 2 선에서만 맞닿으므로 겹친 넓이는 0입니다.
힌트
- 모든 보드 쌍의 겹침을 직접 계산하면 너무 느립니다.
x축 방향으로 왼쪽 변과 오른쪽 변을 이벤트처럼 훑어 보세요.- 어떤
x구간에서 현재 덮인y길이만 알 수 있다면, 그 구간의 넓이는가로 길이 × 덮인 세로 길이로 바로 계산할 수 있습니다. - 문제는 겹친
y구간을 중복 없이 합치는 것이므로, 좌표 압축과 세그먼트 트리를 함께 쓰는 전형적인 sweep line 패턴을 떠올릴 수 있습니다.
해설
직사각형 합집합 넓이를 단순히 처리하면 겹침이 복잡하게 얽혀 O(n^2) 이상으로 커지기 쉽습니다.
이 문제의 핵심은 세로 띠(strip) 단위로 넓이를 쌓는다는 것입니다.
1. x축 이벤트로 바꾸기
직사각형 [x1, y1, x2, y2]는 x축 기준으로 보면 두 순간만 중요합니다.
x1에서 이 직사각형이 활성화됨x2에서 이 직사각형이 비활성화됨
즉 각 직사각형을 다음 두 이벤트로 바꿀 수 있습니다.
(x1, +1, y1, y2)(x2, -1, y1, y2)
이 이벤트들을 x 오름차순으로 정렬해 왼쪽에서 오른쪽으로 훑습니다.
2. 현재 x띠에서 필요한 정보
어떤 두 인접한 이벤트 x = prevX와 x = currX 사이에서는 활성화된 직사각형 집합이 변하지 않습니다.
따라서 이 구간의 넓이는
(currX - prevX) * 현재 덮인 전체 y 길이
입니다.
결국 매 순간 필요한 것은 현재 활성 직사각형들이 y축에서 덮고 있는 총 길이입니다.
3. 왜 단순 누적이 안 될까?
예를 들어 현재 활성 직사각형들의 y구간이
[1, 5)[2, 4)[3, 7)
이라면 길이를 그냥 더하면 4 + 2 + 4 = 10이지만,
실제로 덮인 구간은 [1, 7) 하나이므로 길이는 6입니다.
즉 겹친 y구간은 중복 없이 합쳐진 길이를 알아야 합니다.
4. 좌표 압축 + 세그먼트 트리
직사각형의 y좌표는 최대 1000000 범위라서 그대로 배열을 만들 수 없습니다.
대신 등장하는 모든 y1, y2만 모아 정렬한 뒤 좌표 압축을 합니다.
그러면 세그먼트 트리의 각 노드는 압축된 y구간 하나를 나타낼 수 있습니다.
각 노드에는 보통 두 정보를 둡니다.
coverCount: 이 구간을 완전히 덮는 활성 직사각형 개수coveredLength: 현재 실제로 덮인 길이
갱신 규칙은 다음과 같습니다.
coverCount > 0이면 이 노드 구간 전체가 덮입니다.coverCount == 0이면 자식들의coveredLength합으로 결정합니다.- 리프 노드라면 실제 길이는
ys[right + 1] - ys[left]처럼 압축 전 좌표 차이로 계산합니다.
이 구조를 쓰면 어떤 직사각형이 시작하거나 끝날 때마다 [y1, y2) 구간에 +1 또는 -1을 반영할 수 있고, 루트 노드의 coveredLength가 곧 현재 전체 덮인 y 길이가 됩니다.
5. sweep 순서
- 모든 직사각형을 x 이벤트 2개로 변환합니다.
- 모든 y좌표를 모아 정렬 후 압축합니다.
- 이벤트를 x 오름차순으로 순회합니다.
- 새 이벤트를 처리하기 전에, 직전 x와 현재 x 사이 넓이를 누적합니다.
- 현재 이벤트의
[y1, y2)를 세그먼트 트리에 반영합니다. - 루트의 덮인 y 길이를 다음 띠 계산에 사용합니다.
6. 변만 닿는 경우가 왜 자동 처리될까?
우리는 직사각형을 [x1, x2) × [y1, y2)처럼 생각해도 넓이는 같고, 이 방식에서는
x2에서 끝나는 보드와- 같은
x2에서 시작하는 다른 보드 가 있어도 폭이 0인 선에서만 만난 것이므로 넓이가 중복 계산되지 않습니다.
y축도 마찬가지라서 변만 맞닿는 경우는 겹침 면적 0으로 자연스럽게 처리됩니다.
7. 복잡도
- 이벤트 수는
2n - 이벤트 정렬:
O(n log n) - 각 이벤트당 세그먼트 트리 갱신:
O(log n)
따라서 전체 시간 복잡도는 O(n log n)이고, 추가 공간 복잡도도 O(n) 수준입니다.
코드 작성
starter code를 바탕으로 함수를 완성한 뒤 예제 테스트를 실행해보세요.
커스텀 테스트
함수 인자를 JSON 배열 형태로 입력하세요. 예: [3, 5], [[1, 2, 3]]
실행 결과
아직 실행하지 않았습니다.
댓글
문제 풀이 아이디어, 질문, 반례를 자유롭게 나눠보세요.