Chương 2·Bài 22

Sparse Table

Dãy không đổi, nhiều câu hỏi min trên đoạn: duyệt từng đoạn tốn O(n)O(n) mỗi câu, 2⋅1052 \cdot 10^5 câu trên dãy 2⋅1052 \cdot 10^5 phần tử là 4⋅10104 \cdot 10^{10} phép so sánh. Bảng thưa (sparse table) tính sẵn đáp án cho mọi đoạn có độ dài là lũy thừa của hai, mất O(nlog⁡n)O(n \log n); sau đó mỗi câu hỏi chỉ đọc hai ô bảng. Cái giá là dãy không được thay đổi: cần cập nhật thì dùng cây phân đoạn (bài 17).

Bảng cho các đoạn dài lũy thừa hai

Gọi st[k][i] là giá trị nhỏ nhất của đoạn dài 2k2^k bắt đầu tại ii, tức a[i..i+2k−1]a[i..i + 2^k - 1]. Mức 0 là chính dãy, st[0][i] = a[i]. Một đoạn dài 2k2^k là hai nửa liền nhau dài 2k−12^{k-1}, nên

st[k][i]=min⁡(st[k−1][i], st[k−1][i+2k−1]).\mathrm{st}[k][i] = \min\left(\mathrm{st}[k-1][i],\ \mathrm{st}[k-1][i + 2^{k-1}]\right).

Dựng lần lượt từ mức thấp lên, mỗi mức có không quá nn ô và có ⌊log⁡2n⌋+1\lfloor \log_2 n \rfloor + 1 mức: O(nlog⁡n)O(n \log n) thời gian và bộ nhớ. Với n=2⋅105n = 2 \cdot 10^5 cần 18 mức, khoảng 3,6⋅1063{,}6 \cdot 10^6 ô.

Trả lời một đoạn. Đoạn [l,r][l, r] dài L=r−l+1L = r - l + 1, lấy k=⌊log⁡2L⌋k = \lfloor \log_2 L \rfloor. Khối [l,l+2k−1][l, l + 2^k - 1] ở đầu trái và khối [r−2k+1,r][r - 2^k + 1, r] ở đầu phải đều nằm trong đoạn, và vì 2k+1>L2^{k+1} > L nên hai khối phủ kín đoạn, có thể chồng lên nhau. Phần chồng được xét hai lần nhưng không làm sai min, nên đáp án là min(st[k][l], st[k][r - (1 << k) + 1]). Tính sẵn lg[x] =⌊log⁡2x⌋= \lfloor \log_2 x \rfloor bằng lg[x] = lg[x / 2] + 1 để không phải gọi hàm log trên số thực.

Mỗi mức k gộp hai ô liền nhau của mức k − 1. Kéo l, r để xem hai khối chồng nhau; thử phép tổng để thấy nó sai.

Hỏi nhanh

Hỏi min trên đoạn dài 6 bằng hai khối dài 4, hai khối chồng nhau hai phần tử. Vì sao kết quả vẫn đúng?

Ví dụMức Thấp Nhất

Đề. Dãy n≤2⋅105n \le 2 \cdot 10^5 số không đổi, q≤2⋅105q \le 2 \cdot 10^5 câu hỏi: in giá trị nhỏ nhất trên đoạn [l,r][l, r].

Lời giải. Dựng bảng thưa như trên, mỗi câu hỏi đọc hai ô.

Bước 1: dựng bảng. st[k][i] là min của 2k2^k phần tử bắt đầu ở ii; mức kk ghép hai khối liền nhau của mức k−1k - 1. Chỉ dựng các ô có i+2k−1≤ni + 2^k - 1 \le n, tức khối còn nằm trong dãy. lg[i] =⌊log⁡2i⌋= \lfloor \log_2 i \rfloor tính sẵn bằng lg[i / 2] + 1, để câu hỏi khỏi gọi hàm log.

C++
for (ll i = 2; i <= n; i++) lg[i] = lg[i / 2] + 1;
for (ll k = 1; k < LG; k++)
    for (ll i = 1; i + (1 << k) - 1 <= n; i++)
        st[k][i] = min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);

