Range Minimum Query (RMQ) – Sparse Table
1. Range Minimum Query (RMQ)
Bài toán
Cho mảng \(A\) gồm \(N\) phần tử và \(Q\) truy vấn có dạng (\(l\),\(r\)) \((N, Q ≤ 10^{5})\)
. Với mỗi truy vấn, in ra giá trị nhỏ nhất trong mảng \(A\) từ \(l\) đến \(r\)
. Ví dụ: \(A\) = \([3,6,3,9,10,2]\) \(→\) \(min[2…5] = min(6,3,9,10) = 10\)
Thuật trâu
Ta sẽ duyệt qua tất cả các phần tử có trong đoạn \((l,r)\) xem giá trị min là bao nhiêu rồi in ra giá trị đó \(O(N* Q)\)
Do đó với \(N, Q\) lớn thì thuật toán này sẽ bị TLE
Thuật cải tiến
Ta nhận thấy để tìm min 1 đoạn \((l,r)\) ta có thể tìm min đoạn \((l,r)\) và đoạn \((x + 1,r)\) rồi so sánh 2 kết quả đó với nhau xong in ra số nhỏ nhất
Cải tiến 1
Thay vì duyệt qua từng phần tử, ta có thể duyệt qua từng nhóm 2 phần tử. Từ đó, ta có thể giảm thời gian truy vấn xuống còn \(O(N/2)\)

