Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Bài 38: Chia căn (sqrt decomposition) và thuật toán Mo trong C++ và Python: truy vấn đoạn, đếm số giá trị phân biệt

Bài 38: Chia căn (sqrt decomposition) và thuật toán Mo trong C++ và Python: truy vấn đoạn, đếm số giá trị phân biệt
Series: Thuật toán chuyên sâu Độ khó: Khá Bài 38 / Thuật toán chuyên sâu Chia căn Thuật toán Mo
// Thuật toán chuyên sâu · Bài 38
Chia căn và thuật toán Mo: cắt dãy thành √n khối
Không cần cây, không cần đệ quy: chỉ chia dãy thành khoảng √n khối là đã trả lời được những truy vấn đoạn mà cây phân đoạn phải chật vật. Sắp xếp truy vấn thật khéo, hai con trỏ còn đếm được số giá trị khác nhau trên hàng trăm nghìn đoạn.

Lời mở đầu

Ở bài 31 và bài 37 em đã có trong tay cây Fenwick, cây phân đoạn và lazy propagation: mỗi thao tác chỉ tốn $O(\log n)$. Nhưng cây phân đoạn có một điểm yếu: nó chỉ làm được khi đáp án của một đoạn ghép được từ đáp án của hai nửa. Hỏi “trong đoạn $[l, r]$ có bao nhiêu số $\ge x$” hay “có bao nhiêu giá trị khác nhau” thì hai nửa không ghép được đơn giản như thế, và cây phân đoạn bắt đầu trở nên rất cồng kềnh.

Bài này dạy hai kỹ thuật có chung một con số thần kỳ là $\sqrt{n}$. Chia căn (sqrt decomposition) cắt dãy thành khoảng $\sqrt{n}$ khối, mỗi khối giữ sẵn một chút thông tin, nên mỗi truy vấn chỉ phải đụng tới $O(\sqrt{n})$ thứ. Thuật toán Mo thì đọc hết truy vấn rồi sắp xếp lại thứ tự, sau đó trượt cửa sổ $[l, r]$ bằng đúng kỹ thuật hai con trỏ ở bài 17. Cả hai đều chậm hơn $O(\log n)$ một chút, nhưng bù lại code ngắn, dễ nghĩ và dễ sửa cho rất nhiều dạng bài. Để đọc bài này thoải mái, em nên nhớ cách ước lượng số phép tính ở bài 9 và mảng cộng dồn ở bài 13.

Trong bài này bạn sẽ học: ý tưởng chia dãy thành khối kích thước $B \approx \sqrt{n}$; khuôn mẫu ba vòng lặp “lẻ trái, khối trọn, lẻ phải” cho tổng đoạn và cập nhật điểm; nhãn khối để cộng cả đoạn; cách chọn kích thước khối và khi nào chia căn tiện hơn cây phân đoạn (bài đếm số $\ge x$ trong đoạn với các khối đã sắp xếp); thuật toán Mo xử lý truy vấn offline: sắp xếp theo (khối của $l$, $r$), dịch hai con trỏ và đếm số giá trị phân biệt; độ phức tạp $O((n + q)\sqrt{n})$, mẹo sắp $r$ xen kẽ chẵn lẻ, nén số và lời khuyên thật lòng về Python. Có khung chạy tay chia căn và thuật toán Mo, ba mô phỏng trực quan và 8 bài tập kèm gợi ý.

Lộ trình bài viết, bấm vào từng phần để nhảy tới:

Phần 1Ý tưởng chia căn: tổng đoạn và cập nhật điểm

Bắt đầu bằng bài toán quen thuộc nhất. Cho dãy $a_1, a_2, \ldots, a_n$ và $q$ thao tác ($n, q \le 2 \cdot 10^5$), mỗi thao tác là một trong hai loại:

  • 1 i x: gán $a_i = x$;
  • 2 l r: in ra $a_l + a_{l+1} + \cdots + a_r$.

Duyệt trực tiếp thì mỗi câu hỏi tốn $O(n)$; dùng mảng cộng dồn thì mỗi lần gán lại tốn $O(n)$. Bài 31 đã giải bằng cây Fenwick, còn hôm nay ta giải bằng một ý tưởng “thô” hơn nhiều mà vẫn đủ nhanh.

Chia dãy thành các khối. Chọn một số $B$ (kích thước khối) rồi cắt dãy thành các đoạn liên tiếp dài $B$: khối 0 gồm các chỉ số $0..B-1$, khối 1 gồm $B..2B-1$,… (trong code ta đánh số từ 0, khối cuối có thể ngắn hơn). Phần tử có chỉ số $i$ nằm trong khối $i / B$ (chia lấy phần nguyên). Với mỗi khối $b$, ta lưu sẵn $s_b$ = tổng các phần tử của khối.

  • Gán $a_i = x$: chỉ khối $i / B$ bị đổi tổng, và tổng đổi đúng một lượng $x - a_i$. Vậy s[i / B] += x - a[i]; a[i] = x; là xong, $O(1)$.
  • Hỏi tổng $[l, r]$: đoạn $[l, r]$ gồm ba phần: một ít phần tử lẻ ở đầu trái (thuộc khối bị cắt dở), một dãy khối nằm trọn ở giữa, và một ít phần tử lẻ ở đầu phải. Phần tử lẻ thì cộng từng cái, còn khối nằm trọn thì cộng luôn $s_b$, một phép cộng thay cho $B$ phép.
s₀ = 8s₁ = 15s₂ = 13s₃ = 3132134415569728695103a₃ = 4s₁ = 15s₂ = 13a₁₀ = 3tổng a[3..10] = 4 + 15 + 13 + 3 = 354 phép cộng thay vì 8: hai khối giữa được cộng nguyên cả khối

Hình 1. Dãy 10 phần tử, $B = \lfloor\sqrt{10}\rfloor = 3$, được 4 khối (khối 0 tới khối 3, dưới mỗi khối là tổng $s_b$). Truy vấn tổng $[3, 10]$: phần tử 3 là phần lẻ bên trái (vàng), khối 1 và khối 2 nằm trọn (xanh), phần tử 10 là phần lẻ bên phải.

Vì sao lại là $\sqrt{n}$? Mỗi đầu lẻ có không quá $B - 1$ phần tử, số khối nằm trọn không quá $n / B$. Một câu hỏi tốn khoảng $2B + \frac{n}{B}$ phép cộng. Nếu $B$ nhỏ thì có quá nhiều khối, nếu $B$ lớn thì phần lẻ quá dài. Hai vế $B$ và $\frac{n}{B}$ cân bằng khi $B = \sqrt{n}$, lúc đó một câu hỏi tốn $O(\sqrt{n})$. Với $n = 2 \cdot 10^5$: $\sqrt{n} \approx 447$, mỗi câu hỏi chừng $1300$ phép cộng, $2 \cdot 10^5$ câu hỏi tổng cộng khoảng $2{,}7 \cdot 10^8$ phép đơn giản: C++ chạy trong khoảng một giây.

Em hãy bấm Tiếp để xem một phép gán rồi một câu hỏi tổng đi qua các khối thế nào. Thử đổi đoạn hỏi cho nằm gọn trong một khối, hoặc bắt đầu đúng ở đầu khối, để thấy phần lẻ biến mất:

MÔ PHỎNGChia căn: cập nhật một phần tử và hỏi tổng một đoạnNhập dãy (tối đa 16 số), một phép gán “i x” rồi một truy vấn tổng “l r”. Theo dõi khối nào được cộng nguyên, phần tử nào phải cộng lẻ.
cộng lẻ từng phần tửkhối nằm trọn: cộng sphần tử vừa gánngoài đoạn hỏi

Trong code, cả ba phần được viết bằng ba vòng while nối tiếp nhau, dùng chung con trỏ l chạy từ trái sang phải. Vòng đầu cộng lẻ cho tới khi l rơi đúng vào đầu một khối (l % B == 0); vòng giữa nhảy cả khối khi khối đó còn nằm trọn (l + B - 1 <= r); vòng cuối cộng nốt phần còn lại. Đề cho vị trí đếm từ 1 nên ta trừ 1 trước khi gọi hàm.

