Chương 0·Bài 02

Prefix Sum

Cho một dãy số và một câu hỏi: tổng các số từ vị trí ll đến vị trí rr là bao nhiêu? Một câu thì cộng lại là xong. Nhưng đề thường hỏi hàng trăm nghìn câu trên cùng một dãy, và cộng lại từ đầu cho mỗi câu thì quá chậm.

Tổng tiền tố (prefix sum) chuẩn bị sẵn tổng của mọi đoạn bắt đầu từ đầu dãy. Có những tổng đó rồi, mỗi câu hỏi chỉ còn một phép trừ. Ý tưởng ngắn này là nền của mảng hiệu, tiền tố hai chiều, và về sau là cây Fenwick, cây phân đoạn.

Tổng đoạn bằng một phép trừ

Cộng từng số trong đoạn tốn tới nn phép cộng cho một câu hỏi. Với n=q=2⋅105n = q = 2 \cdot 10^5, cả bài cỡ 4⋅10104 \cdot 10^{10} phép cộng, quá xa giới hạn một giây.

Đánh số dãy từ 1: a1,a2,…,ana_1, a_2, \dots, a_n, và định nghĩa

pre0=0,prei=a1+a2+⋯+ai=prei−1+ai.\mathrm{pre}_0 = 0, \qquad \mathrm{pre}_i = a_1 + a_2 + \dots + a_i = \mathrm{pre}_{i-1} + a_i .

Mỗi prei\mathrm{pre}_i là tổng của tiền tố dài ii. Tổng đoạn [l,r][l, r] là tiền tố dài rr bỏ đi tiền tố dài l−1l - 1:

al+al+1+⋯+ar=prer−prel−1.a_l + a_{l+1} + \dots + a_r = \mathrm{pre}_r - \mathrm{pre}_{l-1}.

Hai tiền tố có chung phần a1,…,al−1a_1, \dots, a_{l-1}, phép trừ xóa đúng phần đó. Ô pre0=0\mathrm{pre}_0 = 0 không thừa: nhờ nó, đoạn bắt đầu từ l=1l = 1 vẫn dùng chung công thức.

Với a=(3,1,4,1,5,9,2,6)a = (3, 1, 4, 1, 5, 9, 2, 6) thì pre=(0,3,4,8,9,14,23,25,31)\mathrm{pre} = (0, 3, 4, 8, 9, 14, 23, 25, 31). Tổng đoạn [3,6][3, 6] là 4+1+5+9=194 + 1 + 5 + 9 = 19, đúng bằng pre6−pre2=23−4\mathrm{pre}_6 - \mathrm{pre}_2 = 23 - 4.

Kéo l, r: tổng đoạn luôn bằng hiệu hai ô của pre.

Hai lỗi hay gặp nằm ngay trong công thức ngắn này. Một là kiểu dữ liệu: mỗi aia_i vừa int, nhưng tổng 2⋅1052 \cdot 10^5 số cỡ 10910^9 lên tới 2⋅10142 \cdot 10^{14}, nên pre\mathrm{pre} phải là long long. Hai là chỉ số: đọc dãy vào a[0..n-1] mà vẫn viết pre[r] - pre[l - 1] thì mọi câu trả lời lệch một vị trí. Cách an toàn nhất: cho pre\mathrm{pre} có n+1n + 1 ô và luôn hiểu prei\mathrm{pre}_i là tổng ii phần tử đầu.

Hỏi nhanh

Dãy được đọc vào a[0], a[1], ..., a[n-1] (đánh số từ 0), và ta dựng pre[i] = a[0] + ... + a[i-1] với pre[0] = 0. Câu hỏi cũng dùng chỉ số từ 0: tổng a[l] + ... + a[r]. Biểu thức nào đúng?

Ví dụTổng Trên Đoạn Cố Định

Đề. Dãy nn số nguyên dương xi≤109x_i \le 10^9 không đổi, qq truy vấn [l,r][l, r], in tổng đoạn. n,q≤2⋅105n, q \le 2 \cdot 10^5.