int a[N], a2[N]; void pre() { for(int i = 1 ; i < n; ++i) a2[i] = min(a[i], a[i + 1]); } int xuly(int l, int r) { int len = r - l + 1; if (len == 1) return a[l]; int mn = INT_MAX; for(int i = l ; i < r ; i += 2) mn = min(mn, a2[i]); // khi kết thúc vòng lặp phía trên, có thể còn 1 số phần tử chưa được xét // ví dụ [l, r] = [3, 7], ta chỉ mới xét a[3...6] chứ chưa xét a[7] // do đó ta chỉ cần xét thêm a2[r - 1] nữa là đủ do a2[r - 1] = min(a[r - 1], a[r]) // gtri vẫn nằm trong đoạn [l, r] mà đề bài yêu cầu mn = min(mn, a2[r - 1]); return mn; }
Cải tiến 2
Tiếp tục cải tiến như phần 1: Thay vì duyệt qua từng nhóm 2 phần tử, ta có thể duyệt qua từng nhóm 4 phần tử. Từ đó, ta có thể giảm thời gian truy vấn xuống còn \(O(N/4)\)

int a[N], a2[N], a4[N]; void pre() { for(int i = 1 ; i < n; ++i) a2[i] = min(a[i], a[i + 1]); for(int i = 1 ; i + 3 <= n; ++i) a4[i] = min(a2[i], a2[i + 1]); } int xuly(int l, int r) { int len = r - l + 1; if (len == 1) return a[l]; else if (len < 4) { return min(a2[l], a2[r - 1]); // do a[l, l + 1] và a[r - 1, r] sẽ nằm trong đoạn [l, r] mà đề bài yêu cầu } int mn = INT_MAX; for(int i = l ; i + 3 <= r ; i += 4) mn = min(mn, a4[i]); // tương tự như phần 1 với những TH thiếu 1 số số chưa được xét ta chỉ cần xét thêm a4[r - 3] là đủ mn = min(mn, a4[r - 3]); return mn; }
Cải tiến 3
Cứ tiếp tục như vậy ta sẽ tối ưu thuật toán đến \(log_{2} N\)
- Nếu dùng mảng \(a[log_{2} N]\) thì rất loằng ngoằng => dễ nhầm lẫn.
- Ta có thể gọi:
- \(st[j][i]\) là giá trị lớn nhất của \(2^{j}\) phần tử tính từ i(min của \(a[i…i+2^{j}−1])\), tương ứng với \(a(2^{j})\)[i]
- Ta có :
Nếu \(j = 0\) => \(st[j][i] = a[i]\)
Nếu \(j > 0\) => \(st[j][i] = min(st[j−1][i],st[j−1][i+2j−1])\)
- Tiền xử lý: \(O(N log N)\)
- Mỗi truy vẫn \(O(1)\)
- Có \(Q\) truy vấn, vì thế tổng độ phức tạp thời gian là \(O(N logN+Q)\)
// __lg(x) là hàm trả về log2 của x // ví dụ: N = 10^5 thì __lg(N) = 16 vì 2^16 = 65536 // LOG = __lg(N) int a[N], st[LOG + 1][N]; void pre() { for(int i = 1 ; i <= n ; ++i) st[0][i] = a[i]; for(int j = 1 ; j <= LOG ; ++j) for(int i = 1 ; i + (1 << j) - 1 <= n ; ++i) st[j][i] = min(st[j - 1][i], st[j - 1][i + (1 << (j - 1))]); } int xuly(int l, int r) { int x = __lg(r - l + 1); return min(st[x][l], st[x][r - (1 << x) + 1]); }
2. Range Sum Queries (RSQ)
Bài toán
Cho mảng \(A\) gồm \(N\) phần tử và \(Q\) truy vấn có dạng \((l,r)\)
. Với mỗi truy vấn, in ra tổng các phần tử trong mảng \(A\) từ \(l\) đến \(r\)
. Giới hạn: \(N, Q ≤ 10^{5}\)
Nguồn bài : https://lqdoj.edu.vn/problem/cses1646
Nhận xét: Ta luôn có thể tách một số nguyên dương thành tổng các lũy thừa phân biệt của 2 (hệ nhị phân). VD: 18 = 2^{4} + 2^{1}
Từ nhận xét trên, ta có thể tách \([l…r]\) thành log2 đoạn có độ dài 2^{x} như sau:
- Đặt \(len\) = r − l + 1
- Duyệt \(j\) từ 0 đến __lg(len), nếu bit thứ \(j\) của \(len\) là \(1\) thì:
- Ta tách \([l…r]\) thành \([l…l+2^{j}−1]\) và \([l+2^{j}…r]\)
- \(l=l+2^{j}\) (tiếp tục tách \([l+2^{j}…r]\) như \([l…r]\)
// __lg(x) là hàm trả về log2 của x // ví dụ: N = 10^5 thì __lg(N) = 16 vì 2^16 = 65536 // LOG = __lg(N) int a[N], st[LOG + 1][N]; void pre() { for(int i = 1 ; i <= n ; ++i) st[0][i] = a[i]; for(int j = 1 ; j <= LOG ; ++j) for(int i = 1 ; i + (1 << j) - 1 <= n ; ++i) st[j][i] = st[j - 1][i] + st[j - 1][i + (1 << (j - 1))]; } int xuly(int l, int r) { int len = r - l + 1; int s = 0; for(int j = 0 ; (1 << j) <= len ; ++j) { if (len >> j & 1) { s = s + st[j][l]; l = l + (1 << j); } } return sum; }
3. Khi nào chúng ta nên sử dụng RMQ ?
- Nên sử dụng RMQ cho những bài toán không có yêu cầu thay đổi các phẩn tử trong mảng do RMQ không thể giải được các bài toán đó.
- Nên sử dụng RMQ khi số lượng truy vấn \(1e6 ≤ Q ≤ 1e7\) do độ phức tạp trong mỗi truy vấn của thuật toán này là \(O(1)\) tránh bị TLE.
4. Luyện tập
Library Checker – Static RMQ
VNOJ – AVLBIT (Dãy cấp số cộng)
Codeforces – 5C (Longest Regular Bracket Sequence)
Codeforces – 1454F (Array Partition)
Codechef – FRMQ (Chef and Array)
Codeforces – 475D (CGCDSSQ)
Codeforces – 487B (Strip)







