Chương 1·Bài 16

Fenwick Tree

Cho dãy nn số và qq thao tác xen kẽ nhau: đổi một phần tử, hoặc hỏi tổng của một đoạn. Nếu dãy đứng yên thì tổng tiền tố (bài 02) trả lời mỗi câu hỏi trong O(1)O(1). Nhưng chỉ cần một phần tử bị sửa, mọi tiền tố đứng sau nó đều sai và dựng lại mất O(n)O(n); với n=q=2⋅105n = q = 2 \cdot 10^5, đó là cỡ 4⋅10104 \cdot 10^{10} phép tính. Cây Fenwick (Fenwick tree, còn gọi là cây chỉ số nhị phân, Binary Indexed Tree, BIT) làm cả hai việc, sửa một phần tử và hỏi một tiền tố, trong O(log⁡n)O(\log n), tức khoảng 18 bước.

Cấu trúc chỉ là một mảng n+1n + 1 số cùng hai vòng lặp ba dòng, nhưng cách nó chia dãy thành các khúc dựa hoàn toàn vào cách viết nhị phân của chỉ số. Cây này cũng là công cụ đếm quen thuộc của các bài nghịch thế, đếm cặp và xếp hạng, nên bài dạy cả phần đếm đó.

Mỗi ô giữ một khúc dài lowbit

Ta cần một mảng a1,…,ana_1, \dots, a_n hỗ trợ hai thao tác: update(pos, val) tăng aposa_{pos} thêm valval (có thể âm), và get(pos) trả về a1+⋯+aposa_1 + \dots + a_{pos}, bằng 0 khi pos=0pos = 0. Tổng đoạn [l,r][l, r] là get(r)−get(l−1)\mathrm{get}(r) - \mathrm{get}(l-1); phép gán ak←ua_k \leftarrow u là update(k, u - a[k]). Có hai cách làm hiển nhiên, mỗi cách chậm một đầu: giữ nguyên mảng thì sửa O(1)O(1) nhưng hỏi tiền tố O(n)O(n); giữ mảng tổng tiền tố thì hỏi O(1)O(1) nhưng sửa một phần tử làm mọi tiền tố phía sau đổi theo, O(n)O(n). Cây Fenwick chọn lối ở giữa: mỗi ô giữ tổng của một khúc, độ dài các khúc là 1,2,4,8,…1, 2, 4, 8, \dots, sao cho mỗi tiền tố ghép được từ ít khúc và mỗi phần tử nằm trong ít khúc, cả hai con số đều O(log⁡n)O(\log n).

Với số nguyên dương ii, lowbit(i)\mathrm{lowbit}(i) là giá trị của bit 1 thấp nhất trong cách viết nhị phân của ii: 12=1100212 = 1100_2 có lowbit(12)=4\mathrm{lowbit}(12) = 4, 7=11127 = 111_2 có lowbit(7)=1\mathrm{lowbit}(7) = 1, 40=101000240 = 101000_2 có lowbit(40)=8\mathrm{lowbit}(40) = 8. Trong C++ đó là i & -i (bài 14). Cây Fenwick là mảng T1,…,TnT_1, \dots, T_n (trong code là bit[1..n]), với

Ti=ai−lowbit(i)+1+⋯+ai−1+ai,T_i = a_{i - \mathrm{lowbit}(i) + 1} + \dots + a_{i-1} + a_i ,

tức tổng của lowbit(i)\mathrm{lowbit}(i) phần tử kết thúc tại ii. Chỉ số lẻ có Ti=aiT_i = a_i. Còn T2=a1+a2T_2 = a_1 + a_2, T6=a5+a6T_6 = a_5 + a_6, T8=a1+⋯+a8T_8 = a_1 + \dots + a_8, T12=a9+⋯+a12T_{12} = a_9 + \dots + a_{12}.

Mỗi thanh T[i] dài lowbit(i) và kết thúc ở cột i; chỉnh pos rồi bấm get hoặc update.

Hỏi nhanh

Cây Fenwick có n=100n = 100. Ô T40T_{40} giữ tổng của những phần tử nào? (Biết 40=101000240 = 101000_2.)

