Chương 2·Bài 22

Sparse Table

Cho một dãy không đổi và hàng trăm nghìn câu hỏi: số nhỏ nhất trong đoạn [l,r][l, r] là bao nhiêu? Tổng tiền tố không giúp được vì phép min không có phép trừ. Duyệt từng đoạn thì mỗi câu tốn O(n)O(n), cả bài cỡ 4⋅10104 \cdot 10^{10} khi n=q=2⋅105n = q = 2 \cdot 10^5.

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 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ỏ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;
#define 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).

Bảng chiếm nhiều bộ nhớ nên cách khai báo cũng đáng chú ý. Vòng ngoài là mức kk, vòng trong là ii, vậy st[k][i] để kk ở chỉ số đầu thì vòng trong đi liền mạch:

Dựng bảng thưa min cho 1 triệu phần tử, trung vị 5 lần chạy, i7-10850H, g++ -O2.st[k][i], int45 msst[i][k], int112 ms · gấp 2,5 lầnst[k][i], long long74 ms · gấp 1,6 lần
Dựng bảng thưa cho 1 triệu phần tử, đo trên i7-10850H, g++ -O2.

Viết st[i][k] chậm hơn khoảng 2,5 lần, vì bảng được dựng từng mức một và st[k][i] giữ cả mức nằm liền nhau; long long chậm hơn khoảng 1,6 lần. Dựng theo mức thì đặt kk ở chỉ số đầu, và dùng int khi giá trị đủ nhỏ.

Phép gộp hợp lệ

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ụ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;
#define 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.

Chặt nhị phân

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 đổ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). Dựng hai bảng thưa, một min một max, cùng mảng lg; 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;
#define 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;
#define 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.

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;
#define 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

Đọc thêm