Lời giải. Đúng bài toán của mục này. Dựng pre\mathrm{pre} khi đọc dãy, mỗi truy vấn in prer−prel−1\mathrm{pre}_r - \mathrm{pre}_{l-1}. Cả bài O(n+q)O(n + q), tổng lớn nhất cỡ 2⋅10142 \cdot 10^{14} nên dùng long long.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, q, a[N], pre[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> q;
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }
    while (q--) {
        ll l, r;
        cin >> l >> r;
        cout << pre[r] - pre[l - 1] << '\n';
    }
}
7 4
4 2 7 1 3 5 6
1 1
2 5
4 7
1 7

XOR và đếm

Công thức prer−prel−1\mathrm{pre}_r - \mathrm{pre}_{l-1} chỉ cần hai điều ở phép cộng: kết hợp được, và có phép ngược để gỡ phần đầu ra. Phép toán nào có đủ hai điều đó cũng dựng tiền tố được.

  • XOR. Phép XOR, viết x⊕yx \oplus y và trong C++ là x ^ y, so hai số theo từng chữ số nhị phân: hai chữ số khác nhau cho 1, giống nhau cho 0. Chẳng hạn 5⊕3=1012⊕0112=1102=65 \oplus 3 = 101_2 \oplus 011_2 = 110_2 = 6. Đặt pxi=a1⊕⋯⊕ai\mathrm{px}_i = a_1 \oplus \dots \oplus a_i. Vì x⊕x=0x \oplus x = 0, các phần tử trước ll xuất hiện hai lần trong pxr⊕pxl−1\mathrm{px}_r \oplus \mathrm{px}_{l-1} và tự triệt tiêu, nên XOR đoạn [l,r][l, r] là pxr⊕pxl−1\mathrm{px}_r \oplus \mathrm{px}_{l-1}.
  • Đếm. Muốn biết đoạn [l,r][l, r] có bao nhiêu phần tử thỏa một tính chất (số chẵn, chữ a, ô có cây), đánh dấu bi=1b_i = 1 nếu phần tử thứ ii thỏa, 00 nếu không, rồi lấy tiền tố của bb. Với xâu chữ thường, 26 mảng tiền tố cho số lần xuất hiện của từng chữ trong mọi đoạn.
Đổi xâu, chữ cần đếm hoặc kéo l, r: số lần xuất hiện luôn là hiệu hai ô của pre.

Phép min và max thì không gỡ được: biết số nhỏ nhất của a1..ara_1..a_r và của a1..al−1a_1..a_{l-1} không cho biết số nhỏ nhất của al..ara_l..a_r. Với các phép này cần công cụ khác: bảng thưa (bài 22) khi dãy không đổi, cây phân đoạn (bài 17) khi dãy bị sửa. Tiền tố của min vẫn có ích khi đoạn luôn bắt đầu từ 1, ví dụ “giá trị nhỏ nhất trong ii phần tử đầu”.

Hỏi nhanh

Bài nào không giải được bằng một mảng tiền tố dựng sẵn rồi trả lời mỗi truy vấn trong O(1)O(1)? (Dãy không thay đổi giữa các truy vấn.)

Ví dụĐếm Cặp AC Trên Đoạn

Đề. Chuỗi SS dài n≤105n \le 10^5 gồm A, C, G, T. Mỗi truy vấn [l,r][l, r] hỏi có bao nhiêu chỉ số ii với l≤i<rl \le i < r mà SiSi+1S_i S_{i+1} là AC.

Lời giải. Một cặp AC chiếm hai ô nên phải chọn gắn nó với ô nào. Gắn với ô bên phải: đặt bi=1b_i = 1 nếu Si−1SiS_{i-1} S_i là AC. Cặp nằm trọn trong [l,r][l, r] khi ô phải của nó thuộc [l+1,r][l + 1, r], vậy đáp án là prer−prel\mathrm{pre}_r - \mathrm{pre}_l, không phải prer−prel−1\mathrm{pre}_r - \mathrm{pre}_{l-1}. Viết nhầm thành l−1l - 1 sẽ đếm cả cặp vắt qua biên trái, có ô A ở vị trí l−1l - 1.

Trên ví dụ AACACGACCT, các cặp có ô phải ở 3, 5, 8. Truy vấn [2,5][2, 5] lấy hai ô 3 và 5; truy vấn [8,10][8, 10] không lấy ô 8, vì A ở vị trí 7 nằm ngoài đoạn.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e5 + 5;
ll n, q, pre[N];
string s;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> q >> s;
    s = " " + s;
    for (ll i = 1; i <= n; i++) pre[i] = pre[i - 1] + (i > 1 && s[i - 1] == 'A' && s[i] == 'C');
    while (q--) {
        ll l, r;
        cin >> l >> r;
        cout << pre[r] - pre[l] << '\n';
    }
}
10 5
AACACGACCT
1 10
2 5
3 7
5 8
8 10

