<h1>
Range Minimum Query (RMQ) - Sparse Table
</h1>
<h2>
1. Range Minimum Query (RMQ)
</h2>
<h3>
Bài toán
</h3>
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
<h3>
Thuật trâu
</h3>
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
<h3>
Thuật cải tiến
</h3>
Ta nhận thấy để tìm min 1 đoạn ($l$,$r$) ta có thể tìm min đoạn ($l$,$x$) 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
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;
}
```
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;
}
```
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*)) [*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])$
- Khi đó độ phức tạp của bài toán sẽ là:
+ 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*log*N*+*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]);
}
```
<h2>
2. Range Sum Queries (RSQ)
</h2>
<h3>
Bài toán
</h3>
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^
<strong>Nguồn bài : [https://lqdoj.edu.vn/problem/cses1646](https://)</strong>
<strong>Nhận xét:</strong> 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;
}
```
<h2>
3. Khi nào chúng ta nên sử dụng RMQ ?
</h2>
- 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.
<h2>
4. Luyện tập
</h2>
- [Library Checker - Static RMQ](https://judge.yosupo.jp/problem/staticrmq)
- [VNOJ - AVLBIT (Dãy cấp số cộng)](https://oj.vnoi.info/problem/avlbit)
- [Codeforces - 5C (Longest Regular Bracket Sequence)](https://codeforces.com/contest/5/problem/C)
- [Codeforces - 1454F (Array Partition)](https://codeforces.com/contest/1454/problem/F)
- [Codechef - FRMQ (Chef and Array)](https://www.codechef.com/problems/FRMQ)
- [Codeforces - 475D (CGCDSSQ)](https://codeforces.com/contest/475/problem/D)
- [Codeforces - 487B (Strip)](https://codeforces.com/contest/487/problem/B)