Truy vấn: ghép tiền tố từ các bit của pospos. Để tính get(pos)\mathrm{get}(pos), bắt đầu từ ô TposT_{pos}: nó giữ lowbit(pos)\mathrm{lowbit}(pos) phần tử cuối của tiền tố. Phần còn lại là tiền tố độ dài pos−lowbit(pos)pos - \mathrm{lowbit}(pos), xử lý đúng cách ấy. Mỗi bước xóa bit 1 thấp nhất, cho tới khi về 0. Với 13=1101213 = 1101_2: get(13)=T13+T12+T8\mathrm{get}(13) = T_{13} + T_{12} + T_8, ba khúc dài 1,4,81, 4, 8 nối khít nhau. Số khúc bằng số bit 1, không quá ⌊log⁡2n⌋+1\lfloor \log_2 n \rfloor + 1.

get(13) ghép ba khúc nối khít nhau, đúng ba bit 1 của 13.

Hỏi nhanh

Gọi get(22)\mathrm{get}(22) trên cây Fenwick (biết 22=10110222 = 10110_2). Chương trình cộng các ô nào?

Cập nhật: sửa mọi ô phủ vị trí pospos. Khi aposa_{pos} tăng, ô TjT_j phải tăng nếu khúc của nó chứa pospos. Ô đầu tiên là TposT_{pos}; các ô còn lại nằm bên phải, và ta đi từ ô này sang ô phủ kế tiếp bằng cách cộng lowbit\mathrm{lowbit}. Với n=16n = 16 và pos=5pos = 5, dãy ô là 5→6→8→165 \to 6 \to 8 \to 16.

update(5, 3) sửa mọi thanh chứa cột 5, nhảy theo i cộng lowbit(i).

Vì sao đúng? Khúc của TkT_k với lowbit(k)=2m\mathrm{lowbit}(k) = 2^m là một khối căn lề (c⋅2m,(c+1)⋅2m](c \cdot 2^m, (c+1) \cdot 2^m], nên hai khúc bất kỳ hoặc rời nhau hoặc lồng nhau. Đặt b=lowbit(i)b = \mathrm{lowbit}(i) và j=i+bj = i + b. Không ô kk nào với i<k<ji < k < j phủ ii, vì lowbit(k)=lowbit(k−i)≤k−i\mathrm{lowbit}(k) = \mathrm{lowbit}(k - i) \le k - i nên khúc của TkT_k bắt đầu sau ii. Ô TjT_j phủ ii, vì jj chia hết cho 2b2b nên khúc của nó dài ít nhất 2b2b. Và với k>jk > j, TkT_k phủ ii khi và chỉ khi TkT_k phủ jj, do tính lồng nhau. Vậy các ô phủ ii đúng là ii, jj, rồi các ô phủ jj. Mỗi lần nhảy, lowbit\mathrm{lowbit} ít nhất gấp đôi, nên có không quá log⁡2n+1\log_2 n + 1 ô.

C++
void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

Hỏi nhanh

Đề cho chỉ số phần tử từ 0 đến n−1n - 1. Một bạn gọi update(0, val) thẳng vào cây cài như trên, quên cộng 1 vào chỉ số. Điều gì xảy ra?

Ví dụCộng Điểm, Hỏi Tiền Tố

Đề. Mảng n≤5⋅106n \le 5 \cdot 10^6 phần tử đánh số từ 0, ban đầu toàn 0. Xử lý q≤5⋅106q \le 5 \cdot 10^6 thao tác: + i delta cộng delta vào aia_i; ? i in a0+⋯+ai−1a_0 + \dots + a_{i-1} (tổng rỗng là 0).

Lời giải. Đúng hai hàm trên, chỉ lệch chỉ số: phần tử aia_i của đề là ô i+1i + 1 của cây, nên + i delta là update(i + 1, delta); còn “ii phần tử đầu” ứng với các ô 1..i1..i, nên ? i là get(i), và get(0) tự trả 0. Mảng bit 5⋅1065 \cdot 10^6 số ll là 40 MB. Dữ liệu cỡ 10710^7 số nên hai dòng tăng tốc là bắt buộc.

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

const ll N = 5e6 + 5;
ll n, q, bit[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

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

    cin >> n >> q;
    while (q--) {
        char op;
        ll i;
        cin >> op >> i;
        if (op == '+') {
            ll d;
            cin >> d;
            update(i + 1, d);
        } else cout << get(i) << '\n';
    }
}
5 8
? 0
+ 1 4
+ 3 -2
? 2
+ 0 7
? 1
? 4
? 5

Gán giá trị và dựng cây trong O(n)

Ba chỗ hay sai khi cài trọn vẹn. Một là chỉ số: cây chạy trên 1..n1..n. Hai là kiểu dữ liệu: tổng của 2⋅1052 \cdot 10^5 số cỡ 10910^9 lên tới 2⋅10142 \cdot 10^{14}, nên bit phải là long long. Ba là phép gán: cây chỉ biết cộng, nên muốn đặt ak=ua_k = u ta giữ thêm mảng giá trị hiện tại a và gọi update(k, u - a[k]); a[k] = u;.