Đếm đoạn con

Nhiều bài hỏi “có bao nhiêu đoạn con thỏa…”. Có n(n+1)2\frac{n(n+1)}{2} đoạn, duyệt hết là O(n2)O(n^2). Mẹo là viết điều kiện của đoạn [l,r][l, r] thành điều kiện trên hai số prel−1\mathrm{pre}_{l-1} và prer\mathrm{pre}_r, rồi cố định đầu phải rr và đếm xem có bao nhiêu chỉ số j=l−1<rj = l - 1 < r ghép được với nó.

Bài kinh điển: đếm đoạn có tổng bằng kk. Điều kiện prer−prej=k\mathrm{pre}_r - \mathrm{pre}_j = k tương đương prej=prer−k\mathrm{pre}_j = \mathrm{pre}_r - k. Duyệt rr từ trái sang phải, giữ bảng đếm cnt các giá trị pre0,…,prer−1\mathrm{pre}_0, \dots, \mathrm{pre}_{r-1} đã gặp; mỗi đầu phải rr góp cnt[pre[r] - k] đoạn. Bảng đếm có sẵn giá trị pre0=0\mathrm{pre}_0 = 0 với số lần 1, chính là tiền tố rỗng ứng với các đoạn bắt đầu từ l=1l = 1.

C++
map<ll, ll> cnt;
cnt[0] = 1;
ll cur = 0, ans = 0;
for (ll i = 1; i <= n; i++) {
    cur += a[i];
    ans += cnt[cur - k];
    cnt[cur]++;
}

Thứ tự hai dòng cuối trong vòng lặp quan trọng: phải cộng cnt[cur - k] trước khi thêm cur của chính đầu phải này, nếu không với k=0k = 0 ta đếm cả đoạn rỗng j=rj = r.

map (bài 09) đếm được theo giá trị bất kỳ, kể cả số âm hay số rất lớn, đổi lại mỗi bước chậm hơn mảng. Khi tiền tố chỉ nhận ít giá trị, ví dụ phần dư khi chia cho mm, hãy dùng mảng đếm. Chênh lệch lớn hơn ta tưởng:

Đếm một triệu tiền tố theo phần dư, trung vị 5 lần chạy, i7-10850H, g++ -O2.mảng đếm29 msunordered_map238 ms · gấp 8,2 lầnmap769 ms · gấp 27 lần
Đếm một triệu tiền tố theo phần dư bằng ba loại bảng đếm, đo trên i7-10850H, g++ -O2.

Cùng một vòng lặp, chỉ khác bảng đếm: map chậm hơn mảng gần 30 lần, unordered_map khoảng 8 lần. Với n=2⋅105n = 2 \cdot 10^5 cả ba đều qua, nhưng khi giới hạn chặt, đó là khác biệt giữa AC và TLE.

Cùng khuôn đó giải được nhiều điều kiện khác:

  • Tổng đoạn chia hết cho mm: prer\mathrm{pre}_r và prej\mathrm{pre}_j cùng số dư khi chia cho mm, đếm cặp cùng phần dư.
  • Tổng đoạn bằng độ dài đoạn: prer−r=prej−j\mathrm{pre}_r - r = \mathrm{pre}_j - j, đếm cặp cùng giá trị prei−i\mathrm{pre}_i - i.
  • Tổng đoạn lớn nhất: với mỗi rr, lấy prer\mathrm{pre}_r trừ tiền tố nhỏ nhất đứng trước nó; bảng đếm thay bằng một biến giữ tiền tố nhỏ nhất.

Bước khó nhất luôn là biến đổi để một vế chỉ chứa rr, vế kia chỉ chứa jj.

Hình dưới chạy trường hợp chia hết trên dữ liệu của ví dụ tiếp theo. Mỗi bước thêm một tiền tố và ghi phần dư của nó khi chia cho 6; cung nối nó với các tiền tố trước cùng dư.

Mỗi cung là một đoạn có tổng chia hết cho 6.

