본문으로 건너뛰기
김신건의 로그

Mo's Algorithm (Mo's)

· 수정 · 📖 약 3분 · 816자/단어 #algorithm #query #mo
mo's algorithm, 모스 알고리즘, mo, offline query sqrt

정의

Mo’s Algorithm 은 오프라인 구간 쿼리를 (L, R) 정렬로 재배치해 포인터 이동을 amortized O((N+Q)√N) 으로 줄이는 기법.

문제 상황과 동기

크기 N 배열에 Q 개의 구간 쿼리 (l, r) 이 주어지고, 구간 안 원소들의 어떤 함수 f 를 계산해야 한다.

  • naive: 매 쿼리마다 l..r 을 새로 순회. O(N · Q).
  • Mo: 쿼리를 L 이 속한 블록 -> R 순으로 정렬. L 과 R 포인터를 앞뒤로 움직이며 f 를 증분 갱신.

핵심 통찰: 포인터 이동을 재사용 하면, L 은 블록 내에서만 흔들리고 R 은 단조 증가. 이동 총합이 O((N+Q)√N).

시각화

핵심 아이디어

invariant: 현재 구간 [L, R) 에 대한 정보를 유지.

sort queries by (L / B, R)   // B ≈ N/√Q
for each query (l, r):
    while L > l: add(--L)
    while R < r: add(R++)
    while L < l: remove(L++)
    while R > r: remove(--R)
    answer[qi] = current_value

amortized 분석:

  • L 이동: 각 블록 내에서 O(B). 블록 수 O(N/B). 총 O(N).
  • R 이동: 각 블록 내에서 단조 증가. 블록 당 O(N). 총 O(N · N/B) = O(N²/B).
  • B = N / √Q 로 잡으면: O(N√Q + Q√N) = O((N+Q)√N).

알고리즘

Mo(queries):
    B = int(sqrt(N)) 또는 N / sqrt(Q)
    sort by (l/B, r if odd block else -r)  // 홀짝 최적화
    L = 0, R = 0, cur = 0
    for each (l, r) in sorted order:
        while L > l: update(--L, +1)
        while R < r: update(R++, +1)
        while L < l: update(L++, -1)
        while R > r: update(--R, -1)
        ans[qi] = cur
    return ans

구현

// Mo's algorithm, distinct count in [l, r]
#include <bits/stdc++.h>
using namespace std;

int main() {
  ios_base::sync_with_stdio(false); cin.tie(nullptr);
  int n; cin >> n;
  vector<int> a(n);
  for (auto& v : a) cin >> v;
  int q; cin >> q;
  struct Q { int l, r, idx; };
  vector<Q> qs(q);
  for (int i = 0; i < q; i++) {
      cin >> qs[i].l >> qs[i].r; qs[i].l--; qs[i].r--;
      qs[i].idx = i;
  }

  int B = int(sqrt(n));
  sort(qs.begin(), qs.end(), [&](auto& x, auto& y) {
      int bx = x.l / B, by = y.l / B;
      if (bx != by) return bx < by;
      return (bx & 1) ? x.r > y.r : x.r < y.r;
  });

  vector<int> cnt(100001, 0), ans(q);
  int L = 0, R = 0, cur = 0;
  auto add = [&](int p) { if (cnt[a[p]]++ == 0) cur++; };
  auto del = [&](int p) { if (--cnt[a[p]] == 0) cur--; };

  add(0);
  for (auto& [l, r, idx] : qs) {
      while (L > l) add(--L);
      while (R < r) add(++R);
      while (L < l) del(L++);
      while (R > r) del(R--);
      ans[idx] = cur;
  }
  for (int x : ans) cout << x << "\n";
}
stdin
5 3
1 2 1 3 2
3
1 3
2 4
1 5
결과
2
3
3

복잡도

항목
시간 (최선)O((N+Q)√N)
시간 (평균)O((N+Q)√N)
시간 (최악)O((N+Q)√N)
공간O(N + Q)
안정성N/A (오프라인)

변형 / 활용

변형설명
Mo with update쿼리 시간축 추가. 3D Mo O(N^(5/3)).
Mo on treeEuler tour technique 로 수열로 펴서 Mo.
Mo on 2D(x, y) 직사각형. 정렬 차원 증가.
홀짝 최적화block 이 홀수면 R 내림차순 정렬 — R 이동 중복 감소.

함정

1. add/remove 순서

while 문에서 add/remove 순서를 잘못 쓰면 구간 정의가 깨짐. 반드시 포인터 먼저 움직인 후 add.

2. 초기 구간

초기 L, R = 0 이고 원소 a[0] 이 이미 포함된 상태. 첫 add(0) 을 빼먹지 말 것.

3. 홀짝 최적화 없이 단순 정렬

R 이 블록마다 다시 0까지 내려가면 O(N²) 까지 가능. 홀짝 최적화는 거의 필수.

BOJ 연습 문제

번호제목정답률링크
BOJ 13547수열과 쿼리 5-kokoa-lab
BOJ 2912백설공주와 난쟁이-kokoa-lab
BOJ 8462배열의 힘-kokoa-lab

참고

이 글의 용어 (3개)
세그먼트 트리 (Segment Tree)algorithm
정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
제곱근 분할 (Sqrt Decomposition)algorithm
정의 제곱근 분할 (Sqrt Decomposition) 은 크기 N 배열을 √N 개씩 블록으로 나누고, 각 블록을 전처리해 구간 연산을 O(√N) 에 처리하는 자료구조 기법. 문…
Fenwick Tree (Binary Indexed Tree): 구간 합 O(log N)algorithm
정의 Fenwick Tree (또는 BIT, Binary Indexed Tree) 는 배열의 prefix sum 을 O(log N) 에 갱신·조회 하는 자료구조입니다. Peter…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기