Dựng cây bằng nn lần update tốn O(nlog⁡n)O(n \log n). Có cách dựng O(n)O(n). Gọi lowbit(i)=2K\mathrm{lowbit}(i) = 2^K; các ô c=i−2kc = i - 2^k với k=0,…,K−1k = 0, \dots, K - 1 có khúc (i−2k+1,i−2k](i - 2^{k+1}, i - 2^k], nối khít thành (i−2K,i−1](i - 2^K, i - 1], tức toàn bộ khúc của TiT_i trừ aia_i. Vậy

Ti=ai+Ti−1+Ti−2+Ti−4+⋯+Ti−2K−1,T_i = a_i + T_{i-1} + T_{i-2} + T_{i-4} + \dots + T_{i - 2^{K-1}},

chẳng hạn T8=a8+T7+T6+T4T_8 = a_8 + T_7 + T_6 + T_4. Mỗi ô con cc thỏa c+lowbit(c)=ic + \mathrm{lowbit}(c) = i, đúng là ô kế tiếp trong vòng cập nhật. Do đó gán bit[i] = a[i], rồi duyệt ii tăng dần và đẩy bit[i] lên ô i+lowbit(i)i + \mathrm{lowbit}(i): khi tới ii thì mọi ô con của nó, đều nhỏ hơn ii, đã đẩy giá trị lên. Mỗi ô đẩy đúng một lần.

Ví dụTổng Đoạn Động

Đề. Cho a1..ana_1..a_n và qq thao tác (n,q≤2⋅105n, q \le 2 \cdot 10^5, 1≤ai,u≤1091 \le a_i, u \le 10^9): 1 k u gán ak=ua_k = u; 2 l r in al+⋯+ara_l + \dots + a_r.

Lời giải. Dựng cây trong O(n)O(n) như trên. Gán là cộng phần chênh lệch u - a[k] (có thể âm) rồi cập nhật a[k]; tổng đoạn là get(r) - get(l - 1). Bất biến: sau mỗi thao tác, get(i) bằng tổng tiền tố của mảng hiện tại. Mỗi thao tác không quá hai lần duyệt cây, mỗi lần 18 bước: cả bài cỡ 7⋅1067 \cdot 10^6 phép, so với nq=4⋅1010nq = 4 \cdot 10^{10} của cách ngây thơ.

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

const ll N = 2e5 + 5;
ll n, q, a[N], bit[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

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

    cin >> n >> q;
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
        bit[i] = a[i];
    }
    for (ll i = 1; i <= n; i++)
        if (i + (i & -i) <= n) bit[i + (i & -i)] += bit[i];
    while (q--) {
        ll t, x, y;
        cin >> t >> x >> y;
        if (t == 1) {
            update(x, y - a[x]);
            a[x] = y;
        } else cout << get(y) - get(x - 1) << '\n';
    }
}
5 6
2 1 4 3 5
2 2 4
1 3 9
2 1 3
1 1 10
2 1 5
2 4 4

Cộng đoạn: mảng hiệu đặt vào cây

Đổi bài toán: thao tác cộng vv vào mọi al..ara_l..a_r, và câu hỏi là giá trị một phần tử aka_k. Dùng mảng hiệu của bài 02, di=ai−ai−1d_i = a_i - a_{i-1} với a0=0a_0 = 0. Cộng đoạn chỉ đổi hai ô, dl+=vd_l \mathrel{+}= v và dr+1−=vd_{r+1} \mathrel{-}= v, còn ak=d1+⋯+dka_k = d_1 + \dots + d_k là một get. Ở bài 02 ta phải đọc hết thao tác rồi cộng dồn một lần; đặt dd vào cây Fenwick thì hỏi aka_k lúc nào cũng được: update(l, v); update(r + 1, -v); rồi aka_k = get(k). Khi r=nr = n, ô n+1n + 1 nằm ngoài cây và vòng lặp tự không chạy.

Hỏi nhanh

Cây Fenwick giữ mảng hiệu dd và aka_k được tính bằng get(k). Thao tác “cộng vv vào al,…,ara_l, \dots, a_r” cần hai lệnh update nào?

Còn khi cần tổng đoạn sau những lần cộng đoạn thì sao? Thay ai=∑j≤idja_i = \sum_{j \le i} d_j vào tổng S(x)=a1+⋯+axS(x) = a_1 + \dots + a_x:

S(x)=∑i=1x∑j=1idj=∑j=1x(x−j+1) dj=(x+1)∑j=1xdj−∑j=1xj dj.S(x) = \sum_{i=1}^{x} \sum_{j=1}^{i} d_j = \sum_{j=1}^{x} (x - j + 1)\, d_j = (x + 1) \sum_{j=1}^{x} d_j - \sum_{j=1}^{x} j\, d_j .

Đổi thứ tự lấy tổng vì djd_j góp mặt trong aj,…,axa_j, \dots, a_x, tức x−j+1x - j + 1 lần. Chẳng hạn với x=3x = 3: S(3)=d1+(d1+d2)+(d1+d2+d3)=3d1+2d2+d3S(3) = d_1 + (d_1 + d_2) + (d_1 + d_2 + d_3) = 3d_1 + 2d_2 + d_3, đúng bằng 4(d1+d2+d3)−(1⋅d1+2⋅d2+3⋅d3)4(d_1 + d_2 + d_3) - (1 \cdot d_1 + 2 \cdot d_2 + 3 \cdot d_3). Hai tổng cuối là hai tiền tố: một trên cây B1B_1 giữ djd_j, một trên cây B2B_2 giữ j⋅djj \cdot d_j. Mỗi lần cộng đoạn sửa cả hai cây tại ll và r+1r + 1, và tổng đoạn là S(r)−S(l−1)S(r) - S(l-1).

Ví dụCộng Đoạn, Tính Tổng

Đề. Có T≤10T \le 10 test, mỗi test một mảng n≤105n \le 10^5 phần tử ban đầu toàn 0 và q≤105q \le 10^5 thao tác: 0 l r v cộng vv vào al..ara_l..a_r; 1 l r in al+⋯+ara_l + \dots + a_r.

Lời giải. Dùng hai cây như ở trên: cộng vv vào đoạn [l,r][l, r] thành bốn lần cập nhật điểm, và tổng tiền tố a1+⋯+axa_1 + \dots + a_x bằng (x+1) B1(x)−B2(x)(x + 1)\,B_1(x) - B_2(x).

Bước 1: một hàm cập nhật cho cả hai cây. Hai cây là hai mảng b1, b2; truyền mảng làm tham số để khỏi viết hai bản update, get giống hệt nhau.

C++
void update(ll b[], ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) b[pos] += val;
}

ll get(ll b[], ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += b[pos];
    return res;
}

Bước 2: cộng đoạn và tổng tiền tố. add(l, r, v) ghi +v+v tại ll và −v-v tại r+1r + 1 vào cây hiệu B1B_1, đồng thời ghi v⋅lv \cdot l và −v⋅(r+1)-v \cdot (r + 1) vào B2B_2. pre(x) ghép hai cây theo công thức trên; tổng đoạn là pre(r) - pre(l - 1).

C++
void add(ll l, ll r, ll v) {
    update(b1, l, v);
    update(b2, l, v * l);
    update(b1, r + 1, -v);
    update(b2, r + 1, -v * (r + 1));
}

ll pre(ll x) {
    return (x + 1) * get(b1, x) - get(b2, x);
}

Bước 3: xóa cây giữa các test. Hai cây là mảng toàn cục, nên dữ liệu của test trước còn nằm đó. Xóa n+1n + 1 ô đầu là đủ, vì test này chỉ đụng tới các ô 1..n1..n (ô r+1=n+1r + 1 = n + 1 nằm ngoài cây và vòng update không ghi vào).

C++
for (ll i = 0; i <= n; i++) b1[i] = b2[i] = 0;

Tổng tới cỡ 105⋅105⋅107=101710^5 \cdot 10^5 \cdot 10^7 = 10^{17}, vừa ll. Với test đầu, sau hai lần cộng mảng là (2,5,3,3,0)(2, 5, 3, 3, 0) và tổng [2,5][2, 5] bằng 11.

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

const ll N = 1e5 + 5;
ll t, n, q, b1[N], b2[N];

void update(ll b[], ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) b[pos] += val;
}

ll get(ll b[], ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += b[pos];
    return res;
}

void add(ll l, ll r, ll v) {
    update(b1, l, v);
    update(b2, l, v * l);
    update(b1, r + 1, -v);
    update(b2, r + 1, -v * (r + 1));
}