Xem code C++ / Python: chia căn: gán một phần tử, hỏi tổng đoạn (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n, q, B;
vector<long long> a, s;                         // s[b] = tổng các phần tử của khối b

void update(int i, long long x){                // gán a[i] = x
    s[i / B] += x - a[i];                       // tổng khối đổi đúng một lượng x - a[i]
    a[i] = x;
}

long long query(int l, int r){                  // tổng a[l..r] (chỉ số từ 0)
    long long res = 0;
    while(l <= r && l % B != 0) res += a[l++];          // đầu trái lẻ
    while(l + B - 1 <= r){ res += s[l / B]; l += B; }   // các khối nằm trọn
    while(l <= r) res += a[l++];                        // đầu phải lẻ
    return res;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    B = max(1, (int)sqrt(n));
    a.assign(n, 0); s.assign(n / B + 1, 0);
    for(int i = 0; i < n; i++){ cin >> a[i]; s[i / B] += a[i]; }
    while(q--){
        int t; long long x, y;
        cin >> t >> x >> y;
        if(t == 1) update(x - 1, y);
        else cout << query(x - 1, y - 1) << "\n";
    }
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [int(x) for x in data[2:2 + n]]
    B = max(1, isqrt(n))
    s = [0] * (n // B + 1)                  # s[b] = tổng khối b
    for i in range(n):
        s[i // B] += a[i]
    out, p = [], 2 + n
    for _ in range(q):
        t, x, y = int(data[p]), int(data[p + 1]), int(data[p + 2]); p += 3
        if t == 1:                          # gán a[x] = y
            i = x - 1
            s[i // B] += y - a[i]
            a[i] = y
        else:                               # tổng a[x..y]
            l, r = x - 1, y - 1
            bl, br = l // B, r // B
            if bl == br:
                out.append(sum(a[l:r + 1]))
            else:                           # lẻ trái + các khối trọn + lẻ phải
                out.append(sum(a[l:(bl + 1) * B]) + sum(s[bl + 1:br]) + sum(a[br * B:r + 1]))
    print('\n'.join(map(str, out)))

main()

Khung chạy tay dưới đây làm đúng ví dụ ở Hình 1. Em để ý ô s nào sáng xanh lá: đó là các khối được cộng nguyên, và chú ý sau phép gán ta hỏi lại đúng đoạn cũ, đáp án giảm đúng $5 - (-2) = 7$.

C++▶ Chạy tay từng dòng codeChia căn: s[b] giữ tổng khối b. Hỏi tổng = phần lẻ trái + các khối nằm trọn + phần lẻ phải; gán chỉ sửa một ô s.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int n, B, a[100005];
5long long s[100005];
6
7long long query(int l, int r){
8 long long res = 0;
9 while(l <= r && l % B != 0) res += a[l++];
10 while(l + B - 1 <= r){ res += s[l / B]; l += B; }
11 while(l <= r) res += a[l++];
12 return res;
13}
14
15int main(){
16 int q;
17 cin >> n >> q;
18 B = max(1, (int)sqrt(n));
19 for(int i = 0; i < n; i++){
20 cin >> a[i];
21 s[i / B] += a[i];
22 }
23 while(q--){
24 int t, x, y;
25 cin >> t >> x >> y;
26 if(t == 1){
27 x--;
28 s[x / B] += y - a[x];
29 a[x] = y;
30 }
31 else cout << query(x - 1, y - 1) << "\n";
32 }
33 return 0;
34}
Khuôn mẫu chia căn: ba vòng lặp
Mọi thao tác trên đoạn [l, r] (chỉ số từ 0) đều đi theo cùng một dáng:
while(l <= r && l % B != 0){ /* xử lý lẻ phần tử l */ l++; }
while(l + B - 1 <= r){ /* xử lý nguyên khối l / B */ l += B; }
while(l <= r){ /* xử lý lẻ phần tử l */ l++; }
Đổi bài toán chỉ là đổi phần “xử lý lẻ” và “xử lý nguyên khối”: tổng, min, max, đếm, đảo bit… Phần còn lại giữ nguyên.
Ba lỗi hay gặp
1. $n$ nhỏ (chẳng hạn $n = 1, 2, 3$) thì (int)sqrt(n) có thể bằng 1, còn nếu em lỡ viết sqrt(n) / 2 thì $B = 0$ và phép chia i / B làm chương trình chết ngay: luôn viết B = max(1, …).
2. Mảng tổng khối phải có ít nhất $\lceil n / B \rceil$ ô; khai báo n / B + 1 ô là an toàn.
3. Tổng có thể vượt int (ví dụ $2 \cdot 10^5$ số cỡ $10^9$), nên s[] và biến kết quả dùng long long.
Với Python
Bản Python ở trên không viết ba vòng while mà dùng sum(a[l:…]) và sum(s[…]): hàm sum trên lát cắt chạy bằng C bên trong nên nhanh hơn vòng lặp Python nhiều lần. Đây là mẹo chung cho mọi bài chia căn bằng Python: đẩy càng nhiều việc vào hàm có sẵn (sum, min, max, bisect) càng tốt.

Phần 2Cộng cả đoạn: nhãn khối

Nâng cấp bài toán: thao tác 1 l r x giờ là cộng $x$ vào mọi phần tử $a_l, \ldots, a_r$, còn 2 l r vẫn hỏi tổng. Nếu cộng từng phần tử thì một thao tác tốn $O(n)$. Ở bài 37 cây phân đoạn giải bằng nhãn lazy; chia căn có một phiên bản đơn giản hơn nhiều: nhãn khối.

Mỗi khối $b$ giữ thêm một số lz[b]: “mọi phần tử của khối này còn được cộng thêm lz[b] mà chưa ghi vào từng a[i]”. Như vậy giá trị thật của phần tử $i$ là a[i] + lz[i / B], còn s[b] luôn là tổng thật của khối (đã tính cả nhãn).

  • Cộng đoạn: phần tử lẻ thì a[i] += x và s[i / B] += x; khối nằm trọn thì chỉ lz[b] += x và s[b] += x * B. Không đụng vào từng phần tử của khối.
  • Hỏi tổng: phần tử lẻ cộng a[i] + lz[i / B]; khối nằm trọn cộng s[b].

Cả hai thao tác vẫn đúng khuôn ba vòng lặp, nên vẫn $O(\sqrt{n})$. Ví dụ với $a = (1, 2, \ldots, 8)$, $B = 2$, thao tác “cộng 10 vào đoạn $[2, 7]$”:

KhốiVị tría[i] lưu trong mảnglzs (tổng thật)Đã làm gì
01, 21, 12013vị trí 2 là phần lẻ trái: cộng thẳng vào a
13, 43, 41027nằm trọn: chỉ dán nhãn, s += 10 · 2
25, 65, 61031nằm trọn: chỉ dán nhãn, s += 10 · 2
37, 817, 8025vị trí 7 là phần lẻ phải: cộng thẳng vào a

Bảng 1. Sau khi cộng 10 vào đoạn $[2, 7]$. Mảng a ở khối 1 và 2 vẫn là 3, 4, 5, 6, nhưng giá trị thật là 13, 14, 15, 16 nhờ nhãn lz = 10. Hỏi tổng $[3, 4]$ chỉ cần đọc s = 27; hỏi tổng $[4, 5]$ thì cộng $(4 + 10) + (5 + 10) = 29$.

Xem code C++ / Python: chia căn: cộng đoạn, hỏi tổng đoạn bằng nhãn khối (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n, q, B;
vector<long long> a, s, lz;   // lz[b]: lượng đã cộng cho CẢ khối b, chưa cộng vào từng a[i]

void add(int l, int r, long long x){
    while(l <= r && l % B != 0){ a[l] += x; s[l / B] += x; l++; }
    while(l + B - 1 <= r){ lz[l / B] += x; s[l / B] += x * B; l += B; }   // dán nhãn cả khối
    while(l <= r){ a[l] += x; s[l / B] += x; l++; }
}

long long query(int l, int r){
    long long res = 0;
    while(l <= r && l % B != 0){ res += a[l] + lz[l / B]; l++; }   // giá trị thật = a[i] + nhãn khối
    while(l + B - 1 <= r){ res += s[l / B]; l += B; }
    while(l <= r){ res += a[l] + lz[l / B]; l++; }
    return res;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    B = max(1, (int)sqrt(n));
    a.assign(n, 0); s.assign(n / B + 1, 0); lz.assign(n / B + 1, 0);
    for(int i = 0; i < n; i++){ cin >> a[i]; s[i / B] += a[i]; }
    while(q--){
        int t, l, r;
        cin >> t >> l >> r;
        if(t == 1){ long long x; cin >> x; add(l - 1, r - 1, x); }
        else cout << query(l - 1, r - 1) << "\n";
    }
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [int(x) for x in data[2:2 + n]]
    B = max(1, isqrt(n))
    nb = n // B + 1
    s, lz = [0] * nb, [0] * nb
    for i in range(n):
        s[i // B] += a[i]
    out, p = [], 2 + n
    for _ in range(q):
        t, l, r = int(data[p]), int(data[p + 1]) - 1, int(data[p + 2]) - 1; p += 3
        if t == 1:
            x = int(data[p]); p += 1
            bl, br = l // B, r // B
            if bl == br:
                for i in range(l, r + 1): a[i] += x
                s[bl] += x * (r - l + 1)
            else:
                for i in range(l, (bl + 1) * B): a[i] += x
                s[bl] += x * ((bl + 1) * B - l)
                for b in range(bl + 1, br):          # dán nhãn cả khối
                    lz[b] += x; s[b] += x * B
                for i in range(br * B, r + 1): a[i] += x
                s[br] += x * (r - br * B + 1)
        else:
            bl, br = l // B, r // B
            if bl == br:
                res = sum(a[l:r + 1]) + lz[bl] * (r - l + 1)
            else:
                res = sum(a[l:(bl + 1) * B]) + lz[bl] * ((bl + 1) * B - l)
                res += sum(s[bl + 1:br])
                res += sum(a[br * B:r + 1]) + lz[br] * (r - br * B + 1)
            out.append(res)
    print('\n'.join(map(str, out)))

main()
Nhãn khối khác lazy của cây phân đoạn ở chỗ nào?
Với phép cộng, nhãn khối không bao giờ phải “đẩy xuống”: phần tử lẻ cứ cộng thẳng vào a[i], giá trị thật vẫn là a[i] + lz. Nhưng nếu thao tác là gán cả đoạn (a[i] = x) thì khác: trước khi sửa lẻ một phần tử của khối đang có nhãn gán, em phải ghi nhãn xuống mọi phần tử của khối đó rồi xoá nhãn (tốn $O(B)$, vẫn ổn). Quên bước này là lỗi kinh điển của bài gán đoạn.

Một biến thể còn dễ hơn: cộng đoạn, hỏi một điểm. Lúc đó chẳng cần s[], giá trị của điểm $i$ chính là a[i] + lz[i / B], mỗi câu hỏi $O(1)$. Em sẽ tự cài ở bài tập 2.

Phần 3Chọn kích thước khối, so sánh với cây phân đoạn

Ở phần 1 ta chọn $B = \sqrt{n}$ vì hai vế chi phí $B$ và $\frac{n}{B}$ cân bằng nhau. Nhưng không phải bài nào hai vế cũng “nặng” như nhau. Quy tắc chung: viết chi phí của một thao tác theo $B$, rồi chọn $B$ để hai vế lớn nhất bằng nhau. Xét bài toán sau, một bài mà cây phân đoạn bình thường không làm được:

Bài toán. Cho dãy $n$ số và $q$ thao tác: 1 i x gán $a_i = x$; 2 l r x đếm xem trong $a_l, \ldots, a_r$ có bao nhiêu số $\ge x$.

Cây phân đoạn bế tắc vì biết “nửa trái có 3 số $\ge 5$, nửa phải có 2 số $\ge 5$” chẳng giúp gì khi câu hỏi sau hỏi $x = 7$. Chia căn thì có cách rất tự nhiên: mỗi khối giữ thêm một bản sao đã sắp xếp tăng dần. Với khối nằm trọn, số phần tử $\ge x$ tìm được bằng tìm kiếm nhị phân (lower_bound) trong $O(\log B)$.

  • Hỏi: phần lẻ so sánh từng số, $O(B)$; mỗi khối trọn một lần tìm kiếm nhị phân, tổng $O(\frac{n}{B} \log B)$.
  • Gán: tìm vị trí giá trị cũ trong bản sao của khối, thay bằng giá trị mới rồi đẩy nó trượt về đúng chỗ (giống một lượt sắp xếp chèn), $O(B)$.

Chi phí câu hỏi là $B + \frac{n}{B}\log B$: cân bằng khi $B \approx \sqrt{n \log n}$. Thực tế với $n = 10^5$ em lấy $B$ trong khoảng 300–1000 đều chạy tốt; nếu thao tác gán nhiều hơn câu hỏi thì nghiêng về $B$ nhỏ, câu hỏi nhiều hơn thì nghiêng về $B$ lớn.

Xem code C++ / Python: chia căn: đếm số ≥ x trong đoạn, có gán phần tử (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n, q, B;
vector<int> a;
vector<vector<int>> blk;          // blk[b]: các phần tử của khối b, luôn giữ tăng dần

int query(int l, int r, int x){   // đếm số a[i] >= x với l <= i <= r
    int res = 0;
    while(l <= r && l % B != 0) res += (a[l++] >= x);
    while(l + B - 1 <= r){
        vector<int> &v = blk[l / B];
        res += v.end() - lower_bound(v.begin(), v.end(), x);   // tìm kiếm nhị phân trong khối
        l += B;
    }
    while(l <= r) res += (a[l++] >= x);
    return res;
}

void update(int i, int x){        // gán a[i] = x, giữ khối vẫn tăng dần
    vector<int> &v = blk[i / B];
    int p = lower_bound(v.begin(), v.end(), a[i]) - v.begin();   // chỗ của giá trị cũ
    v[p] = x;
    while(p > 0 && v[p - 1] > v[p]){ swap(v[p - 1], v[p]); p--; }
    while(p + 1 < (int)v.size() && v[p + 1] < v[p]){ swap(v[p + 1], v[p]); p++; }
    a[i] = x;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    B = max(1, (int)sqrt(n));
    a.resize(n); blk.assign(n / B + 1, {});
    for(int i = 0; i < n; i++){ cin >> a[i]; blk[i / B].push_back(a[i]); }
    for(auto &v : blk) sort(v.begin(), v.end());
    while(q--){
        int t; cin >> t;
        if(t == 1){ int i, x; cin >> i >> x; update(i - 1, x); }
        else { int l, r, x; cin >> l >> r >> x; cout << query(l - 1, r - 1, x) << "\n"; }
    }
    return 0;
}
import sys
from math import isqrt
from bisect import bisect_left, insort

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [int(x) for x in data[2:2 + n]]
    B = max(1, isqrt(n))
    blk = [sorted(a[i:i + B]) for i in range(0, n, B)]     # mỗi khối đã sắp tăng
    out, p = [], 2 + n
    for _ in range(q):
        if data[p] == b'1':
            i, x = int(data[p + 1]) - 1, int(data[p + 2]); p += 3
            v = blk[i // B]
            v.pop(bisect_left(v, a[i]))                   # bỏ giá trị cũ
            insort(v, x)                                  # chèn giá trị mới
            a[i] = x
        else:
            l, r, x = int(data[p + 1]) - 1, int(data[p + 2]) - 1, int(data[p + 3]); p += 4
            bl, br = l // B, r // B
            if bl == br:
                res = sum(1 for i in range(l, r + 1) if a[i] >= x)
            else:
                res = sum(1 for i in range(l, (bl + 1) * B) if a[i] >= x)
                for b in range(bl + 1, br):
                    v = blk[b]
                    res += len(v) - bisect_left(v, x)
                res += sum(1 for i in range(br * B, r + 1) if a[i] >= x)
            out.append(res)
    print('\n'.join(map(str, out)))

main()

Vậy khi nào nên chọn chia căn, khi nào chọn cây? Bảng dưới đây tóm tắt kinh nghiệm thi cử:

Tiêu chíChia cănCây phân đoạn / Fenwick
Một thao tác$O(\sqrt{n})$, đôi khi $O(\sqrt{n \log n})$$O(\log n)$
$n = q = 2 \cdot 10^5$≈ $10^8$ phép, C++ khoảng 0,3–1 giây≈ $7 \cdot 10^6$ phép, rất nhanh
Điều kiệnchỉ cần xử lý được “nguyên khối” và “từng phần tử”đáp án đoạn phải ghép được từ hai nửa
Độ dài code, dễ sửangắn, ba vòng lặp, rất dễ đổi bàidài hơn, lazy dễ sai
Hợp vớiđếm số $\ge x$, đảo đoạn, bước nhảy, thông tin “không ghép được”tổng, min, max, gcd, cộng/gán đoạn

Bảng 2. Chia căn và cây phân đoạn. Nếu bài toán ghép được hai nửa thì cây phân đoạn thường là lựa chọn an toàn hơn về thời gian; chia căn toả sáng khi thông tin của đoạn khó ghép hoặc khi cần viết nhanh, ít lỗi.

Chia căn không chỉ chia theo vị trí
Ý tưởng “cái gì nhỏ thì làm kiểu này, cái gì lớn thì làm kiểu kia, ngưỡng là $\sqrt{n}$” xuất hiện ở rất nhiều nơi. Ví dụ bài tập 4: hỏi tổng $a_x + a_{x+k} + a_{x+2k} + \cdots$. Nếu $k > \sqrt{n}$ thì tổng có ít hơn $\sqrt{n}$ số hạng, cộng thẳng; nếu $k \le \sqrt{n}$ thì chỉ có $\sqrt{n}$ giá trị $k$ khác nhau, tính trước mảng tổng cho từng $k$. Người ta gọi chung là chia căn theo giá trị hay “kỹ thuật ngưỡng”.

Phần 4Thuật toán Mo: sắp xếp truy vấn, dịch hai con trỏ

Bài toán. Cho dãy $n$ số ($|a_i| \le 10^9$) và $q$ câu hỏi, mỗi câu hỏi l r: đoạn $a_l, \ldots, a_r$ có bao nhiêu giá trị khác nhau? Không có thao tác sửa dãy. ($n, q \le 2 \cdot 10^5$.)

Nếu chỉ có một đoạn thì quá dễ: dùng mảng đếm cnt[v] và biến distinct. Hơn nữa, nếu ta đang có đáp án của đoạn $[l, r]$ thì sang các đoạn “hàng xóm” chỉ tốn $O(1)$:

  • add(i) (đưa $a_i$ vào cửa sổ): cnt[a[i]]++, nếu nó vừa từ 0 lên 1 thì distinct++;
  • del(i) (bỏ $a_i$ ra khỏi cửa sổ): cnt[a[i]]--, nếu nó vừa về 0 thì distinct--.

Đây chính là cửa sổ trượt của bài 17. Nếu các câu hỏi có $l$ tăng dần và $r$ tăng dần thì hai con trỏ đi một mạch từ trái sang phải, tổng chỉ $O(n + q)$. Rắc rối là câu hỏi đến theo thứ tự lung tung: đoạn sau có thể ở tít đầu bên kia, mỗi lần chuyển mất $O(n)$, tổng $O(nq)$ quá chậm.

Ý tưởng của Mo: đề không bắt trả lời ngay từng câu, nên ta được phép đọc hết câu hỏi rồi đổi thứ tự xử lý (gọi là xử lý offline), miễn cuối cùng in đáp án đúng thứ tự đề cho. Thứ tự Mo chọn:

  1. chia trục vị trí thành các khối dài $B \approx \sqrt{n}$;
  2. sắp xếp câu hỏi theo khối chứa $l$ tăng dần; cùng khối thì theo $r$ tăng dần;
  3. giữ cửa sổ $[l, r]$ hiện tại (ban đầu rỗng: $l = 0, r = -1$), với mỗi câu hỏi theo thứ tự mới thì dịch hai đầu cửa sổ từng bước một bằng add/del cho tới khi khớp, rồi ghi đáp án vào ans[id].

Trong cùng một khối của $l$, đầu phải $r$ chỉ đi tới, còn đầu trái $l$ chỉ lắc qua lắc lại trong phạm vi một khối, tức không quá $B$ bước mỗi câu. Phần 5 sẽ đếm cụ thể. Trước hết em xem Mo chạy thật trên một ví dụ nhỏ, chú ý bảng cnt thay đổi ra sao ở mỗi bước:

MÔ PHỎNGThuật toán Mo: dịch hai con trỏ l, r và cập nhật bảng đếmNhập dãy (tối đa 12 chữ số 1–9) và tối đa 6 truy vấn “l r” cách nhau bởi dấu phẩy. Mỗi truy vấn hỏi: đoạn [l, r] có bao nhiêu giá trị khác nhau?
trong cửa sổvừa thêmvừa bỏgiá trị đang có mặtngoài cửa sổ

Code dưới đây đúng y như mô phỏng, thêm hai chi tiết sẽ giải thích ở phần 5: nén số (vì $a_i$ tới $10^9$, không lập mảng cnt cỡ đó được) và mẹo chẵn lẻ khi sắp xếp. Bản Python viết thẳng bốn vòng lặp trong thân hàm thay vì gọi add/del, vì lời gọi hàm trong Python khá đắt.

Xem code C++ / Python: thuật toán Mo: đếm số giá trị phân biệt trong đoạn (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n, q, B, distinctCnt = 0;
vector<int> a, cnt;

void add(int i){ if(cnt[a[i]]++ == 0) distinctCnt++; }     // a[i] vào cửa sổ
void del(int i){ if(--cnt[a[i]] == 0) distinctCnt--; }     // a[i] ra khỏi cửa sổ

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    a.resize(n);
    for(auto &x : a) cin >> x;
    vector<int> v = a;                                       // nén số: giá trị -> 0..k-1
    sort(v.begin(), v.end());
    v.erase(unique(v.begin(), v.end()), v.end());
    for(auto &x : a) x = lower_bound(v.begin(), v.end(), x) - v.begin();
    cnt.assign(v.size(), 0);

    B = max(1, (int)sqrt(n));
    vector<int> L(q), R(q), id(q), ans(q);
    for(int k = 0; k < q; k++){ cin >> L[k] >> R[k]; L[k]--; R[k]--; id[k] = k; }
    sort(id.begin(), id.end(), [&](int x, int y){
        int bx = L[x] / B, by = L[y] / B;
        if(bx != by) return bx < by;
        return (bx & 1) ? R[x] > R[y] : R[x] < R[y];        // mẹo chẵn lẻ
    });
    int l = 0, r = -1;                                       // cửa sổ rỗng
    for(int t : id){
        while(r < R[t]) add(++r);                            // mở rộng trước
        while(l > L[t]) add(--l);
        while(r > R[t]) del(r--);                            // thu hẹp sau
        while(l < L[t]) del(l++);
        ans[t] = distinctCnt;
    }
    for(int k = 0; k < q; k++) cout << ans[k] << "\n";
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    comp = {}
    a = [comp.setdefault(int(x), len(comp)) for x in data[2:2 + n]]   # nén số bằng dict
    L = [int(x) - 1 for x in data[2 + n:2 + n + 2 * q:2]]
    R = [int(x) - 1 for x in data[3 + n:3 + n + 2 * q:2]]
    B = max(1, isqrt(n))
    def key(k):                                   # (khối của l, r) với mẹo chẵn lẻ
        b = L[k] // B
        return (b, R[k] if b % 2 == 0 else -R[k])
    order = sorted(range(q), key=key)
    cnt = [0] * len(comp)
    ans = [0] * q
    l, r, d = 0, -1, 0                            # d = số giá trị phân biệt
    for k in order:                               # viết thẳng vòng lặp, không gọi hàm: nhanh hơn
        ql, qr = L[k], R[k]
        while r < qr:
            r += 1; v = a[r]
            if cnt[v] == 0: d += 1
            cnt[v] += 1
        while l > ql:
            l -= 1; v = a[l]
            if cnt[v] == 0: d += 1
            cnt[v] += 1
        while r > qr:
            v = a[r]; cnt[v] -= 1
            if cnt[v] == 0: d -= 1
            r -= 1
        while l < ql:
            v = a[l]; cnt[v] -= 1
            if cnt[v] == 0: d -= 1
            l += 1
        ans[k] = d
    sys.stdout.write('\n'.join(map(str, ans)) + '\n')

main()

Khung chạy tay theo dõi từng dòng: mỗi lần con trỏ dịch, code nhảy vào add hoặc del. Bản này để dễ nhìn nên chưa dùng nén số và mẹo chẵn lẻ, giá trị chỉ từ 1 đến 9.

C++▶ Chạy tay từng dòng codeThuật toán Mo: sắp truy vấn theo (khối của L, R), dịch hai con trỏ l, r; add/del cập nhật cnt và số giá trị phân biệt.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int n, q, B, a[200005], cnt[1000005], L[200005], R[200005], id[200005], ans[200005];
5int distinctCnt = 0;
6
7void add(int i){ if(cnt[a[i]]++ == 0) distinctCnt++; }
8void del(int i){ if(--cnt[a[i]] == 0) distinctCnt--; }
9
10int main(){
11 cin >> n >> q;
12 for(int i = 0; i < n; i++) cin >> a[i];
13 B = max(1, (int)sqrt(n));
14 for(int k = 0; k < q; k++){ cin >> L[k] >> R[k]; L[k]--; R[k]--; id[k] = k; }
15 sort(id, id + q, [](int x, int y){
16 if(L[x] / B != L[y] / B) return L[x] / B < L[y] / B;
17 return R[x] < R[y];
18 });
19 int l = 0, r = -1;
20 for(int k = 0; k < q; k++){
21 int t = id[k];
22 while(r < R[t]) add(++r);
23 while(l > L[t]) add(--l);
24 while(r > R[t]) del(r--);
25 while(l < L[t]) del(l++);
26 ans[t] = distinctCnt;
27 }
28 for(int k = 0; k < q; k++) cout << ans[k] << "\n";
29 return 0;
30}
Khuôn mẫu thuật toán Mo
sort(id.begin(), id.end(), [&](int x, int y){
    if(L[x] / B != L[y] / B) return L[x] / B < L[y] / B;
    return R[x] < R[y];
});
int l = 0, r = -1;                 // cửa sổ rỗng
for(int t : id){
    while(r < R[t]) add(++r);       // mở rộng trước
    while(l > L[t]) add(--l);
    while(r > R[t]) del(r--);       // thu hẹp sau
    while(l < L[t]) del(l++);
    ans[t] = /* đáp án của cửa sổ */;
}
Đổi bài chỉ là đổi add, del và biến đáp án. Câu hỏi để tự kiểm tra: “thêm hoặc bớt một phần tử ở mép cửa sổ, em có cập nhật đáp án trong $O(1)$ được không?” Được thì gần như chắc chắn dùng được Mo.
Thứ tự bốn vòng while
Hãy mở rộng trước, thu hẹp sau. Nếu thu hẹp trước, có lúc $l$ vượt quá $r + 1$ (cửa sổ “âm”), del sẽ bớt những phần tử chưa từng được thêm, cnt bị âm. Với bài đếm phân biệt có khi vẫn may mắn ra đúng, nhưng với bài như tổng $cnt^2$ hay đếm cặp thì sai ngay. Ngoài ra, Mo cơ bản chỉ dùng cho bài không có thao tác sửa dãy và cho phép trả lời offline.

Phần 5Độ phức tạp của Mo và các mẹo tối ưu

Đếm số bước dịch con trỏ. Gọi $B$ là kích thước khối, có $\frac{n}{B}$ khối.

  • Con trỏ phải $r$: trong một khối của $l$, $r$ chỉ tăng nên đi tối đa $n$ bước; sang khối mới nó có thể lùi về, thêm tối đa $n$ bước nữa. Tổng: $O(\frac{n}{B} \cdot n)$.
  • Con trỏ trái $l$: giữa hai câu hỏi cùng khối, $l$ dịch không quá $B$ bước; khi đổi khối thì dịch không quá $2B$. Tổng: $O(qB)$ (cộng thêm $O(n)$ cho các lần đổi khối).

Tổng cộng $O\left(\frac{n^2}{B} + qB\right)$. Chọn $B = \sqrt{n}$ được $O((n + q)\sqrt{n})$. Nếu $q$ khác $n$ nhiều, chọn $B = \frac{n}{\sqrt{q}}$ thì hai vế cân bằng, được $O(n\sqrt{q})$. Với $n = q = 2 \cdot 10^5$: khoảng $2 \cdot 2 \cdot 10^5 \cdot 447 \approx 1{,}8 \cdot 10^8$ lần gọi add/del, mỗi lần chỉ vài phép tính: C++ chạy chừng 0,5–1 giây.

khối 0khối 1khối 2khối 3khối 4lr11551010151520202525123456789Mỗi truy vấn là một điểm (l, r).Số trong chấm tròn: thứ tự Mo.Trong một dải khối:• r chỉ tăng → con trỏ phải đi tối đa n bước cho cả dải;• l chỉ nhảy trong dải rộng B.Sang dải mới, r có thể phảilùi về: đó là các đoạnđường dốc xuống.

Hình 2. Chín câu hỏi với $n = 25$, $B = 5$, vẽ thành các điểm $(l, r)$. Đường nét đứt là đường đi của “cửa sổ” theo thứ tự Mo: đi lên trong từng dải, mỗi lần sang dải mới thì tụt xuống.

Mô phỏng dưới đây dùng đúng dữ liệu của Hình 2. Em bấm Tiếp để thấy chi phí từng lần chuyển, rồi so tổng số bước của thứ tự đề cho, thứ tự Mo và Mo chẵn lẻ. Thử tự nhập các câu hỏi “đầu này, đầu kia” xen kẽ để thấy thứ tự đề cho tệ cỡ nào:

MÔ PHỎNGVì sao phải sắp xếp truy vấn: đếm số bước dịch con trỏNhập n (4–30) và từ 2 đến 10 truy vấn “l r”. So sánh tổng số bước của thứ tự đề cho, thứ tự Mo và Mo chẵn lẻ.
truy vấn đang xử lýđã xử lýchưa tới

Mẹo 1: sắp $r$ xen kẽ chẵn lẻ. Trong thứ tự Mo cơ bản, cuối mỗi khối $r$ đang ở rất xa bên phải, sang khối sau lại lùi về gần $l$ rồi chạy lên lại. Nếu ở khối chẵn ta sắp $r$ tăng, khối lẻ sắp $r$ giảm, thì $r$ đi lên ở khối này và quét xuống luôn ở khối sau, đỡ được gần một nửa quãng đường của $r$. Chỉ cần sửa một dòng trong hàm so sánh: return (bx & 1) ? R[x] > R[y] : R[x] < R[y];. Trên dữ liệu lớn, mẹo này thường giúp nhanh hơn 20–40%.

Mẹo 2: nén số. Mảng cnt đánh chỉ số theo giá trị, mà $a_i$ tới $10^9$ (hoặc âm) thì không thể lập mảng cỡ đó. Nhưng bài đếm phân biệt chỉ quan tâm hai số bằng nhau hay không, nên ta đổi mỗi giá trị về thứ hạng của nó trong danh sách các giá trị khác nhau: sắp xếp bản sao, xoá trùng bằng unique, rồi dùng lower_bound để tra thứ hạng (cách nén giá trị em đã gặp ở bài 31). Trong Python còn gọn hơn: comp.setdefault(x, len(comp)) gán số hiệu theo thứ tự xuất hiện, không cần sắp xếp. Đừng dùng map/dict thay cho mảng cnt bên trong add/del: nó được gọi hàng trăm triệu lần, chậm thêm một hằng số là quá giờ ngay.

Kỹ thuậtChuẩn bịMỗi thao tácTổngOffline?
Duyệt trực tiếp–$O(n)$$O(nq)$không
Chia căn, tổng đoạn$O(n)$$O(\sqrt{n})$$O(n + q\sqrt{n})$không
Chia căn, khối sắp xếp$O(n \log n)$$O(\sqrt{n} \log n)$$O(q\sqrt{n}\log n)$không
Thuật toán Mo$O(q \log q)$ sắp xếptrung bình $O(\frac{n}{\sqrt{q}})$ bước$O((n + q)\sqrt{n})$có, không sửa dãy
Cây phân đoạn$O(n)$$O(\log n)$$O(n + q\log n)$không

Bảng 3. Độ phức tạp các cách xử lý truy vấn đoạn đã học. Chia căn và Mo chậm hơn cây phân đoạn, nhưng giải được những bài mà đáp án không ghép được từ hai nửa.

Nói thật về Python
Thuật toán Mo trong Python rất chậm: Python chỉ làm được khoảng $10^7$ thao tác đơn giản mỗi giây, nên $1{,}8 \cdot 10^8$ bước dịch con trỏ sẽ mất cả chục giây. Bản Python ở trên đã tối ưu những gì có thể (đọc nhanh bằng sys.stdin.buffer, nén số bằng dict, viết vòng lặp trực tiếp, mẹo chẵn lẻ) nhưng vẫn chỉ an toàn khi $n, q$ cỡ $2 \cdot 10^4$–$3 \cdot 10^4$. Với giới hạn $10^5$ trở lên, thầy khuyên dùng C++ cho các bài Mo. Riêng các bài chia căn ở phần 1–3, Python vẫn ổn nếu em đẩy việc vào sum, min, bisect như đã làm.
Dấu hiệu nhận ra bài dùng Mo
1. Nhiều câu hỏi trên đoạn, không có thao tác sửa (hoặc cho phép đọc hết câu hỏi trước).
2. Đáp án của đoạn không ghép được từ hai nửa: số giá trị phân biệt, số giá trị xuất hiện đúng $k$ lần, tổng $cnt^2$, số cặp bằng nhau, số đoạn con có XOR bằng $k$…
3. Thêm hoặc bớt một phần tử ở mép cửa sổ thì cập nhật đáp án được trong $O(1)$.
4. $n, q \le 10^5$ đến $2 \cdot 10^5$, giới hạn thời gian 1–3 giây.
Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Truy vấn đoạn.

Bài tậpLuyện chia căn và thuật toán Mo

Bài 1Min đoạn có gán phần tửDễ

Cho dãy $n$ số nguyên ($n, q \le 10^5$, $|a_i| \le 10^9$) và $q$ thao tác: 1 i x gán $a_i = x$; 2 l r in ra số nhỏ nhất trong $a_l, \ldots, a_r$. Hãy giải bằng chia căn.

Input
8 4
4 7 2 9 5 1 8 6
2 2 6
1 6 10
2 2 6
2 6 8
Output
1
2
6

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Mỗi khối giữ mn[b] là min của khối. Khác với tổng, khi gán một phần tử ta không sửa min theo kiểu “cộng thêm chênh lệch” được (nếu phần tử nhỏ nhất bị tăng lên thì min mới là ai?), nên cứ tính lại min của cả khối trong $O(B)$, vẫn ổn. Câu hỏi đi đúng khuôn ba vòng lặp. Ví dụ: đoạn $[2, 6]$ là 7 2 9 5 1, min = 1; sau khi gán $a_6 = 10$ thì min = 2; đoạn $[6, 8]$ là 10 8 6, min = 6.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, q, B;
vector<long long> a, mn;              // mn[b] = min của khối b

void rebuild(int b){                  // tính lại min của khối b: O(B)
    mn[b] = LLONG_MAX;
    for(int i = b * B; i < min(n, (b + 1) * B); i++) mn[b] = min(mn[b], a[i]);
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    B = max(1, (int)sqrt(n));
    a.resize(n); mn.assign(n / B + 1, LLONG_MAX);
    for(int i = 0; i < n; i++){ cin >> a[i]; mn[i / B] = min(mn[i / B], a[i]); }
    while(q--){
        int t; long long x, y;
        cin >> t >> x >> y;
        if(t == 1){ a[x - 1] = y; rebuild((x - 1) / B); }
        else {
            int l = x - 1, r = y - 1;
            long long res = LLONG_MAX;
            while(l <= r && l % B != 0) res = min(res, a[l++]);
            while(l + B - 1 <= r){ res = min(res, mn[l / B]); l += B; }
            while(l <= r) res = min(res, a[l++]);
            cout << res << "\n";
        }
    }
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [int(x) for x in data[2:2 + n]]
    B = max(1, isqrt(n))
    mn = [min(a[i:i + B]) for i in range(0, n, B)]      # min của từng khối
    out, p = [], 2 + n
    for _ in range(q):
        t, x, y = int(data[p]), int(data[p + 1]), int(data[p + 2]); p += 3
        if t == 1:
            i = x - 1
            a[i] = y
            b = i // B
            mn[b] = min(a[b * B:(b + 1) * B])           # tính lại khối: O(B)
        else:
            l, r = x - 1, y - 1
            bl, br = l // B, r // B
            if bl == br:
                out.append(min(a[l:r + 1]))
            else:
                res = min(min(a[l:(bl + 1) * B]), min(a[br * B:r + 1]))
                if bl + 1 < br:
                    res = min(res, min(mn[bl + 1:br]))
                out.append(res)
    print('\n'.join(map(str, out)))

main()
Bài 2Cộng đoạn, hỏi một điểmDễ

Cho dãy $n$ số nguyên ($n, q \le 2 \cdot 10^5$, $|a_i|, |x| \le 10^9$) và $q$ thao tác: 1 l r x cộng $x$ vào mọi phần tử $a_l, \ldots, a_r$; 2 i in ra giá trị hiện tại của $a_i$.

Input
6 5
1 2 3 4 5 6
1 2 5 10
2 3
1 1 6 -1
2 6
2 1
Output
13
5
0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Nhãn khối của phần 2, nhưng không cần mảng tổng: phần tử lẻ cộng thẳng vào a[i], khối nằm trọn chỉ lz[b] += x. Giá trị thật của $a_i$ là a[i] + lz[i / B], trả lời trong $O(1)$. Giá trị có thể tới $2 \cdot 10^5 \cdot 10^9$ nên dùng long long. Ví dụ: $a_3 = 3 + 10 = 13$, $a_6 = 6 - 1 = 5$, $a_1 = 1 - 1 = 0$. (Bài này cũng giải được bằng mảng hiệu kết hợp Fenwick như bài 31; ở đây ta luyện chia căn.)
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, q;
    cin >> n >> q;
    int B = max(1, (int)sqrt(n));
    vector<long long> a(n), lz(n / B + 1, 0);
    for(auto &x : a) cin >> x;
    while(q--){
        int t; cin >> t;
        if(t == 1){
            int l, r; long long x;
            cin >> l >> r >> x; l--; r--;
            while(l <= r && l % B != 0) a[l++] += x;
            while(l + B - 1 <= r){ lz[l / B] += x; l += B; }   // cả khối: chỉ dán nhãn
            while(l <= r) a[l++] += x;
        } else {
            int i; cin >> i; i--;
            cout << a[i] + lz[i / B] << "\n";                  // giá trị thật = a[i] + nhãn
        }
    }
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [int(x) for x in data[2:2 + n]]
    B = max(1, isqrt(n))
    lz = [0] * (n // B + 1)
    out, p = [], 2 + n
    for _ in range(q):
        if data[p] == b'1':
            l, r, x = int(data[p + 1]) - 1, int(data[p + 2]) - 1, int(data[p + 3]); p += 4
            while l <= r and l % B:
                a[l] += x; l += 1
            while l + B - 1 <= r:
                lz[l // B] += x; l += B
            while l <= r:
                a[l] += x; l += 1
        else:
            i = int(data[p + 1]) - 1; p += 2
            out.append(a[i] + lz[i // B])
    print('\n'.join(map(str, out)))

main()
Bài 3Bật tắt đènTrung bình

Có $n$ bóng đèn xếp hàng, ban đầu tất cả đều tắt ($n, q \le 10^5$). Có $q$ thao tác: 1 l r bấm công tắc mọi đèn từ $l$ tới $r$ (đèn đang tắt thì sáng, đang sáng thì tắt); 2 l r đếm số đèn đang sáng trong đoạn $[l, r]$.

Input
8 5
1 2 6
2 1 8
1 4 8
2 1 8
2 5 6
Output
5
4
0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Mỗi khối giữ nhãn fl[b] (cả khối đã bị đảo lẻ lần hay chưa) và on[b] là số đèn sáng thật của khối. Trạng thái thật của đèn $i$ là a[i] ^ fl[i / B]. Đảo nguyên khối: fl[b] ^= 1 và on[b] = độ dài khối − on[b]. Đảo lẻ một đèn: a[i] ^= 1 rồi tăng hoặc giảm on[b] theo trạng thái thật mới. Câu hỏi: phần lẻ cộng a[i] ^ fl, khối trọn cộng on[b]. Ví dụ: sau thao tác đầu đèn 2..6 sáng (5 đèn); sau thao tác thứ hai đèn 4, 5, 6 tắt, đèn 7, 8 sáng, còn 2, 3, 7, 8 sáng (4 đèn); đèn 5, 6 đều tắt.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, q, B;
vector<int> a, on, fl;        // a[i]: trạng thái riêng, fl[b]: cả khối b đã bị đảo chưa, on[b]: số đèn sáng thật

int len(int b){ return min(n, (b + 1) * B) - b * B; }

void flipOne(int i){
    int b = i / B;
    a[i] ^= 1;
    if((a[i] ^ fl[b]) == 1) on[b]++; else on[b]--;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    B = max(1, (int)sqrt(n));
    a.assign(n, 0); on.assign(n / B + 1, 0); fl.assign(n / B + 1, 0);
    while(q--){
        int t, l, r;
        cin >> t >> l >> r; l--; r--;
        if(t == 1){
            while(l <= r && l % B != 0) flipOne(l++);
            while(l + B - 1 <= r){ int b = l / B; fl[b] ^= 1; on[b] = len(b) - on[b]; l += B; }
            while(l <= r) flipOne(l++);
        } else {
            int res = 0;
            while(l <= r && l % B != 0){ res += a[l] ^ fl[l / B]; l++; }
            while(l + B - 1 <= r){ res += on[l / B]; l += B; }
            while(l <= r){ res += a[l] ^ fl[l / B]; l++; }
            cout << res << "\n";
        }
    }
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    B = max(1, isqrt(n))
    nb = (n + B - 1) // B
    a = [0] * n
    on = [0] * nb                               # số đèn sáng thật của khối
    fl = [0] * nb                               # nhãn đảo cả khối
    ln = [min(n, (b + 1) * B) - b * B for b in range(nb)]
    out, p = [], 2
    for _ in range(q):
        t, l, r = int(data[p]), int(data[p + 1]) - 1, int(data[p + 2]) - 1; p += 3
        if t == 1:
            i = l
            while i <= r:
                b = i // B
                if i % B == 0 and i + B - 1 <= r:   # cả khối nằm trọn
                    fl[b] ^= 1; on[b] = ln[b] - on[b]; i += B
                else:
                    a[i] ^= 1
                    on[b] += 1 if a[i] ^ fl[b] else -1
                    i += 1
        else:
            res, i = 0, l
            while i <= r:
                b = i // B
                if i % B == 0 and i + B - 1 <= r:
                    res += on[b]; i += B
                else:
                    res += a[i] ^ fl[b]; i += 1
            out.append(res)
    print('\n'.join(map(str, out)))

main()
Bài 4Tổng theo bước nhảyTrung bình

Cho dãy $n$ số nguyên ($n, q \le 10^5$, $|a_i| \le 10^9$) và $q$ câu hỏi, mỗi câu gồm hai số $x, k$ ($1 \le x, k \le n$): tính $a_x + a_{x+k} + a_{x+2k} + \cdots$ (cộng tới khi chỉ số vượt quá $n$).

Input
7
1 2 3 4 5 6 7
3
1 3
2 1
6 5
Output
12
27
6

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Chia căn theo giá trị của $k$. Nếu $k > \sqrt{n}$, tổng có ít hơn $\sqrt{n}$ số hạng: cộng thẳng. Nếu $k \le \sqrt{n}$: gom các câu hỏi cùng $k$ lại, với mỗi $k$ như thế tính một lần mảng tổng hậu tố suf[i] = a[i] + suf[i + k] (đi từ phải sang trái) trong $O(n)$, rồi mỗi câu hỏi đọc suf[x]. Có không quá $\sqrt{n}$ giá trị $k$ nhỏ nên tổng là $O((n + q)\sqrt{n})$. Ví dụ: $a_1 + a_4 + a_7 = 12$; $k = 1$ thì cộng hết từ $a_2$, được 27; $a_6 = 6$ vì $6 + 5 > 7$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n; cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    int q; cin >> q;
    int B = max(1, (int)sqrt(n));
    vector<long long> ans(q);
    vector<vector<pair<int,int>>> byK(B + 1);          // truy vấn có bước nhỏ, gom theo k
    for(int j = 0; j < q; j++){
        int x, k; cin >> x >> k; x--;
        if(k <= B) byK[k].push_back({x, j});
        else {                                         // bước lớn: nhảy thẳng, tối đa n / k < √n bước
            long long s = 0;
            for(int i = x; i < n; i += k) s += a[i];
            ans[j] = s;
        }
    }
    vector<long long> suf(n);
    for(int k = 1; k <= B; k++){
        if(byK[k].empty()) continue;
        for(int i = n - 1; i >= 0; i--) suf[i] = a[i] + (i + k < n ? suf[i + k] : 0);
        for(auto [x, j] : byK[k]) ans[j] = suf[x];
    }
    for(int j = 0; j < q; j++) cout << ans[j] << "\n";
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    a = [int(x) for x in data[1:1 + n]]
    q = int(data[1 + n])
    B = max(1, isqrt(n))
    ans = [0] * q
    byK = {}
    p = 2 + n
    for j in range(q):
        x, k = int(data[p]) - 1, int(data[p + 1]); p += 2
        if k <= B:
            byK.setdefault(k, []).append((x, j))
        else:
            ans[j] = sum(a[x::k])                 # bước lớn: ít hơn √n số hạng
    for k, lst in byK.items():                    # bước nhỏ: một mảng tổng hậu tố cho mỗi k
        suf = a[:]
        for i in range(n - 1 - k, -1, -1):
            suf[i] += suf[i + k]
        for x, j in lst:
            ans[j] = suf[x]
    print('\n'.join(map(str, ans)))

main()
Bài 5Giá trị xuất hiện đúng hai lầnTrung bình

Cho dãy $n$ số nguyên ($n, q \le 10^5$, $|a_i| \le 10^9$) và $q$ câu hỏi l r: trong đoạn $a_l, \ldots, a_r$ có bao nhiêu giá trị xuất hiện đúng hai lần?

Input
8 3
1 2 1 3 2 3 3 1
1 5
2 7
1 8
Output
2
1
1

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Thuật toán Mo với biến two đếm số giá trị có cnt đúng bằng 2. Trong add và del: trước khi đổi cnt[v], nếu nó đang bằng 2 thì two--; sau khi đổi, nếu nó bằng 2 thì two++. Nhớ nén số vì $a_i$ tới $10^9$. Ví dụ: đoạn $[1, 5]$ = 1 2 1 3 2 có số 1 và số 2 xuất hiện hai lần; đoạn $[2, 7]$ = 2 1 3 2 3 3 chỉ có số 2; cả dãy có số 1 ba lần, số 3 ba lần, chỉ số 2 hai lần.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, q, B, two = 0;
vector<int> a, cnt;

void add(int i){ if(cnt[a[i]] == 2) two--; cnt[a[i]]++; if(cnt[a[i]] == 2) two++; }
void del(int i){ if(cnt[a[i]] == 2) two--; cnt[a[i]]--; if(cnt[a[i]] == 2) two++; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    a.resize(n);
    for(auto &x : a) cin >> x;
    vector<int> v = a;
    sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end());
    for(auto &x : a) x = lower_bound(v.begin(), v.end(), x) - v.begin();
    cnt.assign(v.size(), 0);
    B = max(1, (int)sqrt(n));
    vector<int> L(q), R(q), id(q), ans(q);
    for(int k = 0; k < q; k++){ cin >> L[k] >> R[k]; L[k]--; R[k]--; id[k] = k; }
    sort(id.begin(), id.end(), [&](int x, int y){
        if(L[x] / B != L[y] / B) return L[x] / B < L[y] / B;
        return ((L[x] / B) & 1) ? R[x] > R[y] : R[x] < R[y];
    });
    int l = 0, r = -1;
    for(int t : id){
        while(r < R[t]) add(++r);
        while(l > L[t]) add(--l);
        while(r > R[t]) del(r--);
        while(l < L[t]) del(l++);
        ans[t] = two;
    }
    for(int k = 0; k < q; k++) cout << ans[k] << "\n";
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    comp = {}
    a = [comp.setdefault(int(x), len(comp)) for x in data[2:2 + n]]
    L = [int(x) - 1 for x in data[2 + n:2 + n + 2 * q:2]]
    R = [int(x) - 1 for x in data[3 + n:3 + n + 2 * q:2]]
    B = max(1, isqrt(n))
    order = sorted(range(q), key=lambda k: (L[k] // B, R[k] if (L[k] // B) % 2 == 0 else -R[k]))
    cnt = [0] * len(comp)
    ans = [0] * q
    l, r, two = 0, -1, 0
    for k in order:
        ql, qr = L[k], R[k]
        while r < qr:
            r += 1; v = a[r]
            if cnt[v] == 2: two -= 1
            cnt[v] += 1
            if cnt[v] == 2: two += 1
        while l > ql:
            l -= 1; v = a[l]
            if cnt[v] == 2: two -= 1
            cnt[v] += 1
            if cnt[v] == 2: two += 1
        while r > qr:
            v = a[r]
            if cnt[v] == 2: two -= 1
            cnt[v] -= 1
            if cnt[v] == 2: two += 1
            r -= 1
        while l < ql:
            v = a[l]
            if cnt[v] == 2: two -= 1
            cnt[v] -= 1
            if cnt[v] == 2: two += 1
            l += 1
        ans[k] = two
    sys.stdout.write('\n'.join(map(str, ans)) + '\n')

main()
Bài 6Sức mạnh của đoạnKhá

Cho dãy $n$ số nguyên dương ($n, q \le 2 \cdot 10^5$, $1 \le a_i \le 10^6$). Với đoạn $[l, r]$, gọi $c_v$ là số lần giá trị $v$ xuất hiện trong đoạn. Sức mạnh của đoạn là $\sum_v c_v^2 \cdot v$. Trả lời $q$ câu hỏi l r.

Input
6 3
1 2 1 3 2 1
1 3
2 5
1 6
Output
6
12
20

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Mo với biến cur là sức mạnh của cửa sổ. Khi $c_v$ tăng từ $c$ lên $c + 1$, $c^2 v$ thành $(c+1)^2 v$, tức tăng thêm $(2c + 1)\,v$; khi giảm từ $c + 1$ về $c$ thì bớt đúng lượng đó. Vì $a_i \le 10^6$ nên dùng thẳng mảng cnt cỡ $10^6$, không cần nén. Đáp án có thể tới $n^2 \cdot 10^6 = 4 \cdot 10^{16}$: bắt buộc long long. Ví dụ đoạn $[1, 6]$: số 1 ba lần, số 2 hai lần, số 3 một lần: $9 \cdot 1 + 4 \cdot 2 + 1 \cdot 3 = 20$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, q, B, a[200005], cnt[1000005];
long long cur = 0;                     // tổng cnt[v]^2 * v của cửa sổ

void add(int i){ int v = a[i]; cur += (2LL * cnt[v] + 1) * v; cnt[v]++; }   // (c+1)^2 - c^2 = 2c + 1
void del(int i){ int v = a[i]; cnt[v]--; cur -= (2LL * cnt[v] + 1) * v; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 0; i < n; i++) cin >> a[i];
    B = max(1, (int)sqrt(n));
    vector<int> L(q), R(q), id(q);
    vector<long long> ans(q);
    for(int k = 0; k < q; k++){ cin >> L[k] >> R[k]; L[k]--; R[k]--; id[k] = k; }
    sort(id.begin(), id.end(), [&](int x, int y){
        if(L[x] / B != L[y] / B) return L[x] / B < L[y] / B;
        return ((L[x] / B) & 1) ? R[x] > R[y] : R[x] < R[y];
    });
    int l = 0, r = -1;
    for(int t : id){
        while(r < R[t]) add(++r);
        while(l > L[t]) add(--l);
        while(r > R[t]) del(r--);
        while(l < L[t]) del(l++);
        ans[t] = cur;
    }
    for(int k = 0; k < q; k++) cout << ans[k] << "\n";
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [int(x) for x in data[2:2 + n]]
    L = [int(x) - 1 for x in data[2 + n:2 + n + 2 * q:2]]
    R = [int(x) - 1 for x in data[3 + n:3 + n + 2 * q:2]]
    B = max(1, isqrt(n))
    order = sorted(range(q), key=lambda k: (L[k] // B, R[k] if (L[k] // B) % 2 == 0 else -R[k]))
    cnt = [0] * (max(a) + 1)
    ans = [0] * q
    l, r, cur = 0, -1, 0
    for k in order:
        ql, qr = L[k], R[k]
        while r < qr:
            r += 1; v = a[r]
            cur += (2 * cnt[v] + 1) * v; cnt[v] += 1
        while l > ql:
            l -= 1; v = a[l]
            cur += (2 * cnt[v] + 1) * v; cnt[v] += 1
        while r > qr:
            v = a[r]; cnt[v] -= 1
            cur -= (2 * cnt[v] + 1) * v; r -= 1
        while l < ql:
            v = a[l]; cnt[v] -= 1
            cur -= (2 * cnt[v] + 1) * v; l += 1
        ans[k] = cur
    sys.stdout.write('\n'.join(map(str, ans)) + '\n')

main()
Bài 7Đoạn con có XOR bằng kKhá

Cho dãy $n$ số nguyên không âm ($n, q \le 10^5$, $0 \le a_i, k < 2^{20}$) và $q$ câu hỏi l r: đếm số đoạn con $[i, j]$ với $l \le i \le j \le r$ mà $a_i \oplus a_{i+1} \oplus \cdots \oplus a_j = k$ ($\oplus$ là phép XOR). Dòng đầu gồm $n, q, k$.

Input
6 3 3
1 2 3 1 2 3
1 3
2 6
1 6
Output
2
4
6

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đặt XOR tiền tố $P_0 = 0$, $P_i = P_{i-1} \oplus a_i$. Đoạn $[i, j]$ có XOR là $P_j \oplus P_{i-1}$. Vậy câu hỏi $[l, r]$ trở thành: trong các chỉ số tiền tố từ $l - 1$ tới $r$, đếm cặp $x < y$ với $P_x \oplus P_y = k$, tức $P_x = P_y \oplus k$. Chạy Mo trên mảng $P$: add(i) cộng cnt[P[i] ^ k] vào đáp án rồi mới tăng cnt[P[i]]; del(i) giảm cnt[P[i]] trước rồi trừ cnt[P[i] ^ k] (thứ tự này quan trọng khi $k = 0$). Ví dụ đoạn $[1, 3]$ có hai đoạn con XOR bằng 3: $1 \oplus 2$ và $3$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, q, K, B, P[200005], cnt[1 << 20];
long long cur = 0;                     // số cặp x < y trong cửa sổ có P[x] ^ P[y] = K

void add(int i){ cur += cnt[P[i] ^ K]; cnt[P[i]]++; }      // đếm trước, thêm sau
void del(int i){ cnt[P[i]]--; cur -= cnt[P[i] ^ K]; }      // bớt trước, trừ sau

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q >> K;
    for(int i = 1; i <= n; i++){ int x; cin >> x; P[i] = P[i - 1] ^ x; }
    B = max(1, (int)sqrt(n + 1));
    vector<int> L(q), R(q), id(q);
    vector<long long> ans(q);
    for(int k = 0; k < q; k++){ cin >> L[k] >> R[k]; L[k]--; id[k] = k; }   // đoạn chỉ số tiền tố [l-1, r]
    sort(id.begin(), id.end(), [&](int x, int y){
        if(L[x] / B != L[y] / B) return L[x] / B < L[y] / B;
        return ((L[x] / B) & 1) ? R[x] > R[y] : R[x] < R[y];
    });
    int l = 0, r = -1;
    for(int t : id){
        while(r < R[t]) add(++r);
        while(l > L[t]) add(--l);
        while(r > R[t]) del(r--);
        while(l < L[t]) del(l++);
        ans[t] = cur;
    }
    for(int k = 0; k < q; k++) cout << ans[k] << "\n";
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q, K = int(data[0]), int(data[1]), int(data[2])
    P = [0] * (n + 1)
    for i in range(1, n + 1):
        P[i] = P[i - 1] ^ int(data[2 + i])
    L = [int(x) - 1 for x in data[3 + n:3 + n + 2 * q:2]]      # đoạn tiền tố [l-1, r]
    R = [int(x) for x in data[4 + n:4 + n + 2 * q:2]]
    B = max(1, isqrt(n + 1))
    order = sorted(range(q), key=lambda k: (L[k] // B, R[k] if (L[k] // B) % 2 == 0 else -R[k]))
    cnt = [0] * (1 << 20)
    ans = [0] * q
    l, r, cur = 0, -1, 0
    for k in order:
        ql, qr = L[k], R[k]
        while r < qr:
            r += 1; v = P[r]
            cur += cnt[v ^ K]; cnt[v] += 1
        while l > ql:
            l -= 1; v = P[l]
            cur += cnt[v ^ K]; cnt[v] += 1
        while r > qr:
            v = P[r]; cnt[v] -= 1
            cur -= cnt[v ^ K]; r -= 1
        while l < ql:
            v = P[l]; cnt[v] -= 1
            cur -= cnt[v ^ K]; l += 1
        ans[k] = cur
    sys.stdout.write('\n'.join(map(str, ans)) + '\n')

main()
Bài 8Bàn bật nảyKhó

Có $n$ ô xếp thành hàng, ô $i$ có lò xo độ mạnh $a_i \ge 1$: viên bi rơi vào ô $i$ sẽ bật sang ô $i + a_i$, và rời bàn khi chỉ số vượt quá $n$ ($n, q \le 10^5$, $1 \le a_i \le n$). Có $q$ thao tác: 0 i x đổi độ mạnh ô $i$ thành $x$; 1 i thả bi vào ô $i$, in ra ô cuối cùng bi chạm vào trước khi rời bàn và số lần bật (tính cả lần bật ra khỏi bàn).

Input
8 4
1 2 1 3 1 2 1 1
1 1
0 4 5
1 1
1 6
Output
8 5
4 3
8 2

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Chia dãy thành khối. Với mỗi ô $i$, tính sẵn: nxt[i] là ô đầu tiên bi tới được ngoài khối của $i$, jumps[i] là số lần bật để tới đó, last[i] là ô cuối cùng trong khối trước khi ra. Tính trong mỗi khối từ phải sang trái: nếu $j = i + a_i$ đã ra khỏi khối thì nxt[i] = j, jumps[i] = 1, last[i] = i; ngược lại kế thừa từ ô $j$. Thả bi: mỗi bước nhảy hẳn qua một khối, $O(\frac{n}{B})$. Đổi độ mạnh: chỉ tính lại một khối, $O(B)$. Ví dụ: bi từ ô 1 đi 1 → 2 → 4 → 7 → 8 → ra ngoài (5 lần bật); sau khi $a_4 = 5$ thì 1 → 2 → 4 → ra ngoài (ô cuối là 4, 3 lần bật); bi từ ô 6 đi 6 → 8 → ra ngoài.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, q, B, a[200005], nxt[200005], jumps[200005], last[200005];

void rebuild(int b){                         // tính lại khối b từ phải sang trái
    int lo = b * B, hi = min(n, lo + B) - 1;
    for(int i = hi; i >= lo; i--){
        int j = i + a[i];
        if(j > hi){ nxt[i] = j; jumps[i] = 1; last[i] = i; }        // nhảy ra khỏi khối
        else { nxt[i] = nxt[j]; jumps[i] = jumps[j] + 1; last[i] = last[j]; }
    }
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 0; i < n; i++) cin >> a[i];
    B = max(1, (int)sqrt(n));
    for(int b = 0; b * B < n; b++) rebuild(b);
    while(q--){
        int t; cin >> t;
        if(t == 0){ int i, x; cin >> i >> x; a[i - 1] = x; rebuild((i - 1) / B); }
        else {
            int i; cin >> i; i--;
            int tot = 0, lastHole = i;
            while(i < n){ tot += jumps[i]; lastHole = last[i]; i = nxt[i]; }   // mỗi bước qua hẳn một khối
            cout << lastHole + 1 << " " << tot << "\n";
        }
    }
    return 0;
}
import sys
from math import isqrt

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [int(x) for x in data[2:2 + n]]
    B = max(1, isqrt(n))
    nxt, jumps, last = [0] * n, [0] * n, [0] * n
    def rebuild(b):                          # tính lại khối b từ phải sang trái
        lo = b * B; hi = min(n, lo + B) - 1
        for i in range(hi, lo - 1, -1):
            j = i + a[i]
            if j > hi:
                nxt[i] = j; jumps[i] = 1; last[i] = i
            else:
                nxt[i] = nxt[j]; jumps[i] = jumps[j] + 1; last[i] = last[j]
    for b in range((n + B - 1) // B):
        rebuild(b)
    out, p = [], 2 + n
    for _ in range(q):
        if data[p] == b'0':
            i, x = int(data[p + 1]) - 1, int(data[p + 2]); p += 3
            a[i] = x
            rebuild(i // B)
        else:
            i = int(data[p + 1]) - 1; p += 2
            tot, lh = 0, i
            while i < n:
                tot += jumps[i]; lh = last[i]; i = nxt[i]
            out.append('%d %d' % (lh + 1, tot))
    print('\n'.join(out))

main()
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét