Beberapa persoalan range query tidak bisa diselesaikan dengan segment tree atau Fenwick karena operasinya tidak asosiatif atau memerlukan state kompleks (mis. count distinct elemen, sum of squares of frequency). Untuk skenario offline (semua query diketahui di awal) tanpa update, Mo’s algorithm memberi solusi O((n+q)√n) dengan teknik yang elegan: urutkan query berdasarkan blok √n, dan geser dua pointer L dan R secara incremental antar query. Selain Mo, terdapat juga sqrt decomposition biasa dan Hilbert curve Mo yang lebih optimal. Pada Chapter ini kita membedah Mo’s algorithm dengan implementasi penuh, menjawab query “sum of value × freq²” yang sulit dilakukan dengan struktur data biasa.
Aturan: Offline Range Query Tanpa Update #
Diberikan array A[0..n-1] statis (tidak ada update) dan q query berupa range [l, r]. Untuk setiap query, hitung agregat yang tidak trivial. Contoh:
- Count distinct elemen di
[l, r]. - Sum of
freq(x)² × xuntuk setiap nilaixdi[l, r]. - Number of inversions in subarray
[l, r]. - Mode (elemen paling sering muncul) di
[l, r].
Untuk operasi-operasi ini, segment tree lazy tidak langsung apply karena tidak monoid sederhana. Mo’s algorithm menyelesaikannya dalam O((n+q)√n) bila add/remove elemen O(1).
Mekanisme: Sort by Block + Slide Two Pointers #
Ide Mo:
- Bagi array menjadi blok ukuran
B = √n. Setiap indeksimasuk bloki / B. - Sortir query dengan komparator:
- Blok dari
Lterkecil ke terbesar. - Dalam blok yang sama, sortir
Rnaik (atau turun untuk blok ganjil — optimasi Hilbert-like).
- Blok dari
- Proses query secara berurutan, geser pointer
cur_ldancur_rdari query sebelumnya ke query saat ini:- Expand:
add(--cur_l)atauadd(++cur_r). - Shrink:
remove(cur_l++)atauremove(cur_r--).
- Expand:
- Total pergerakan pointer = O(n√n) (lihat analisis di bawah).
Analisis Kompleksitas #
Misal n elemen, q query, B = √n:
- Pergerakan
R: dalam satu blokL,Rmonoton naik. Total gerakanRper blok = O(n). Adan/B = √nblok → totalRmovement = O(n√n). - Pergerakan
L: dalam satu query,Lbergerak paling jauhB = √n. TotalLmovement per blok = O(qB/√n × √n) = O(q√n)? Sebenarnya total =O(q × B)=O(q√n). - Total:
O((n + q)√n).
Bila q ≈ n, total = O(n^1.5). Untuk n = 10^5, ini sekitar 3 × 10^7 operasi — bisa dijalankan.
Implementasi Mo’s Algorithm #
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <map>
// Mo's algorithm: process offline range queries in sqrt(n) blocks.
// Adding/removing element O(1) amortized -> total O((n+q) * sqrt(n)).
int block_size;
std::vector<int> arr;
long long current_answer = 0;
int freq[100]; // assume values in [0, 100)
void add(int idx) {
int v = arr[idx];
int old = freq[v];
// contribution: sum of (freq(x))^2 * x for each distinct value x
current_answer -= (long long)old * old * v;
freq[v]++;
int now = freq[v];
current_answer += (long long)now * now * v;
}
void remove(int idx) {
int v = arr[idx];
int old = freq[v];
current_answer -= (long long)old * old * v;
freq[v]--;
int now = freq[v];
current_answer += (long long)now * now * v;
}
struct Query {
int l, r, idx;
long long ans;
};
int main() {
arr = {1, 2, 1, 3, 1, 2, 1, 4, 2, 1};
int n = arr.size();
std::vector<Query> queries = {
{0, 4, 0, 0}, {1, 5, 1, 0}, {2, 7, 2, 0}, {0, 9, 3, 0}, {3, 6, 4, 0}
};
block_size = std::max(1, (int)std::sqrt((double)n));
// Sort queries: by block of L, then by R (alternating for even blocks - hilbert optimization)
auto mo_cmp = [&](const Query& a, const Query& b) {
int ba = a.l / block_size;
int bb = b.l / block_size;
if (ba != bb) return ba < bb;
// Hilbert-like: alternasi naik/turun R untuk blok genap/ganjil
if (ba % 2 == 0) return a.r < b.r;
return a.r > b.r;
};
std::sort(queries.begin(), queries.end(), mo_cmp);
int cur_l = 0, cur_r = -1;
for (auto& Q : queries) {
while (cur_l > Q.l) add(--cur_l);
while (cur_r < Q.r) add(++cur_r);
while (cur_l < Q.l) remove(cur_l++);
while (cur_r > Q.r) remove(cur_r--);
Q.ans = current_answer;
}
// Restore original order
std::sort(queries.begin(), queries.end(), [](const Query& a, const Query& b) {
return a.idx < b.idx;
});
for (const auto& Q : queries) {
std::cout << "Q[" << Q.idx << "] range [" << Q.l << "," << Q.r << "] = " << Q.ans << '\n';
}
return 0;
}Output yang Dihasilkan:
Q[0] range [0,4] = 14
Q[1] range [1,5] = 15
Q[2] range [2,7] = 18
Q[3] range [0,9] = 50
Q[4] range [3,6] = 9Bedah Kode Baris-per-Baris #
block_size = sqrt(n): ukuran blok optimal untuk Mo’s. Bila q » n, gunakann / sqrt(q)(block size disesuaikan).freq[100]: frequency counter untuk setiap nilai (asumsi nilai di [0, 100)). Untuk nilai besar, koordinat compress dulu ke [0, n) sebelum Mo.add(idx): tambahkanarr[idx]ke jendela saat ini. Updatecurrent_answerdengan kontribusi nilaiv = arr[idx]:- Sebelum add: kontribusi
vadalahfreq[v]² × v(lama). - Setelah
freq[v]++: kontribusi baru(freq[v]+1)² × v. - Update
current_answer -= old² × v; freq[v]++; current_answer += new² × v;.
- Sebelum add: kontribusi
remove(idx): kebalikanadd. Kurangifreq[v], update kontribusi.mo_cmp: komparator sort. Pertama sort by blokL / block_size. Dalam blok yang sama, bila blok genap sortRnaik, bila ganjil sortRturun. Trik alternasi arah menghemat pergerakanR(saat pindah blok genap ke ganjil,Rtidak perlu reset ke 0 — lanjut dari posisi terakhir).while (cur_l > Q.l) add(--cur_l): expand jendela ke kiri.--cur_ldulu (pindah ke kiri), baru add elemen baru.while (cur_r < Q.r) add(++cur_r): expand jendela ke kanan.while (cur_l < Q.l) remove(cur_l++): shrink dari kiri.remove(cur_l)dulu (elemen dicur_lkeluar), barucur_l++.while (cur_r > Q.r) remove(cur_r--): shrink dari kanan.- Urutan expand sebelum shrink penting: bila jendela baru lebih besar dari lama, expand dulu agar tidak pernah kosong. Bila shrink dulu, bisa terjadi
cur_l > cur_r(jendela kosong) yang break invariantadd/remove. Q.ans = current_answer: jawaban query = state saat ini.- Restore original order: sort kembali by
idxagar output sesuai urutan query input. - Verifikasi:
arr = [1,2,1,3,1,2,1,4,2,1].- Q[0] range [0,4] = arr[0..4] = [1,2,1,3,1]. freq: 1→3, 2→1, 3→1. Sum = 3²×1 + 1²×2 + 1²×3 = 9+2+3 = 14. ✓
- Q[1] range [1,5] = [2,1,3,1,2]. freq: 1→2, 2→2, 3→1. Sum = 4×1 + 4×2 + 1×3 = 4+8+3 = 15. ✓
- Q[2] range [2,7] = [1,3,1,2,1,4]. freq: 1→3, 2→1, 3→1, 4→1. Sum = 9×1 + 1×2 + 1×3 + 1×4 = 9+2+3+4 = 18. ✓
- Q[3] range [0,9] = all. freq: 1→5, 2→3, 3→1, 4→1. Sum = 25×1 + 9×2 + 1×3 + 1×4 = 25+18+3+4 = 50. ✓
- Q[4] range [3,6] = [3,1,2,1]. freq: 1→2, 2→1, 3→1. Sum = 4×1 + 1×2 + 1×3 = 4+2+3 = 9. ✓
Variasi: Hilbert Curve Mo #
Hilbert curve Mo menggunakan urutan Hilbert untuk sort query — kompleksitas O((n+q)√n) tetapi konstanta lebih kecil (~2-3× lebih cepat). Ide: petakan setiap (l, r) ke nilai Hilbert curve, sort by nilai Hilbert. Hilbert curve menjaga locality: query yang berdekatan dalam 2D memiliki jarak kecil dalam 1D, mengurangi pergerakan pointer.
// Hilbert order function
long long hilbertOrder(int x, int y, int pow = 21, int rotate = 0) {
if (pow == 0) return 0;
int hpow = 1 << (pow - 1);
int seg = (x < hpow) ? ((y < hpow) ? 0 : 3) : ((y < hpow) ? 1 : 2);
seg = (seg + rotate) & 3;
static const int rotateDelta[4] = {3, 0, 0, 1};
int nx = x & (x ^ hpow), ny = y & (y ^ hpow);
int nrot = (rotate + rotateDelta[seg]) & 3;
long long sub = hilbertOrder(nx, ny, pow - 1, nrot);
return (seg << (2 * (pow - 1))) | sub;
}
// Sort by hilbertOrder(l, r)
Sqrt Decomposition Biasa #
Untuk masalah dengan update dan query, sqrt decomposition biasa juga berguna:
- Bagi array menjadi blok ukuran
B = √n. - Setiap blok menyimpan aggregate (sum, min, sorted list, dsb.).
- Point update: update elemen + aggregate blok. O(1) atau O(log B).
- Range query: scan penuh blok-blok dalam range + elemen-elemen di blok parsial. O(B + n/B) = O(√n).
- Range update: lazy tag per blok. O(√n).
Lebih lambat dari segment tree lazy (O(√n) vs O(log n)) tetapi lebih mudah diimplementasi untuk operasi kompleks.
Kompleksitas #
| Teknik | Preprocess | Query | Update | Use Case |
|---|---|---|---|---|
| Sparse table | O(n log n) | O(1) | — | Static, idempotent |
| Segment tree | O(n) | O(log n) | O(log n) | Dynamic, monoid |
| Fenwick | O(n) | O(log n) | O(log n) | Dynamic, prefix-friendly |
| Mo’s algorithm | — | O((n+q)√n) total | — | Static, offline, non-monoid |
| Sqrt decomposition | O(n) | O(√n) | O(√n) | Dynamic, complex ops |
Ringkasan #
- Mo’s algorithm = sort query by block + slide two pointers. Total O((n+q)√n).
add/removeharus O(1) — bukan monoid sederhana (count distinct, sum freq², mode, inversion count).- Optimasi: alternasi arah
Runtuk blok genap/ganjil menghemat gerakan pointer. - Hilbert curve Mo: kompleksitas sama, konstanta lebih kecil ~2-3×.
- Sqrt decomposition biasa: untuk update + query dengan operasi kompleks. O(√n) per operasi.
- Pilihan: Mo bila offline static, segment tree bila online dynamic, sparse table bila static idempotent.