에듀테크 프로젝트 대시보드
힐베르트 곡선 (Hilbert Curve)
하나의 선(1차원)이 평면(2차원)의 모든 칸을 빠짐없이, 그것도 한 번씩만 지나갈 수 있을까? 1891년 다비트 힐베르트가 답을 보였다. 이 공간 채움 곡선(space-filling curve)은 선 위에서 가까운 두 점이 평면에서도 거의 항상 가깝다는 지역성(locality) 성질 때문에 이미지 처리, 공간 데이터베이스 인덱스, 디더링 순서 등에 실제로 쓰인다.
커서 주변 3×3 이웃 9칸이 스캔 순서(1차원 인덱스)에서 얼마나 흩어지는지 표시된다. 인덱스 최대 간격이 작을수록 지역성이 좋다.
차수 n=1의 힐베르트 곡선은 2×2 격자를 잇는 ㄷ자(U자) 모양 하나다.
차수를 하나 올릴 때마다 격자는 가로·세로로 2배가 되고, 네 개의 사분면에
축소된 이전 차수 곡선을 하나씩 넣은 뒤 짧은 선분 3개로 이어 붙인다.
이때 아래쪽 두 사분면의 곡선은 서로 다른 방향으로 회전·반사시켜야
곡선의 끝과 다음 곡선의 시작이 딱 맞물린다. 이 "방향을 틀어 넣기"가 힐베르트 곡선의 핵심이다.
n → 격자 2ⁿ × 2ⁿ, 방문하는 칸 4ⁿ개
곡선 위의 위치를 거리 d(0부터 4ⁿ−1까지)라고 하면, 모든 d는 격자 좌표
(x, y) 하나에 대응한다. 힐베르트 곡선의 값어치는 이 대응이
양방향으로 지역성을 보존한다는 데 있다.
단순한 행 우선 스캔은 한 행 끝에서 다음 행 시작으로 격자 폭만큼 점프하고, Z-곡선(모튼 순서)은 사분면 경계에서 큰 점프가 생긴다. 힐베르트 곡선은 이런 큰 점프가 없어 세 방식 중 지역성이 가장 좋다.
재귀 그림 없이도, d를 2비트씩 잘라 보며 사분면을 따라 내려가면서
필요할 때 좌표축을 회전(swap/flip)시키면 O(n)에 변환된다.
위키백과에 실린 고전적인 구현이다.
# d → (x, y) : side = 2^n 격자에서 곡선상 거리 d의 좌표 def d2xy(side, d): x = y = 0 s = 1 while s < side: rx = 1 & (d // 2) ry = 1 & (d ^ rx) # 현재 s×s 블록을 회전/반사시켜 방향을 맞춘다 if ry == 0: if rx == 1: x = s - 1 - x y = s - 1 - y x, y = y, x x += s * rx y += s * ry d //= 4 s *= 2 return x, y # (x, y) → d : 역변환도 같은 회전 규칙을 거꾸로 적용 def xy2d(side, x, y): d = 0 s = side // 2 while s > 0: rx = 1 if (x & s) > 0 else 0 ry = 1 if (y & s) > 0 else 0 d += s * s * ((3 * rx) ^ ry) if ry == 0: if rx == 1: x = s - 1 - x y = s - 1 - y x, y = y, x s //= 2 return d
위 두 함수는 서로 역함수다: d2xy(side, xy2d(side, x, y)) == (x, y).
아래 데모의 곡선도 정확히 이 규칙으로 그려진다.