ll pre(ll x) {
    return (x + 1) * get(b1, x) - get(b2, x);
}

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

    cin >> t;
    while (t--) {
        cin >> n >> q;
        for (ll i = 0; i <= n; i++) b1[i] = b2[i] = 0;
        while (q--) {
            ll type, l, r;
            cin >> type >> l >> r;
            if (type == 0) {
                ll v;
                cin >> v;
                add(l, r, v);
            } else cout << pre(r) - pre(l - 1) << '\n';
        }
    }
}
2
5 5
1 1 5
0 2 4 3
1 1 3
0 1 2 2
1 2 5
4 4
0 1 4 1
0 2 3 4
1 1 4
1 2 2

Mỗi thao tác tốn bốn lần cập nhật hoặc bốn lần truy vấn, mỗi lần O(log⁡n)O(\log n).

Fenwick đếm: chỉ số là giá trị

Từ đầu bài, chỉ số là vị trí. Ta cũng có thể lấy chỉ số là giá trị: ô của giá trị vv đếm số phần tử bằng vv đang có trong một đa tập. Chèn vv là update(v, 1), xóa là update(v, -1), và get(v) là số phần tử ≤v\le v. Mọi câu hỏi kiểu “đã thấy bao nhiêu phần tử nhỏ hơn, lớn hơn, hay nằm trong khoảng nào” đều thành một hay hai lần get. Đây là cách dùng phổ biến nhất của cây trong đề thi.

Giá trị tới 10910^9 thì không thể dành 10910^9 ô. Nếu biết trước mọi giá trị có thể xuất hiện (đọc hết input trước, kể cả giá trị trong các lệnh đổi), ta nén tọa độ như ở bài 03: sắp xếp, bỏ trùng thành mảng vals, rồi thay mỗi giá trị bằng hạng 1..m1..m của nó, lower_bound(vals.begin(), vals.end(), x) - vals.begin() + 1. Nén giữ nguyên thứ tự nên mọi phép so sánh << và ≤\le vẫn đúng.

Phần tử thứ kk. Bài toán ngược: biết kk, tìm giá trị nhỏ thứ kk của đa tập, tức chỉ số nhỏ nhất pp có get(p)≥k\mathrm{get}(p) \ge k. Chặt nhị phân trên pp rồi gọi get tốn O(log⁡n⋅log⁡n)O(\log n \cdot \log n); đi thẳng xuống trong cây chỉ O(log⁡n)O(\log n). Cho pp chạy từ 0; với 2e2^e lần lượt là lũy thừa của 2 lớn nhất không vượt nn, rồi giảm dần tới 1: nếu p+2e≤np + 2^e \le n và Tp+2e<kT_{p + 2^e} < k thì nhảy p+=2ep \mathrel{+}= 2^e và trừ TpT_p khỏi kk. Cuối cùng đáp án là p+1p + 1. Khi xét bước 2e2^e, pp là bội của 2e+12^{e+1} nên khúc của ô p+2ep + 2^e đúng là (p,p+2e](p, p + 2^e], và đây là chặt nhị phân trên từng bit của đáp án. Cách này cần giá trị trong cây không âm.

Mỗi bước thử nhảy 8, 4, 2, 1 ô; nhảy khi cả khúc vẫn chưa đủ k phần tử.

Chương trình dưới giữ một đa tập các số trong [1,n][1, n]: + x chèn xx, ? x in phần tử nhỏ thứ xx, - x xóa phần tử nhỏ thứ xx và in giá trị bị xóa.

C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5;
ll n, q, bit[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] += val;
}

ll kth(ll k) {
    ll p = 0;
    for (ll e = __lg(n); e >= 0; e--)
        if (p + (1LL << e) <= n && bit[p + (1LL << e)] < k) {
            p += 1LL << e;
            k -= bit[p];
        }
    return p + 1;
}

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

    cin >> n >> q;
    while (q--) {
        char op;
        ll x;
        cin >> op >> x;
        if (op == '+') update(x, 1);
        else if (op == '?') cout << kth(x) << '\n';
        else {
            ll v = kth(x);
            cout << v << '\n';
            update(v, -1);
        }
    }
}
10 9
+ 7
+ 3
+ 7
+ 5
? 2
? 3
- 2
? 2
? 3

Đa tập có {3,5,7,7}\{3, 5, 7, 7\} nên phần tử nhỏ thứ 2 và thứ 3 là 5 và 7. Lệnh - 2 xóa số 5, còn {3,7,7}\{3, 7, 7\} nên hai câu hỏi sau cùng cho 7.