Bước 2: trả lời một đoạn. Với k=⌊log⁡2(r−l+1)⌋k = \lfloor \log_2 (r - l + 1) \rfloor, hai khối dài 2k2^k, một bắt đầu ở ll, một kết thúc ở rr, phủ trọn đoạn và có thể chồng nhau; min không bận tâm phần chồng. Cả hai khối đều nằm trong [l,r][l, r] nên không bao giờ đọc ô ngoài dãy.

C++
ll get(ll l, ll r) {
    ll k = lg[r - l + 1];
    return min(st[k][l], st[k][r - (1 << k) + 1]);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5, LG = 18;
ll n, q, lg[N], st[LG][N];

ll get(ll l, ll r) {
    ll k = lg[r - l + 1];
    return min(st[k][l], st[k][r - (1 << k) + 1]);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> q;
    for (ll i = 1; i <= n; i++) cin >> st[0][i];
    for (ll i = 2; i <= n; i++) lg[i] = lg[i / 2] + 1;
    for (ll k = 1; k < LG; k++)
        for (ll i = 1; i + (1 << k) - 1 <= n; i++)
            st[k][i] = min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
    while (q--) {
        ll l, r;
        cin >> l >> r;
        cout << get(l, r) << '\n';
    }
}
7 4
8 3 6 2 7 2 5
1 3
2 6
4 4
5 7

Bảng 18×2⋅10518 \times 2 \cdot 10^5 ô dựng trong O(nlog⁡n)O(n \log n), mỗi câu hỏi O(1)O(1).

Phép gộp nào dùng được

Cách trả lời bằng hai khối chồng nhau cần phép gộp ff kết hợp (gộp theo thứ tự nào cũng được) và lũy đẳng, f(x,x)=xf(x, x) = x. Min và max thỏa, GCD thỏa vì gcd⁡(x,x)=x\gcd(x, x) = x, AND và OR theo bit cũng thỏa. Có thể giữ nhiều bảng song song, chẳng hạn một bảng min và một bảng max cho độ chênh của đoạn.

Tổng thì không: phần chồng bị cộng hai lần. Tổng trên đoạn tĩnh đã có tổng tiền tố (bài 02) trả lời O(1)O(1); nếu buộc dùng bảng cho phép gộp không lũy đẳng, tách [l,r][l, r] thành các khối rời nhau theo các bit của LL, mất O(log⁡n)O(\log n) mỗi câu. Bảng thưa cũng không hợp với cập nhật: một phần tử nằm trong tới 2k2^k ô ở mức kk, tổng cộng khoảng 2n2n ô phải sửa.

Với GCD, mỗi câu hỏi tốn thêm O(log⁡V)O(\log V) cho phép tính GCD, VV là giá trị lớn nhất, nhưng vẫn nhanh hơn nhiều so với cây phân đoạn vì chỉ có một phép gộp.

Hỏi nhanh

Phép gộp nào không dùng được với cách hỏi bằng hai khối chồng nhau?

Ví dụHai Cận Trong Đoạn

Đề. Dãy n≤105n \le 10^5 số không đổi, m≤105m \le 10^5 câu hỏi: in cả giá trị nhỏ nhất lẫn lớn nhất trên đoạn [l,r][l, r].

Lời giải. Hai bảng dựng cùng lúc, mn theo min và mx theo max, dùng chung mảng lg. Mỗi câu hỏi tính kk và vị trí khối thứ hai j một lần, rồi đọc bốn ô, hai ô mỗi bảng.

C++
ll k = lg[r - l + 1], j = r - (1 << k) + 1;
cout << min(mn[k][l], mn[k][j]) << ' ' << max(mx[k][l], mx[k][j]) << '\n';

Với ví dụ, đoạn [3,6][3, 6] dài 4 nên k=2k = 2 và hai khối trùng nhau, cho 11 và 88.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5, LG = 17;
ll n, m, lg[N], mn[LG][N], mx[LG][N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    for (ll i = 1; i <= n; i++) {
        cin >> mn[0][i];
        mx[0][i] = mn[0][i];
    }
    for (ll i = 2; i <= n; i++) lg[i] = lg[i / 2] + 1;
    for (ll k = 1; k < LG; k++)
        for (ll i = 1; i + (1 << k) - 1 <= n; i++) {
            mn[k][i] = min(mn[k - 1][i], mn[k - 1][i + (1 << (k - 1))]);
            mx[k][i] = max(mx[k - 1][i], mx[k - 1][i + (1 << (k - 1))]);
        }
    while (m--) {
        ll l, r;
        cin >> l >> r;
        ll k = lg[r - l + 1], j = r - (1 << k) + 1;
        cout << min(mn[k][l], mn[k][j]) << ' ' << max(mx[k][l], mx[k][j]) << '\n';
    }
}
6 3
9
4
7
1
8
3
1 4
3 6
5 5

Dựng O(nlog⁡n)O(n \log n), mỗi câu hỏi O(1)O(1).

Ví dụKhác Ước Chung

Đề. Dãy n≤105n \le 10^5 số nguyên dương. Mỗi câu hỏi [l,r][l, r]: gọi gg là GCD của đoạn, đếm số vị trí trong đoạn có ai≠ga_i \ne g.

Lời giải. GCD của đoạn chia hết mọi phần tử của đoạn, nên phần tử khác gg chính là phần tử lớn hơn gg; đáp án là độ dài đoạn trừ số phần tử bằng đúng gg. Việc còn lại là lấy gg và đếm số lần gg xuất hiện trong [l,r][l, r]. Với đoạn [4,7][4, 7] là 6,9,3,66, 9, 3, 6: g=3g = 3, chỉ một vị trí bằng 3, đáp án 3.

Bước 1: bảng thưa GCD. Như bảng min, thay min bằng __gcd. GCD lũy đẳng (gcd⁡(x,x)=x\gcd(x, x) = x) nên hai khối chồng nhau vẫn cho đúng.

C++
ll get(ll l, ll r) {
    ll k = lg[r - l + 1];
    return __gcd(st[k][l], st[k][r - (1 << k) + 1]);
}

Bước 2: các vị trí theo giá trị. Sắp các cặp (giá trị, chỉ số) tăng dần. Các vị trí cùng giá trị gg nằm liền nhau và tăng theo chỉ số, nên hai lần lower_bound với (g,l)(g, l) và (g,r+1)(g, r + 1) chặn đúng các vị trí có giá trị gg và chỉ số trong [l,r][l, r].

C++
ll g = get(l, r);
ll same = lower_bound(v + 1, v + n + 1, make_pair(g, r + 1)) - lower_bound(v + 1, v + n + 1, make_pair(g, l));
cout << r - l + 1 - same << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5, LG = 17;
ll n, q, lg[N], st[LG][N];
pair<ll, ll> v[N];

ll get(ll l, ll r) {
    ll k = lg[r - l + 1];
    return __gcd(st[k][l], st[k][r - (1 << k) + 1]);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> st[0][i];
        v[i] = {st[0][i], i};
    }
    sort(v + 1, v + n + 1);
    for (ll i = 2; i <= n; i++) lg[i] = lg[i / 2] + 1;
    for (ll k = 1; k < LG; k++)
        for (ll i = 1; i + (1 << k) - 1 <= n; i++)
            st[k][i] = __gcd(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
    cin >> q;
    while (q--) {
        ll l, r;
        cin >> l >> r;
        ll g = get(l, r);
        ll same = lower_bound(v + 1, v + n + 1, make_pair(g, r + 1)) - lower_bound(v + 1, v + n + 1, make_pair(g, l));
        cout << r - l + 1 - same << '\n';
    }
}
7
6 10 15 6 9 3 6
5
1 3
1 4
4 7
1 1
1 7

Dựng O(nlog⁡n)O(n \log n), mỗi câu hỏi O(log⁡n)O(\log n) cho hai lần chặt nhị phân.

Tìm kiếm nhị phân theo đầu phải

Cố định đầu trái ll và kéo dài đầu phải rr: min chỉ giảm, max chỉ tăng, GCD chỉ giảm hoặc giữ nguyên (mỗi lần đổi ít nhất chia đôi, nên chỉ đổi không quá log⁡2V\log_2 V lần), AND chỉ mất bit, OR chỉ thêm bit. Mọi điều kiện kiểu “max − min ≤k\le k”, “GCD >1> 1”, “AND ≥k\ge k” vì thế đúng trên một khúc đầu các giá trị rr rồi sai mãi về sau. Với mỗi ll, tìm kiếm nhị phân (bài 05) vị trí rr xa nhất còn thỏa, mỗi bước kiểm bằng một câu hỏi O(1)O(1) trên bảng: tổng O(nlog⁡n)O(n \log n).

Hai con trỏ (bài 06) cũng làm được khi đầu trái và đầu phải cùng tiến, nhưng cần một cấu trúc gộp được khi bỏ phần tử ở đầu trái, như hàng đợi đơn điệu cho min/max; với GCD không có cách bỏ phần tử đơn giản, nên bảng thưa cộng tìm kiếm nhị phân là cách gọn nhất.

Ví dụĐoạn Ổn Định Dài Nhất

Đề. Dãy n≤105n \le 10^5 số và số kk. Đoạn ổn định nếu max − min không vượt kk. In độ dài lớn nhất LL của đoạn ổn định, số đoạn dài đúng LL, rồi liệt kê chúng theo đầu trái tăng dần.

Lời giải. Với mỗi đầu trái ii, max − min của [i,r][i, r] không giảm khi rr tăng (max chỉ tăng, min chỉ giảm khi kéo dài), nên tìm kiếm nhị phân rr xa nhất còn ổn định, gọi là RiR_i. Đoạn ổn định dài nhất bắt đầu tại ii là [i,Ri][i, R_i]; độ dài lớn nhất toàn bài là max của Ri−i+1R_i - i + 1, và mỗi đoạn dài đúng LL bắt đầu tại một ii có Ri−i+1=LR_i - i + 1 = L, nên duyệt ii tăng dần là có đúng thứ tự cần in. Với ví dụ, ba đầu trái 1, 4, 7 cho đoạn dài 3.

Với đầu trái đã chọn, mỗi bước thử một r giữa vùng chưa biết; ô xanh lá là đoạn ổn định tìm được.

Bước 1: kiểm một đoạn trong O(1)O(1). Hai bảng thưa min và max như ví dụ trước; ok(l, r) đọc max và min của đoạn rồi so hiệu với kk.

C++
bool ok(ll l, ll r) {
    ll t = lg[r - l + 1], j = r - (1 << t) + 1;
    return max(mx[t][l], mx[t][j]) - min(mn[t][l], mn[t][j]) <= k;
}

Bước 2: chặt nhị phân cho mỗi đầu trái. Đoạn một phần tử luôn ổn định nên res bắt đầu bằng ii.

C++
for (ll i = 1; i <= n; i++) {
    ll l = i, r = n, res = i;
    while (l <= r) {
        ll mid = (l + r) >> 1;
        if (ok(i, mid)) l = (res = mid) + 1;
        else r = mid - 1;
    }
    R[i] = res;
    best = max(best, res - i + 1);
}

Bước 3: đếm và in. Lượt thứ hai đi lại ii tăng dần, đếm rồi in các đoạn có độ dài bằng best.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5, LG = 17;
ll n, k, lg[N], mn[LG][N], mx[LG][N], R[N];

bool ok(ll l, ll r) {
    ll t = lg[r - l + 1], j = r - (1 << t) + 1;
    return max(mx[t][l], mx[t][j]) - min(mn[t][l], mn[t][j]) <= k;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> k;
    for (ll i = 1; i <= n; i++) {
        cin >> mn[0][i];
        mx[0][i] = mn[0][i];
    }
    for (ll i = 2; i <= n; i++) lg[i] = lg[i / 2] + 1;
    for (ll t = 1; t < LG; t++)
        for (ll i = 1; i + (1 << t) - 1 <= n; i++) {
            mn[t][i] = min(mn[t - 1][i], mn[t - 1][i + (1 << (t - 1))]);
            mx[t][i] = max(mx[t - 1][i], mx[t - 1][i + (1 << (t - 1))]);
        }
    ll best = 0, cnt = 0;
    for (ll i = 1; i <= n; i++) {
        ll l = i, r = n, res = i;
        while (l <= r) {
            ll mid = (l + r) >> 1;
            if (ok(i, mid)) l = (res = mid) + 1;
            else r = mid - 1;
        }
        R[i] = res;
        best = max(best, res - i + 1);
    }
    for (ll i = 1; i <= n; i++)
        if (R[i] - i + 1 == best) cnt++;
    cout << best << ' ' << cnt << '\n';
    for (ll i = 1; i <= n; i++)
        if (R[i] - i + 1 == best) cout << i << ' ' << R[i] << '\n';
}
9 2
4 6 5 9 8 7 1 2 3

Dựng O(nlog⁡n)O(n \log n), mỗi đầu trái O(log⁡n)O(\log n) lần kiểm O(1)O(1): tổng O(nlog⁡n)O(n \log n).

Ví dụĐoạn Cùng Phần Dư

Đề. Nhiều test, tổng n≤2⋅105n \le 2 \cdot 10^5, các số đôi một khác nhau và tới 101810^{18}. Tìm đoạn dài nhất mà có một m≥2m \ge 2 làm mọi phần tử trong đoạn cùng số dư khi chia cho mm.

Lời giải. Mọi phần tử cùng dư theo mm khi và chỉ khi mm chia hết mọi hiệu kề nhau trong đoạn, tức chia hết GCD của các hiệu đó. Đặt di=∣ai+1−ai∣d_i = |a_{i+1} - a_i|; đoạn [i,j+1][i, j + 1] tốt khi gcd⁡(di,…,dj)>1\gcd(d_i, \dots, d_j) > 1. GCD không tăng khi kéo dài, nên với mỗi ii tìm kiếm nhị phân jj xa nhất. Các số khác nhau nên di≥1d_i \ge 1, và đoạn một phần tử luôn tốt. Với test đầu, hiệu là 7,3,6,6,137, 3, 6, 6, 13, đoạn hiệu 3,6,63, 6, 6 có GCD 3, cho đoạn dài 4.

Bước 1: bảng thưa GCD trên mảng hiệu. lg tính một lần cho mọi test; mỗi test dựng lại bảng trên mảng hiệu dài m=n−1m = n - 1.

C++
for (ll i = 1; i <= m; i++) st[0][i] = abs(a[i + 1] - a[i]);
for (ll k = 1; k < LG; k++)
    for (ll i = 1; i + (1 << k) - 1 <= m; i++)
        st[k][i] = __gcd(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);

Bước 2: chặt nhị phân cho mỗi ii. res = i - 1 nghĩa là chưa có jj nào: đoạn tốt dài nhất bắt đầu ở ii chỉ có 1 phần tử. Đoạn hiệu [i,j][i, j] ứng với đoạn số [i,j+1][i, j + 1], dài j−i+2j - i + 2.

C++
for (ll i = 1; i <= m; i++) {
    ll l = i, r = m, res = i - 1;
    while (l <= r) {
        ll mid = (l + r) >> 1;
        if (get(i, mid) > 1) l = (res = mid) + 1;
        else r = mid - 1;
    }
    ans = max(ans, res - i + 2);
}

Với n=1n = 1 không có hiệu nào, vòng lặp không chạy và ans giữ 1.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5, LG = 18;
ll n, a[N], lg[N], st[LG][N];

ll get(ll l, ll r) {
    ll k = lg[r - l + 1];
    return __gcd(st[k][l], st[k][r - (1 << k) + 1]);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    for (ll i = 2; i < N; i++) lg[i] = lg[i / 2] + 1;
    ll T;
    cin >> T;
    while (T--) {
        cin >> n;
        for (ll i = 1; i <= n; i++) cin >> a[i];
        ll m = n - 1, ans = 1;
        for (ll i = 1; i <= m; i++) st[0][i] = abs(a[i + 1] - a[i]);
        for (ll k = 1; k < LG; k++)
            for (ll i = 1; i + (1 << k) - 1 <= m; i++)
                st[k][i] = __gcd(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
        for (ll i = 1; i <= m; i++) {
            ll l = i, r = m, res = i - 1;
            while (l <= r) {
                ll mid = (l + r) >> 1;
                if (get(i, mid) > 1) l = (res = mid) + 1;
                else r = mid - 1;
            }
            ans = max(ans, res - i + 2);
        }
        cout << ans << '\n';
    }
}
3
6
1 8 5 11 17 4
1
10
5
6 10 14 9 21

Mỗi test O(nlog⁡n)O(n \log n) cả dựng lẫn chặt nhị phân, cộng một thừa số log⁡\log cho phép tính GCD.

Bảng thưa hai chiều

Ý tưởng mở rộng nguyên vẹn sang ma trận. Khi mọi câu hỏi là hình vuông, gọi mn[k][i][j] là min của hình vuông cạnh 2k2^k có góc trên trái (i,j)(i, j); nó là min của bốn hình vuông cạnh 2k−12^{k-1} ở bốn góc. Hình vuông cạnh bb được phủ bởi bốn hình vuông cạnh 2k2^k với k=⌊log⁡2b⌋k = \lfloor \log_2 b \rfloor, đặt ở bốn góc của nó; chúng chồng nhau, nhưng min và max không bận tâm. Bộ nhớ O(n2log⁡n)O(n^2 \log n), mỗi câu hỏi O(1)O(1).

Hình chữ nhật tùy ý cần hai chỉ số mũ riêng cho hàng và cột, st[a][b][i][j] cho khối 2a×2b2^a \times 2^b, tốn O(nmlog⁡nlog⁡m)O(nm \log n \log m) bộ nhớ; chỉ dùng khi ma trận nhỏ, cỡ 500×500500 \times 500.

Ví dụĐộ Chênh Trên Ô Vuông

Đề. Ma trận n×nn \times n với n≤250n \le 250 và một cạnh bb cố định. Mỗi câu hỏi (x,y)(x, y) là hình vuông b×bb \times b có góc trên trái (x,y)(x, y); in max − min trong hình vuông đó. Có tới 10510^5 câu hỏi.

Lời giải. Bảng thưa hai chiều cho hình vuông: ô mn[k][i][j] là min của hình vuông cạnh 2k2^k góc trên trái (i,j)(i, j), và tương tự mx. Bốn hình vuông cạnh 2k2^k đặt ở bốn góc phủ trọn hình vuông cạnh bb khi 2k≤b<2k+12^k \le b < 2^{k+1}. Duyệt thẳng b2b^2 ô mỗi câu là 6⋅1096 \cdot 10^9 phép khi b=n=250b = n = 250. Với câu hỏi (2,2)(2, 2) hình vuông gồm 3,6,9,13, 6, 9, 1, độ chênh 88.

Bốn khung màu là bốn hình vuông cạnh 2ᵏ phủ hình vuông cam; bấm ô để dời góc, kéo b để đổi cạnh.

Bước 1: dựng hai bảng. Hình vuông cạnh 2k2^k ghép từ bốn hình vuông cạnh 2k−12^{k-1}, góc trên trái lệch nhau h=2k−1h = 2^{k-1} theo hàng và theo cột.

C++
for (ll k = 1; k <= lg[n]; k++) {
    ll h = 1 << (k - 1);
    for (ll i = 1; i + 2 * h - 1 <= n; i++)
        for (ll j = 1; j + 2 * h - 1 <= n; j++) {
            mn[k][i][j] = min({mn[k - 1][i][j], mn[k - 1][i + h][j], mn[k - 1][i][j + h], mn[k - 1][i + h][j + h]});
            mx[k][i][j] = max({mx[k - 1][i][j], mx[k - 1][i + h][j], mx[k - 1][i][j + h], mx[k - 1][i + h][j + h]});
        }
}

Bước 2: trả lời. Mọi câu hỏi cùng cạnh bb, nên k=⌊log⁡2b⌋k = \lfloor \log_2 b \rfloor và độ dời s = b - 2^k tính một lần ngoài vòng. Bốn góc là (x,y)(x, y), (x+s,y)(x + s, y), (x,y+s)(x, y + s), (x+s,y+s)(x + s, y + s).

C++
ll k = lg[b], s = b - (1 << k);
while (q--) {
    ll x, y;
    cin >> x >> y;
    ll lo = min({mn[k][x][y], mn[k][x + s][y], mn[k][x][y + s], mn[k][x + s][y + s]});
    ll hi = max({mx[k][x][y], mx[k][x + s][y], mx[k][x][y + s], mx[k][x + s][y + s]});
    cout << hi - lo << '\n';
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 255;
ll n, b, q, lg[N], mn[8][N][N], mx[8][N][N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> b >> q;
    for (ll i = 1; i <= n; i++)
        for (ll j = 1; j <= n; j++) {
            cin >> mn[0][i][j];
            mx[0][i][j] = mn[0][i][j];
        }
    for (ll i = 2; i <= n; i++) lg[i] = lg[i / 2] + 1;
    for (ll k = 1; k <= lg[n]; k++) {
        ll h = 1 << (k - 1);
        for (ll i = 1; i + 2 * h - 1 <= n; i++)
            for (ll j = 1; j + 2 * h - 1 <= n; j++) {
                mn[k][i][j] = min({mn[k - 1][i][j], mn[k - 1][i + h][j], mn[k - 1][i][j + h], mn[k - 1][i + h][j + h]});
                mx[k][i][j] = max({mx[k - 1][i][j], mx[k - 1][i + h][j], mx[k - 1][i][j + h], mx[k - 1][i + h][j + h]});
            }
    }
    ll k = lg[b], s = b - (1 << k);
    while (q--) {
        ll x, y;
        cin >> x >> y;
        ll lo = min({mn[k][x][y], mn[k][x + s][y], mn[k][x][y + s], mn[k][x + s][y + s]});
        ll hi = max({mx[k][x][y], mx[k][x + s][y], mx[k][x][y + s], mx[k][x + s][y + s]});
        cout << hi - lo << '\n';
    }
}
4 2 4
1 5 2 4
7 3 6 0
8 9 1 2
4 6 5 3
1 1
1 3
2 2
3 3

Mỗi bảng 8×25528 \times 255^2 ô, khoảng 4 MB; dựng O(n2log⁡n)O(n^2 \log n), câu hỏi O(1)O(1).

Khi đi thi

  • Dãy tĩnh, câu hỏi min, max, GCD, AND, OR trên đoạn, số câu hỏi lớn hoặc câu hỏi nằm trong vòng tìm kiếm nhị phân: bảng thưa nhanh và ngắn hơn cây phân đoạn; có cập nhật thì đổi sang cây phân đoạn.
  • Ước lượng bộ nhớ trước: 18×2⋅10518 \times 2 \cdot 10^5 số ll là khoảng 29 MB cho một bảng; hai bảng hoặc n=106n = 10^6 có thể cần int.
  • Bài “đoạn dài nhất / đếm đoạn thỏa điều kiện trên min, max, GCD” thường là: cố định đầu trái, điều kiện đơn điệu theo đầu phải, tìm kiếm nhị phân trên bảng.

Tóm tắt

  • st[k][i] là kết quả trên a[i..i+2k−1]a[i..i + 2^k - 1], dựng bằng hai nửa của mức dưới trong O(nlog⁡n)O(n \log n).
  • Đoạn [l,r][l, r] được phủ bởi hai khối dài 2⌊log⁡2L⌋2^{\lfloor \log_2 L \rfloor} ở hai đầu; đúng với mọi phép gộp kết hợp và lũy đẳng.
  • Tổng không lũy đẳng: dùng tổng tiền tố; bảng không hỗ trợ cập nhật.
  • Hàm trên đoạn đơn điệu theo đầu phải nên bảng thưa ghép tốt với tìm kiếm nhị phân.
  • Hai chiều: bốn hình vuông chồng nhau phủ một hình vuông.

Bài tập

1
Biên Độ Của Đoạncanuc80k2202Hai bảng min và max, mỗi câu in hiệu của chúng.
LQDOJ
2
Tầm Nhìn Qua Dãy Đồicanuc80k2203Đồi giữa là đoạn [min⁡(a,b)+1,max⁡(a,b)−1][\min(a, b) + 1, \max(a, b) - 1]; thành công khi đoạn rỗng hoặc max trên đó không vượt hah_a.
LQDOJ
3
Mô-đun Đồng Dư Lớn Nhấtcanuc80k2204Các số đồng dư theo mm khi mm chia hết mọi hiệu kề; đáp án là GCD của ∣ai+1−ai∣|a_{i+1} - a_i| trên [l,r−1][l, r - 1], đoạn một phần tử in 0.
LQDOJ
4
Biên Phải Của Phép ANDcanuc80k2205AND không tăng khi kéo dài đầu phải; tìm kiếm nhị phân rr xa nhất trên bảng AND, kiểm trước al≥ka_l \ge k.
LQDOJ
5
Giá Trị Nhỏ Nhất Trên Đoạn Nửa Mởcanuc80k2210Đoạn nửa mở đánh số từ 0: đoạn đóng tương ứng là [l,r−1][l, r - 1]; nn tới 5⋅1055 \cdot 10^5 nên cân nhắc bảng int cho đỡ bộ nhớ.
LQDOJ
6
Đỉnh Của Đoạncanuc80k2213Bảng max chuẩn; có tới 2⋅1062 \cdot 10^6 câu hỏi nên đọc ghi nhanh.
LQDOJ
7
Ước Chung Của Hình Chữ Nhậtcanuc80k2218gcd⁡(Ai+Bj)\gcd(A_i + B_j) trên hình chữ nhật bằng GCD của ô góc với GCD các hiệu kề của AA trên dải hàng và của BB trên dải cột.
LQDOJ
8
Đoạn Có Phần Tử Chia Hếtcanuc80k2222Đoạn tốt khi min bằng GCD; có đoạn tốt dài LL thì có đoạn dài L−1L - 1, nên tìm kiếm nhị phân độ dài rồi liệt kê.
LQDOJ
9
Tần Suất Ước Chungcanuc80k2223Cố định đầu trái, GCD đổi không quá 30 lần; nhảy tới chỗ đổi bằng tìm kiếm nhị phân trên bảng GCD, cộng số đầu phải vào map theo giá trị.
LQDOJ
10
Cửa Sổ Theo Hạn Mứccanuc80k2224Một bảng max cho mỗi cột; tìm kiếm nhị phân độ dài và quét mọi cửa sổ, cửa sổ hợp lệ khi tổng mm max không vượt kk.
LQDOJ
11
Điểm Gặp Của Hai Dảicanuc80k2225Cố định ll: max của AA tăng, min của BB giảm theo rr; hai lần tìm kiếm nhị phân cho khúc rr có hai giá trị bằng nhau.
LQDOJ
12
Ba Khối Cùng Mốccanuc80k2226Thử từng xx, khối giữa dài yy: min khối giữa giảm và max khối cuối giảm theo yy, tìm kiếm nhị phân trên bảng min cho khúc yy thỏa.
LQDOJ
13
Chi Phí Chia Khốicanuc80k2233Chỉ cần đoạn dài 1 hoặc đúng cc; dp[i]=min⁡(dp[i−1]+ai, dp[i−c]+tổng−min⁡)\mathrm{dp}[i] = \min(\mathrm{dp}[i-1] + a_i,\ \mathrm{dp}[i-c] + \text{tổng} - \min) với min của cc phần tử cuối lấy từ bảng.
LQDOJ
14
Sức Mạnh Theo Độ Dàicanuc80k2248Mỗi aia_i là min của đoạn dài nhất giữa hai phần tử nhỏ hơn gần nhất; cập nhật đáp án ở độ dài đó rồi lấy max hậu tố theo độ dài.
LQDOJ

Đọc thêm

  • Range Minimum Query (RMQ)VNOI WikiBảng thưa và các cách khác cho RMQ, viết bằng tiếng Việt.
  • Sparse Tablecp-algorithmsCông thức dựng, câu hỏi tổng O(log⁡n)O(\log n) bằng khối rời và câu hỏi min O(1)O(1).
  • ST 表OI WikiĐiều kiện kết hợp và lũy đẳng, có bài luyện Luogu P3865.
  • Static Range QueriesUSACO GuideCác cách trả lời câu hỏi trên dãy tĩnh, gồm bảng thưa và chia để trị.