Chương 1·Bài 16

Fenwick Tree

Cho một dãy số, các 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) sửa một phần tử và hỏi một tiền tố đều trong O(log⁡n)O(\log n), khoảng 18 bước. Nó chỉ là một mảng n+1n + 1 số cùng hai vòng lặp ba dòng, nhưng cách 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 bài nghịch thế, đếm cặp, xếp hạng, nên bài dạy cả phần đếm đó.

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 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}.

Trong hình, mỗi thanh TiT_i dài lowbit(i)\mathrm{lowbit}(i) và kết thúc ở cột ii. Chỉnh pos rồi bấm get hoặc update.

Các khúc của cây Fenwick.

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.

Hình dưới là get(13): ba khúc nối khít nhau, đúng ba bit 1 của 13.

Truy vấn ghép các khúc theo bit.

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.

Hình dưới là update(5, 3): sửa mọi thanh chứa cột 5, nhảy theo ii cộng lowbit(i)\mathrm{lowbit}(i).

Cập nhật nhảy lên các khúc phủ vị trí.

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;
#define 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 và dựng cây

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

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

Độ phức tạp: 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

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.

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

Tìm phần tử thứ kk bằng nhảy bit.

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;
#define 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;
#define 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à nhanh hơn, đổi lại nó chỉ làm được các việc kể trên.

Một triệu cập nhật và một triệu truy vấn tổng đoạn, n = 500 nghìn, trung vị 5 lần chạy, i7-10850H, g++ -O2.cây phân đoạn đệ quy670 ms · gấp 7,2 lầncây Fenwick94 ms
Một triệu cập nhật và một triệu truy vấn tổng đoạn trên 500 nghìn phần tử, đo trên i7-10850H, g++ -O2.

Cây Fenwick nhanh hơn cây phân đoạn đệ quy khoảng 7 lần cho cùng công việc. Quy tắc: bài chỉ cần cộng và tổng thì chọn Fenwick.

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.

Độ phức tạp: 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;
#define 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âu hỏi tổng hay đếm trên tiền tố, đoạn: 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.
  • Bài đếm cặp (nghịch thế, “bao nhiêu phần tử trước đó nhỏ hơn”): quét theo một khóa, 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

Đọc thêm