Hỏi nhanh

Đếm các đoạn [l,r][l, r] có tổng bằng độ dài r−l+1r - l + 1. Đặt ci=prei−ic_i = \mathrm{pre}_i - i. Điều kiện của đoạn tương đương với điều gì?

Ví dụĐếm Đoạn Có Tổng Chia Hết

Đề. Dãy N≤2⋅105N \le 2 \cdot 10^5 số nguyên, ∣Ai∣≤109|A_i| \le 10^9, có thể âm. Đếm số đoạn con có tổng chia hết cho NN.

Lời giải. Đoạn [l,r][l, r] thỏa khi và chỉ khi prer−prel−1\mathrm{pre}_r - \mathrm{pre}_{l-1} chia hết cho NN, tức hai tiền tố có cùng số dư khi chia cho NN, nên mỗi cặp tiền tố cùng phần dư ứng với đúng một đoạn. Duyệt rr, giữ cnt[d] là số tiền tố đứng trước có phần dư dd, cộng cnt của phần dư hiện tại vào đáp án rồi mới tăng nó. Phần dư chỉ có NN giá trị nên dùng mảng, cả bài O(N)O(N).

Chỗ dễ sai là số âm: trong C++, -7 % 6 bằng -1 chứ không phải 5, nên phải đưa phần dư về khoảng từ 00 tới N−1N - 1 bằng ((x % n) + n) % n, nếu không hai tiền tố cùng số dư rơi vào hai ô khác nhau. Đáp án có thể tới N(N+1)2≈2⋅1010\frac{N(N+1)}{2} \approx 2 \cdot 10^{10}, cần long long.

Trên ví dụ, tiền tố là 0,2,−1,3,4,0,60, 2, -1, 3, 4, 0, 6, phần dư theo 6 là 0,2,5,3,4,0,00, 2, 5, 3, 4, 0, 0. Ba tiền tố dư 0 (vị trí 0, 5, 6) cho ba cặp, ứng với các đoạn [1,5][1, 5], [1,6][1, 6], [6,6][6, 6].

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, cnt[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    cnt[0] = 1;
    ll cur = 0, ans = 0;
    for (ll i = 1; i <= n; i++) {
        ll x;
        cin >> x;
        cur = ((cur + x) % n + n) % n;
        ans += cnt[cur];
        cnt[cur]++;
    }
    cout << ans << '\n';
}
6
2 -3 4 1 -4 6

Mảng hiệu

Bài toán ngược lại: dãy ban đầu toàn 0, có mm thao tác “cộng xx vào mọi phần tử từ ll đến rr”, cuối cùng in dãy. Làm thẳng tốn O(n)O(n) mỗi thao tác. Mảng hiệu (difference array) diff\mathrm{diff} làm mỗi thao tác chỉ bằng hai phép sửa:

diffl+=x,diffr+1−=x.\mathrm{diff}_l \mathrel{+}= x, \qquad \mathrm{diff}_{r+1} \mathrel{-}= x .

Sau mọi thao tác, ai=diff1+⋯+diffia_i = \mathrm{diff}_1 + \dots + \mathrm{diff}_i, tức aa là tổng tiền tố của diff\mathrm{diff}. Một thao tác trên [l,r][l, r] làm tổng diff1+⋯+diffi\mathrm{diff}_1 + \dots + \mathrm{diff}_i tăng xx khi i≥li \ge l rồi giảm lại xx khi i≥r+1i \ge r + 1, nên đúng các vị trí l≤i≤rl \le i \le r được cộng xx. Mảng hiệu và tổng tiền tố là hai phép ngược của nhau.

Ba thao tác của chương trình dưới: mỗi thao tác sửa hai ô, một lượt cộng dồn cho lại dãy.
C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, m, diff[N];

signed main() {
    cin >> n >> m;
    while (m--) {
        ll l, r, x;
        cin >> l >> r >> x;
        diff[l] += x;
        diff[r + 1] -= x;
    }
    ll cur = 0;
    for (ll i = 1; i <= n; i++) {
        cur += diff[i];
        cout << cur << (i == n ? '\n' : ' ');
    }
}
8 3
2 5 2
4 7 3
1 3 -1

