Mo's 알고리즘이란
배열에 대한 여러 구간 질의 \([l, r]\)를, 현재 구간을 양 끝에서 한 칸씩 넓히고
좁히며 답을 갱신하는 오프라인 기법이다. "구간 안 서로 다른 수의 개수", "구간
원소 빈도의 제곱 합" 등 점진적으로 갱신 가능한 질의에 강력하다.
전제는 다음 네 연산을 각각 \(O(1)\)(또는 충분히 싸게)에 할 수 있다는 것이다.
add(x): 현재 구간에 원소 \(x\)를 추가하며 답 갱신.remove(x): 현재 구간에서 원소 \(x\)를 제거하며 답 갱신.
오른쪽 끝을 \(+1\)/\(-1\), 왼쪽 끝을 \(+1\)/\(-1\) 움직이는 네 가지로 임의의 구간으로
이동할 수 있다.
왜 블록 정렬인가
질의를 임의 순서로 처리하면 포인터가 마구 움직여 총 이동량이 \(O(NQ)\)가 될 수
있다. Mo는 인덱스를 크기 \(B\)의 블록으로 나누고, 질의를 다음 기준으로
정렬한다.
\(l\)이 속한 블록 번호를 1순위, \(r\) 을 2순위로 정렬.
(블록이 짝수면 \(r\) 오름차순, 홀수면 내림차순으로 두면 더 빠르다 — 지그재그.)
이렇게 하면 포인터 이동량을 분석적으로 묶을 수 있다.
복잡도 분석
- 오른쪽 포인터 \(r\): 같은 블록 안의 질의들끼리는 \(r\)이 단조이므로 한 블록
안 이동이 \(O(N)\). 블록이 \(N/B\)개라 총 \(O(N^2 / B)\). - 왼쪽 포인터 \(l\): 같은 블록 안에서 \(l\)은 블록 크기 \(B\) 안에서만 움직이고
질의가 \(Q\)개이므로 총 \(O(QB)\).
합 \(O(N^2/B + QB)\)를 \(B\)로 최소화하면 \(B = N / \sqrt{Q}\)에서 최솟값
\(O((N + Q)\sqrt{N})\), 흔히 \(O((N+Q)\sqrt N)\)로 적는다(\(\approx N \approx Q\)
가정). 블록 크기 선택이 성능에 직결되므로 보통 \(B = N / \sqrt{Q}\)나 단순히
\(\sqrt N\)을 쓴다.
한계와 변형
- 점 갱신이 섞이면 시간 차원을 추가한 Mo with updates(3차원 정렬,
\(O(N^{5/3})\))를 쓴다. - 트리 경로 질의는 오일러 투어로 펼쳐 트리에서의 Mo로 변형한다.
add/remove가 무거우면(예: \(O(\log N)\)) 그만큼 곱해진다. 가능하면 \(O(1)\)
갱신을 설계하는 것이 핵심이다.
Mo는 "갱신 가능한 구간 통계"를 자료 구조 없이 정렬만으로 처리하는 우아한
오프라인 도구다.