Ví dụNhững Cặp Đảo Thứ Tự

Đề. Đếm số cặp u<vu < v có au>ava_u > a_v trong dãy n≤6⋅104n \le 6 \cdot 10^4 số nguyên 1≤ai≤6⋅1041 \le a_i \le 6 \cdot 10^4; hai số bằng nhau không tạo nghịch thế.

Lời giải. Mỗi nghịch thế có đúng một đầu phải, nên đếm theo đầu phải. Quét ii từ trái sang và giữ trong cây Fenwick, theo giá trị, các phần tử đã quét. Khi tới phần tử thứ ii (đếm từ 0), cây chứa đúng ii phần tử đứng trước, trong đó get(a[i]) phần tử không lớn hơn aia_i, nên i−get(ai)i - \mathrm{get}(a_i) phần tử lớn hơn hẳn. Phải hỏi trước khi thêm chính aia_i, nếu không aia_i tự đếm mình. Giá trị chỉ tới 6⋅1046 \cdot 10^4 nên dùng thẳng làm chỉ số; giá trị tới 10910^9 thì nén trước. Với ví dụ 4,1,3,2,5,24, 1, 3, 2, 5, 2: các lượt cộng 0,1,1,2,0,30, 1, 1, 2, 0, 3, tổng 7; số 2 cuối không đếm số 2 đứng trước vì bằng nó.

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

const ll M = 6e4;
ll n, bit[M + 5];

void update(ll pos, ll val) {
    for (; pos <= M; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

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

    cin >> n;
    ll ans = 0;
    for (ll i = 0; i < n; i++) {
        ll x;
        cin >> x;
        ans += i - get(x);
        update(x, 1);
    }
    cout << ans << '\n';
}
6
4
1
3
2
5
2

Hỏi nhanh

Một bạn viết ans += i - get(x - 1) thay cho i - get(x). Với dãy 2,2,22, 2, 2, chương trình in ra bao nhiêu, và đáp án đúng của đề là bao nhiêu?

Khi nào Fenwick không đủ

Công thức get(r)−get(l−1)\mathrm{get}(r) - \mathrm{get}(l-1) cần phép toán có phép ngược, nên cây hợp với cộng, XOR và đếm, giống điều kiện của tổng tiền tố ở bài 02. Với min và max thì không: min⁡(a1..ar)\min(a_1..a_r) không cho biết min⁡(al..ar)\min(a_l..a_r).

Có một ngoại lệ hữu ích. Nếu mọi cập nhật chỉ làm giá trị tăng lên và ta chỉ hỏi tiền tố, thay phép cộng bằng max: bit[pos] = max(bit[pos], val) trong vòng cập nhật, và lấy max trong vòng truy vấn. Đây là cách giải nhiều bài quy hoạch động theo giá trị. Nhưng nếu giá trị có thể giảm đi, hoặc cần max trên đoạn [l,r][l, r] bất kỳ, hoặc gán cả một đoạn, thì phải dùng cây phân đoạn (bài 17).

Cây có bản hai chiều bằng hai vòng lặp lồng nhau, mỗi thao tác O(log⁡n⋅log⁡n)O(\log n \cdot \log n) (bài 45). Với nhiều test, đặt lại n+1n + 1 ô cho mỗi test, nên chỉ ổn khi tổng nn qua các test bị chặn. So với cây phân đoạn, Fenwick ngắn hơn và thường chạy nhanh hơn, đổi lại nó chỉ làm được các việc kể trên.

Ví dụDãy Tăng Có Trọng Số

Đề. Có n≤2⋅105n \le 2 \cdot 10^5 phần tử, phần tử ii có chiều cao hih_i (một hoán vị của 1..n1..n) và trọng số bi≤109b_i \le 10^9. Chọn một dãy con có chiều cao tăng nghiêm ngặt với tổng trọng số lớn nhất.

Lời giải. Quy hoạch động như dãy con tăng của bài 12: dpi=bi+max⁡dpj\mathrm{dp}_i = b_i + \max \mathrm{dp}_j trên các j<ij < i có hj<hih_j < h_i, nhưng tìm max kiểu duyệt mọi jj là O(n2)O(n^2). Quét ii tăng dần và giữ một cây Fenwick max theo chiều cao: ô của chiều cao hh ghi dp\mathrm{dp} của phần tử đã quét có chiều cao đó. Khi tới ii, các phần tử đã quét đều đứng trước ii, nên max⁡dpj\max \mathrm{dp}_j cần tìm là max tiền tố trên chiều cao hi−1h_i - 1. Mỗi ô chỉ được ghi một lần và giá trị trong cây chỉ tăng, đúng ngoại lệ cho phép dùng max. Cả bài O(nlog⁡n)O(n \log n); tổng tới 2⋅10142 \cdot 10^{14}.

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

const ll N = 2e5 + 5;
ll n, h[N], b[N], bit[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] = max(bit[pos], val);
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res = max(res, bit[pos]);
    return res;
}

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

    cin >> n;
    for (ll i = 0; i < n; i++) cin >> h[i];
    for (ll i = 0; i < n; i++) cin >> b[i];
    ll ans = 0;
    for (ll i = 0; i < n; i++) {
        ll dp = get(h[i] - 1) + b[i];
        update(h[i], dp);
        ans = max(ans, dp);
    }
    cout << ans << '\n';
}
6
4 2 6 1 5 3
7 4 8 5 6 10