Mảng diff phải có ô n+1n + 1 vì thao tác có r=nr = n sửa tới đó. Kỹ thuật này chỉ dùng được khi mọi thao tác đến trước mọi câu hỏi. Nếu cộng đoạn và hỏi xen kẽ, cần cây Fenwick hoặc cây phân đoạn có cập nhật đoạn.

Mảng hiệu hay xuất hiện ở dạng độ phủ: có nhiều đoạn trên trục số, hỏi mỗi điểm nằm trong bao nhiêu đoạn. Mỗi đoạn cộng 1 lên [l,r][l, r], một lượt cộng dồn cho độ phủ của mọi điểm.

Ví dụĐiểm Đạt Ngưỡng

Đề. Trên trục 1..2⋅1051..2 \cdot 10^5 có NN đoạn. Một điểm nguyên đạt ngưỡng nếu nằm trong ít nhất KK đoạn. QQ truy vấn [a,b][a, b] hỏi đoạn đó chứa bao nhiêu điểm đạt ngưỡng. N,Q≤2⋅105N, Q \le 2 \cdot 10^5.

Lời giải. Hai tầng tiền tố. Tầng một là mảng hiệu cho độ phủ: mỗi đoạn tăng diff[l], giảm diff[r + 1], cộng dồn được số đoạn chứa từng điểm xx. Tầng hai là tiền tố của dãy đánh dấu “điểm xx đạt ngưỡng”, gọi là good. Truy vấn trả lời bằng good[b] - good[a - 1]. Gọi M=2⋅105M = 2 \cdot 10^5 là độ dài trục, cả bài O(N+M+Q)O(N + M + Q); đếm lại từng điểm cho mỗi truy vấn thì tốn tới Q⋅M=4⋅1010Q \cdot M = 4 \cdot 10^{10}.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll M = 2e5;
ll n, k, q, diff[M + 5], good[M + 5];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> k >> q;
    for (ll i = 1; i <= n; i++) {
        ll l, r;
        cin >> l >> r;
        diff[l]++;
        diff[r + 1]--;
    }
    ll cover = 0;
    for (ll x = 1; x <= M; x++) {
        cover += diff[x];
        good[x] = good[x - 1] + (cover >= k);
    }
    while (q--) {
        ll a, b;
        cin >> a >> b;
        cout << good[b] - good[a - 1] << '\n';
    }
}
4 3 5
1 5
3 7
4 9
6 10
1 10
3 6
5 8
1 3
7 10

Tiền tố hai chiều

Trên bảng R×CR \times C, đặt P(i,j)P(i, j) là tổng hình chữ nhật từ ô (1,1)(1, 1) tới ô (i,j)(i, j), với hàng 0 và cột 0 bằng 0. Mỗi ô tính từ ba ô đã có:

P(i,j)=ai,j+P(i−1,j)+P(i,j−1)−P(i−1,j−1).P(i, j) = a_{i,j} + P(i-1, j) + P(i, j-1) - P(i-1, j-1).

Hai hình P(i−1,j)P(i-1, j) và P(i,j−1)P(i, j-1) cùng chứa phần P(i−1,j−1)P(i-1, j-1), nên phần đó bị cộng hai lần và phải trừ đi một lần. Tổng hình chữ nhật từ hàng r1r_1 tới r2r_2, cột c1c_1 tới c2c_2 ghép theo cùng cách nghĩ, gọi là bao hàm loại trừ: lấy hình lớn, trừ hai dải thừa, rồi cộng lại góc bị trừ hai lần:

S=P(r2,c2)−P(r1−1,c2)−P(r2,c1−1)+P(r1−1,c1−1).S = P(r_2, c_2) - P(r_1 - 1, c_2) - P(r_2, c_1 - 1) + P(r_1 - 1, c_1 - 1).
Kéo chuột trên bảng để chọn hình chữ nhật.

Dựng bảng tốn O(RC)O(RC), mỗi câu hỏi O(1)O(1). Mảng hiệu cũng có bản hai chiều: cộng xx lên một hình chữ nhật bằng cách sửa bốn góc, +x+x tại (r1,c1)(r_1, c_1) và (r2+1,c2+1)(r_2 + 1, c_2 + 1), −x-x tại (r1,c2+1)(r_1, c_2 + 1) và (r2+1,c1)(r_2 + 1, c_1), rồi lấy tiền tố hai chiều một lần ở cuối.