Khi đi thi

  • Dãy bị sửa xen kẽ với các câu hỏi tổng hay đếm trên tiền tố, đoạn là dấu hiệu của Fenwick; dãy đứng yên chỉ cần tổng tiền tố (bài 02); cần min, max trên đoạn bất kỳ hay gán cả đoạn thì chuyển sang cây phân đoạn (bài 17).
  • Cài bằng chỉ số từ 1: đề cho từ 0 thì cộng 1 trước khi gọi update (vì update(0, ·) lặp vô hạn), dùng long long cho tổng, đổi phép gán thành cộng phần chênh lệch, và lấy tổng đoạn bằng get(r) - get(l - 1).
  • Bài đếm cặp (nghịch thế, “bao nhiêu phần tử trước đó nhỏ hơn hay lớn hơn”): quét theo một khóa, dùng Fenwick theo giá trị đã nén của khóa kia, hỏi trước khi thêm phần tử hiện tại và kiểm kỹ chỗ nào là <<, chỗ nào là ≤\le khi có giá trị trùng.

Tóm tắt

  • TiT_i là tổng của lowbit(i)\mathrm{lowbit}(i) phần tử kết thúc tại ii, với lowbit(i)\mathrm{lowbit}(i) = i & -i; hai khúc bất kỳ rời nhau hoặc lồng nhau.
  • get(pos): cộng TposT_{pos} rồi pos−=lowbit(pos)pos \mathrel{-}= \mathrm{lowbit}(pos); update(pos, val): sửa TposT_{pos} rồi pos+=lowbit(pos)pos \mathrel{+}= \mathrm{lowbit}(pos). Mỗi vòng O(log⁡n)O(\log n), tổng đoạn là hiệu hai get.
  • Phép gán đổi thành cộng phần chênh lệch; dựng cây trong O(n)O(n) bằng cách đẩy mỗi TiT_i lên Ti+lowbit(i)T_{i + \mathrm{lowbit}(i)}.
  • Cộng đoạn dùng mảng hiệu trên cây; cộng đoạn kèm hỏi tổng đoạn cần hai cây, với S(x)=(x+1)∑j≤xdj−∑j≤xj djS(x) = (x+1) \sum_{j \le x} d_j - \sum_{j \le x} j\, d_j.
  • Lấy chỉ số là giá trị thì cây đếm được nghịch thế và xếp hạng, nén tọa độ khi giá trị lớn, và nhảy nhị phân xuống cây tìm phần tử thứ kk trong O(log⁡n)O(\log n).
  • Cây cần phép ngược: min và max chỉ dùng được cho tiền tố khi giá trị không giảm.

Bài tập

1
Cộng Đoạn, Hỏi Điểmcanuc80k1602Mảng hiệu đặt vào Fenwick: cộng uu tại ll, trừ uu tại r+1r + 1, giá trị aka_k là một get.
LQDOJ
2
Khoảng Mức Hiện Tạicanuc80k1604Fenwick đếm theo mức; nén cả các mức ban đầu lẫn các giá trị trong lệnh đổi, mỗi lệnh đổi là một update(-1) rồi một update(+1).
LQDOJ
3
Xóa Theo Thứ Hạngcanuc80k1605Đánh dấu 1 ở các phần tử còn lại; phần tử đứng thứ pp tìm bằng nhảy nhị phân xuống cây, rồi trừ 1 tại vị trí đó.
LQDOJ
4
Kho Số Theo Thứ Tựcanuc80k1612Fenwick đếm trên giá trị 1..n1..n; xóa phần tử nhỏ thứ kk là nhảy nhị phân xuống cây rồi trừ 1; dữ liệu tới 2⋅1062 \cdot 10^6 số nên đọc nhanh.
LQDOJ
5
Chuỗi Xen Kẽ Sau Khi Đảocanuc80k1611Đảo cả đoạn chỉ làm đổi trạng thái đúng hai biên (l−1,ll - 1, l và r,r+1r, r + 1); Fenwick lưu các biên có hai ký tự giống nhau.
LQDOJ
6
Số Đoạn Nằm Bên Trongcanuc80k1623Sắp theo đầu trái giảm dần rồi quét; đếm các đầu phải đã thấy nhỏ hơn rir_i trên Fenwick của đầu phải đã nén.
LQDOJ
7
Nghịch Thế Sau Mỗi Lần Xoaycanuc80k1618Đếm nghịch thế một lần bằng Fenwick, rồi mỗi lần chuyển phần tử đầu xx xuống cuối thì kết quả đổi đúng (n−1−x)−x(n - 1 - x) - x.
LQDOJ
8
Bảng Chữ Cái Trên Đoạncanuc80k161326 cây, mỗi cây đánh dấu vị trí của một chữ cái; đổi ký tự là xóa dấu ở cây cũ và thêm dấu ở cây mới.
LQDOJ
9
Lớn Hơn Ngưỡngcanuc80k1607Offline: sắp phần tử và truy vấn theo giá trị giảm dần, thêm dấu 1 ở vị trí các phần tử lớn hơn kk trước khi trả lời truy vấn đó.
LQDOJ
10
Bao Nhiêu Giá Trị Khác Nhaucanuc80k1606Offline: sắp truy vấn theo rr, quét mảng và chỉ giữ dấu 1 ở lần xuất hiện cuối của mỗi giá trị.
LQDOJ
11
Bộ Ba Giảm Nghiêm Ngặtcanuc80k1624Với mỗi jj, số phần tử lớn hơn đứng trước nhân số phần tử nhỏ hơn đứng sau; hai lượt quét Fenwick trên giá trị đã nén.
LQDOJ
12
Đếm Dãy Con Tăngcanuc80k1619Cùng khuôn quét rồi hỏi tiền tố, nhưng dpidp_i là số dãy con tăng kết thúc tại ii và lấy tổng theo modulo trên các giá trị nhỏ hơn hẳn; nén giá trị.
LQDOJ
13
Khoảng Cách Giữa Các Điểm Không Gặp Nhaucanuc80k1630Sắp theo tọa độ, mỗi điểm đóng góp x⋅c−sx \cdot c - s với cc, ss là số lượng và tổng tọa độ các điểm trước có vận tốc không lớn hơn; hai Fenwick.
LQDOJ
14
Trung Vị Của Mỗi Cửa Sổcanuc80k1620Fenwick tần suất trên giá trị đã nén, trượt cửa sổ bằng một lần thêm và một lần xóa; trung vị là phần tử nhỏ thứ (k+1)/2(k + 1)/2.
LQDOJ
15
Đếm Đoạn Có Tổng Nhỏ Hơn Ngưỡngcanuc80k1628Đoạn (i,j](i, j] có tổng nhỏ hơn tt khi Pi>Pj−tP_i > P_j - t; nén các tổng tiền tố và quét jj, đếm các PiP_i đứng trước lớn hơn Pj−tP_j - t.
LQDOJ

Đọc thêm

  • Cây chỉ số nhị phân (Binary Indexed Tree)VNOI WikiBài tiếng Việt đi từ cách cộng dồn ngây thơ tới cây Fenwick và nêu rõ giới hạn: phép toán phải có phép ngược.
  • Fenwick Treecp-algorithmsCách đánh số từ 0 bằng i&(i+1)i \mathbin{\&} (i+1), dựng O(n)O(n), cộng đoạn kèm hỏi đoạn bằng hai cây và bản hai chiều.
  • 树状数组OI WikiTrình bày đầy đủ nhất: cây quản lý, độ phức tạp, cây trọng số tìm phần tử thứ kk, đếm nghịch thế, mở rộng cho max.
  • Point Update Range SumUSACO GuideBộ bài tập chọn lọc từ CSES, USACO, Kattis, SPOJ kèm mức độ khó, dùng được cho cả Fenwick lẫn cây phân đoạn.
  • Fenwick TreesSOI wikiLưu ý về chỉ số 0, cộng đoạn bằng hàm tuyến tính, tìm kiếm nhị phân trên cây và vì sao max chỉ dùng được khi giá trị không giảm.