Hai vòng lặp dựng bảng đổi chỗ cho nhau thì công thức vẫn đúng, độ phức tạp vẫn O(RC)O(RC). Tốc độ thật thì khác: mảng hai chiều trong C++ nằm liền nhau theo từng hàng, nên để cột jj ở vòng trong là đọc các ô sát nhau, còn để hàng ii ở vòng trong là mỗi bước nhảy qua cả một hàng.

Dựng tiền tố hai chiều 3000 × 3000, trung vị 5 lần chạy, i7-10850H, g++ -O2.theo hàng (i ngoài, j trong)39 mstheo cột (j ngoài, i trong)107 ms · gấp 2,8 lần
Dựng tiền tố hai chiều trên bảng 3000 × 3000 theo hai thứ tự vòng lặp, đo trên i7-10850H, g++ -O2.

Cùng 9⋅1069 \cdot 10^6 phép tính mà chậm hơn khoảng hai lần rưỡi. Quy tắc dễ nhớ: chỉ số cuối của mảng nằm ở vòng lặp trong cùng.

Ví dụĐếm Cây Trong Hình Chữ Nhật

Đề. Lưới n×nn \times n (n≤1000n \le 1000), ô * có cây. q≤2⋅105q \le 2 \cdot 10^5 truy vấn, mỗi truy vấn cho góc trên-trái và góc dưới-phải của một hình chữ nhật, hỏi có bao nhiêu cây bên trong.

Lời giải. Coi ô có cây là 1, ô trống là 0, dựng PP theo công thức trên rồi trả lời bằng bốn góc. Dựng O(n2)=106O(n^2) = 10^6, mỗi truy vấn O(1)O(1). Tên biến cho góc tránh y1: trong <cmath> đã có hàm tên đó, khai báo biến toàn cục trùng tên sẽ lỗi biên dịch.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1005;
ll n, q, p[N][N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> q;
    for (ll i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (ll j = 1; j <= n; j++) p[i][j] = (s[j - 1] == '*') + p[i - 1][j] + p[i][j - 1] - p[i - 1][j - 1];
    }
    while (q--) {
        ll r1, c1, r2, c2;
        cin >> r1 >> c1 >> r2 >> c2;
        cout << p[r2][c2] - p[r1 - 1][c2] - p[r2][c1 - 1] + p[r1 - 1][c1 - 1] << '\n';
    }
}
5 5
.*..*
**...
..*..
*..**
...*.
1 1 1 5
1 1 5 5
2 2 4 4
3 3 3 3
4 4 5 5

Khi đi thi

  • Thấy nhiều câu hỏi về đoạn trên dãy không đổi là nghĩ ngay tới tiền tố; nếu dãy bị sửa xen giữa các câu hỏi thì chuyển sang cây Fenwick hoặc cây phân đoạn.
  • Viết pre\mathrm{pre} có n+1n + 1 ô với pre0=0\mathrm{pre}_0 = 0, dùng long long, và đưa phần dư âm về khoảng từ 00 tới m−1m - 1 trước khi dùng làm chỉ số.
  • Bài đếm đoạn con: biến đổi điều kiện để một vế chỉ chứa prer\mathrm{pre}_r và rr, vế kia chỉ chứa prej\mathrm{pre}_j và jj, rồi đếm bằng bảng đếm khi duyệt đầu phải.

Tóm tắt

  • prei=prei−1+ai\mathrm{pre}_i = \mathrm{pre}_{i-1} + a_i, pre0=0\mathrm{pre}_0 = 0; tổng đoạn [l,r][l, r] là prer−prel−1\mathrm{pre}_r - \mathrm{pre}_{l-1}, dựng O(n)O(n), hỏi O(1)O(1).
  • Phép toán kết hợp có phép ngược (cộng, XOR, đếm) đều dựng tiền tố được; min, max thì không.
  • Đếm đoạn con thỏa điều kiện: cố định đầu phải, đếm các tiền tố trước đó ghép được, nhớ đưa tiền tố rỗng vào từ đầu.
  • Mảng hiệu là phép ngược của tiền tố: cộng một đoạn bằng hai phép sửa, cộng dồn một lần ở cuối.
  • Bản hai chiều dựng và hỏi bằng bao hàm loại trừ trên bốn góc.

Bài tập

Đọc thêm