Chương 4·Bài 45

2D Fenwick Tree

Một lưới 1000×10001000 \times 1000, liên tục có lệnh cộng thêm vào một ô xen với câu hỏi tổng một hình chữ nhật. Tổng tiền tố hai chiều (bài 02) trả lời câu hỏi rất nhanh nhưng mỗi lần sửa phải dựng lại cả bảng; cộng thẳng vào ô thì mỗi câu hỏi lại phải duyệt cả hình.

Fenwick (bài 16) đã giải đúng bài này trên một dãy. Trên lưới, ta lồng hai Fenwick: vòng ngoài nhảy theo hàng, vòng trong nhảy theo cột, mỗi thao tác O(log⁡nlog⁡m)O(\log n \log m). Thêm mảng hiệu bốn góc thì cập nhật được cả hình chữ nhật; thêm vài cây theo tính chẵn lẻ của tọa độ thì vừa cập nhật vừa hỏi hình chữ nhật.

Fenwick lồng nhau

bit[i][j] giữ tổng các ô (x,y)(x, y) với xx trong khối của ii và yy trong khối của jj, khối theo nghĩa của Fenwick một chiều: từ i−lowbit(i)+1i - \mathrm{lowbit}(i) + 1 tới ii. Cộng vào ô (x,y)(x, y): nhảy i=x,x+lowbit(x),…i = x, x + \mathrm{lowbit}(x), \dots và trong mỗi ii nhảy j=y,y+lowbit(y),…j = y, y + \mathrm{lowbit}(y), \dots. Tổng tiền tố P(x,y)P(x, y) của hình chữ nhật [1..x]×[1..y][1..x] \times [1..y] đi ngược lại. Tổng hình chữ nhật [x1..x2]×[y1..y2][x_1..x_2] \times [y_1..y_2] theo bao hàm loại trừ:

P(x2,y2)−P(x1−1,y2)−P(x2,y1−1)+P(x1−1,y1−1).P(x_2, y_2) - P(x_1 - 1, y_2) - P(x_2, y_1 - 1) + P(x_1 - 1, y_1 - 1).
Bấm một ô để cộng v vào nó và xem khối mà ô (i, j) của cây giữ; chuyển sang Hỏi hình chữ nhật rồi kéo chuột để chọn.
C++
void update(ll x, ll y, ll val) {
    for (ll i = x; i <= n; i += i & -i)
        for (ll j = y; j <= n; j += j & -j) bit[i][j] += val;
}

ll get(ll x, ll y) {
    ll res = 0;
    for (ll i = x; i > 0; i -= i & -i)
        for (ll j = y; j > 0; j -= j & -j) res += bit[i][j];
    return res;
}

Bộ nhớ O(nm)O(nm), nên chỉ hợp khi lưới vừa bộ nhớ (cỡ 2000×20002000 \times 2000 số ll). Tọa độ bắt đầu từ 0 thì dịch thêm 1 trước khi vào cây. Đổi giá trị một ô từ uu sang vv là cộng chênh lệch v−uv - u, nên cần giữ thêm mảng giá trị hiện tại.

Lưới ban đầu đã có số thì đừng gọi update cho từng ô. Chép lưới vào bit, rồi đẩy mỗi ô sang ô cha của nó theo cột, sau đó theo hàng, giống cách dựng Fenwick một chiều trong O(n)O(n):

C++
for (ll i = 1; i <= n; i++)
    for (ll j = 1; j <= n; j++) {
        ll k = j + (j & -j);
        if (k <= n) bit[i][k] += bit[i][j];
    }
for (ll i = 1; i <= n; i++) {
    ll k = i + (i & -i);
    if (k <= n)
        for (ll j = 1; j <= n; j++) bit[k][j] += bit[i][j];
}
Dựng Fenwick hai chiều cho lưới 2000 × 2000, trung vị 5 lần chạy, i7-10850H, g++ -O2.cộng từng ô bằng update115 ms · gấp 4,4 lầndựng tuyến tính26 ms
Dựng Fenwick hai chiều cho lưới 2000 × 2000, cộng từng ô bằng update và dựng tuyến tính, đo trên i7-10850H, g++ -O2.

Dựng tuyến tính nhanh gấp hơn 4 lần, vì mỗi ô chỉ được cộng hai lần thay cho cỡ log⁡2n≈120\log^2 n \approx 120 lần.

Ví dụTổng Trên Lưới

Đề. Bảng S×SS \times S với S≤1024S \le 1024, ban đầu toàn 0, tọa độ từ 0. Lệnh cộng AA vào một ô và lệnh hỏi tổng một hình chữ nhật; dừng ở lệnh 3.

Lời giải. Đúng hai hàm update, get ở trên. Tổng hình chữ nhật là tiền tố góc phải trên trừ hai dải bên trái và bên dưới, cộng lại góc bị trừ hai lần. Tọa độ đề từ 0 mà Fenwick không có ô 0, nên mọi tọa độ cộng 1.

n được đọc ở lệnh 0, trước mọi lệnh khác.

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

const ll N = 1030;
ll n, bit[N][N];

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

ll get(ll x, ll y) {
    ll res = 0;
    for (ll i = x; i > 0; i -= i & -i)
        for (ll j = y; j > 0; j -= j & -j) res += bit[i][j];
    return res;
}

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

    ll t;
    while (cin >> t && t != 3) {
        if (t == 0) cin >> n;
        else if (t == 1) {
            ll x, y, a;
            cin >> x >> y >> a;
            update(x + 1, y + 1, a);
        } else {
            ll l, b, r, tp;
            cin >> l >> b >> r >> tp;
            l++, b++, r++, tp++;
            cout << get(r, tp) - get(l - 1, tp) - get(r, b - 1) + get(l - 1, b - 1) << '\n';
        }
    }
}
0 4
1 0 0 5
1 2 3 7
2 0 0 2 3
1 0 0 -2
2 0 0 0 0
3

Độ phức tạp: mỗi lệnh O(log⁡2S)≈100O(\log^2 S) \approx 100 bước.

Cộng hình, hỏi một ô

Mảng hiệu một chiều cộng vv vào đoạn [l,r][l, r] bằng +v+v tại ll và −v-v tại r+1r + 1; giá trị một điểm là tổng tiền tố. Hai chiều làm tương tự với bốn góc: cộng vv vào hình chữ nhật [x1..x2]×[y1..y2][x_1..x_2] \times [y_1..y_2] là +v+v tại (x1,y1)(x_1, y_1), −v-v tại (x1,y2+1)(x_1, y_2 + 1) và (x2+1,y1)(x_2 + 1, y_1), +v+v tại (x2+1,y2+1)(x_2 + 1, y_2 + 1). Giá trị ô (x,y)(x, y) là tổng tiền tố P(x,y)P(x, y) của mảng hiệu, đúng một câu hỏi Fenwick hai chiều. Với phép lật bit, cộng thay bằng XOR và mọi dấu trừ đều là XOR.

Hỏi nhanh

Cộng vv vào hình chữ nhật [2..3]×[2..4][2..3] \times [2..4] bằng mảng hiệu bốn góc. Ô (4,3)(4, 3) nhận được những dấu nào trong tổng tiền tố của nó?

Ví dụLật Hình Chữ Nhật

Đề. Nhiều test; mỗi test ma trận nhị phân n×nn \times n toàn 0, n≤1000n \le 1000. Lệnh C lật mọi bit trong một hình chữ nhật, lệnh Q in bit tại một ô. In một dòng trống giữa kết quả hai test.

Lời giải. Lật bit là XOR 1. Giữ mảng hiệu hai chiều trong một Fenwick XOR: giá trị một ô là XOR tiền tố của mảng hiệu.

Bước 1: Fenwick XOR. Như Fenwick cộng, mọi phép cộng thành XOR.

C++
void update(ll x, ll y) {
    for (ll i = x; i <= n; i += i & -i)
        for (ll j = y; j <= n; j += j & -j) bit[i][j] ^= 1;
}

Bước 2: lật hình chữ nhật. XOR 1 tại bốn góc của mảng hiệu. Góc ở hàng hay cột n+1n + 1 bị vòng lặp bỏ qua, không sao vì chúng chỉ ảnh hưởng các ô ngoài bảng.

C++
update(x1, y1);
update(x1, y2 + 1);
update(x2 + 1, y1);
update(x2 + 1, y2 + 1);

Bước 3: nhiều test. Mỗi test xóa đúng n×nn \times n ô đã dùng, không cần xóa cả mảng. In dòng trống trước mọi test trừ test đầu, tức giữa hai test liền nhau.

C++
for (ll i = 1; i <= n; i++)
    for (ll j = 1; j <= n; j++) bit[i][j] = 0;
if (tc > 1) cout << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1005;
ll n, bit[N][N];

void update(ll x, ll y) {
    for (ll i = x; i <= n; i += i & -i)
        for (ll j = y; j <= n; j += j & -j) bit[i][j] ^= 1;
}

ll get(ll x, ll y) {
    ll res = 0;
    for (ll i = x; i > 0; i -= i & -i)
        for (ll j = y; j > 0; j -= j & -j) res ^= bit[i][j];
    return res;
}

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

    ll T;
    cin >> T;
    for (ll tc = 1; tc <= T; tc++) {
        ll q;
        cin >> n >> q;
        for (ll i = 1; i <= n; i++)
            for (ll j = 1; j <= n; j++) bit[i][j] = 0;
        if (tc > 1) cout << '\n';
        while (q--) {
            char op;
            cin >> op;
            if (op == 'C') {
                ll x1, y1, x2, y2;
                cin >> x1 >> y1 >> x2 >> y2;
                update(x1, y1);
                update(x1, y2 + 1);
                update(x2 + 1, y1);
                update(x2 + 1, y2 + 1);
            } else {
                ll x, y;
                cin >> x >> y;
                cout << get(x, y) << '\n';
            }
        }
    }
}
2
3 3
C 1 1 2 2
Q 1 1
Q 3 3
2 2
C 1 2 2 2
Q 2 2

Độ phức tạp: mỗi lệnh O(log⁡2n)O(\log^2 n), mỗi test thêm O(n2)O(n^2) để xóa.

Cộng hình, hỏi hình

Muốn cả cập nhật hình chữ nhật lẫn hỏi tổng hình chữ nhật, tổng tiền tố của mảng giá trị phải tính từ mảng hiệu dd: P(x,y)=∑i≤x,j≤ydij⋅(x−i+1)(y−j+1)P(x, y) = \sum_{i \le x, j \le y} d_{ij} \cdot (x - i + 1)(y - j + 1). Khai triển tích ra bốn số hạng, mỗi số hạng là một Fenwick hai chiều riêng (dd, d⋅id \cdot i, d⋅jd \cdot j, d⋅ijd \cdot ij). Với XOR, tích (x−i+1)(y−j+1)(x - i + 1)(y - j + 1) chỉ cần tính chẵn lẻ: dijd_{ij} góp vào P(x,y)P(x, y) khi và chỉ khi x−ix - i và y−jy - j đều chẵn, tức ii cùng tính chẵn lẻ với xx và jj cùng với yy. Vậy giữ bốn cây theo (i mod 2,j mod 2)(i \bmod 2, j \bmod 2); cập nhật góc (i,j)(i, j) vào cây của nó, hỏi tiền tố (x,y)(x, y) chỉ đọc cây (x mod 2,y mod 2)(x \bmod 2, y \bmod 2).

Hỏi nhanh

Trong mảng hiệu XOR, góc (i,j)=(2,3)(i, j) = (2, 3) có góp vào XOR tiền tố của ô (x,y)=(5,4)(x, y) = (5, 4) trên mảng giá trị không?

Ví dụXOR Trên Ma Trận

Đề. Ma trận n×nn \times n với n≤1000n \le 1000, ban đầu 0. m≤105m \le 10^5 thao tác: XOR v<262v < 2^{62} vào mọi ô một hình chữ nhật, hoặc in XOR mọi ô một hình chữ nhật.

Lời giải. Cập nhật hình chữ nhật là XOR vv tại bốn góc của mảng hiệu, câu hỏi hình chữ nhật là XOR bốn tiền tố như bao hàm loại trừ. Khó là phải lấy XOR tiền tố của XOR tiền tố. Góc (i,j)(i, j) của mảng hiệu góp vào XOR tiền tố tại (x,y)(x, y) đúng (x−i+1)(y−j+1)(x - i + 1)(y - j + 1) lần, và lặp chẵn lần thì triệt tiêu. Vậy chỉ những góc có i≡xi \equiv x và j≡y(mod2)j \equiv y \pmod 2 còn lại: chia mảng hiệu thành bốn cây Fenwick theo tính chẵn lẻ của tọa độ.

Bước 1: chọn cây. bit[x & 1][y & 1] là cây của góc (x,y)(x, y). Cả cập nhật lẫn câu hỏi chọn cây theo chính tọa độ gốc (x,y)(x, y), không theo các nút (i,j)(i, j) đi qua.

C++
void update(ll x, ll y, ll v) {
    for (ll i = x; i <= n; i += i & -i)
        for (ll j = y; j <= n; j += j & -j) bit[x & 1][y & 1][i][j] ^= v;
}

Bước 2: cập nhật và câu hỏi. Cập nhật XOR vv tại bốn góc; câu hỏi XOR bốn tiền tố, mọi dấu trừ thành XOR.

C++
if (t == 1) cout << (get(x1, y1) ^ get(x0 - 1, y1) ^ get(x1, y0 - 1) ^ get(x0 - 1, y0 - 1)) << '\n';
else {
    ll v;
    cin >> v;
    update(x0, y0, v);
    update(x0, y1 + 1, v);
    update(x1 + 1, y0, v);
    update(x1 + 1, y1 + 1, v);
}

Giá trị dưới 2622^{62} nên dùng long long; bốn cây 100521005^2 số khoảng 32 MB.

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

const ll N = 1005;
ll n, m, bit[2][2][N][N];

void update(ll x, ll y, ll v) {
    for (ll i = x; i <= n; i += i & -i)
        for (ll j = y; j <= n; j += j & -j) bit[x & 1][y & 1][i][j] ^= v;
}

ll get(ll x, ll y) {
    ll res = 0;
    for (ll i = x; i > 0; i -= i & -i)
        for (ll j = y; j > 0; j -= j & -j) res ^= bit[x & 1][y & 1][i][j];
    return res;
}

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

    cin >> n >> m;
    while (m--) {
        ll t, x0, y0, x1, y1;
        cin >> t >> x0 >> y0 >> x1 >> y1;
        if (t == 1) cout << (get(x1, y1) ^ get(x0 - 1, y1) ^ get(x1, y0 - 1) ^ get(x0 - 1, y0 - 1)) << '\n';
        else {
            ll v;
            cin >> v;
            update(x0, y0, v);
            update(x0, y1 + 1, v);
            update(x1 + 1, y0, v);
            update(x1 + 1, y1 + 1, v);
        }
    }
}
3 4
2 1 1 2 2 5
1 1 1 1 1
2 2 2 3 3 3
1 1 1 3 3

Độ phức tạp: mỗi thao tác O(log⁡2n)O(\log^2 n).

Ba chiều

Fenwick lồng được bao nhiêu tầng cũng được: ba chiều là ba vòng lặp lồng nhau, mỗi thao tác O(log⁡3n)O(\log^3 n). Tổng một hộp theo bao hàm loại trừ cần tám tổng tiền tố, dấu là (−1)(-1) mũ số tọa độ lấy cận dưới. Bộ nhớ n3n^3 nhanh chóng trở thành giới hạn: 1283128^3 số ll là 16 MB, 2563256^3 đã 128 MB.

Ví dụKhông Gian UFO

Đề. Khối N×N×NN \times N \times N với N≤128N \le 128, tọa độ từ 0. Tới 10510^5 lệnh: cộng vào một ô, hoặc hỏi tổng một hộp; dừng ở lệnh 3.

Lời giải. Fenwick ba chiều: thêm một vòng lặp lồng cho chiều thứ ba. Tổng một hộp ghép từ tám tiền tố theo bao hàm loại trừ.

Bước 1: cập nhật và tiền tố. Ba vòng lồng nhau, tọa độ đề cộng 1 trước khi đưa vào cây.

C++
void update(ll x, ll y, ll z, ll val) {
    for (ll i = x; i <= n; i += i & -i)
        for (ll j = y; j <= n; j += j & -j)
            for (ll k = z; k <= n; k += k & -k) bit[i][j][k] += val;
}

Bước 2: tám góc. Mặt nạ s 3 bit chọn cho mỗi chiều cận trên hay cận dưới. Trên cây, tiền tố tới cận trên là hi+1hi + 1, còn tiền tố đứng ngay trước cận dưới là lolo (tọa độ lo−1lo - 1 của đề cộng 1). Mỗi bit bật đổi dấu một lần, đúng dấu của bao hàm loại trừ. Khi lo=0lo = 0 thì tiền tố tới 0 bằng 0, đúng như mong muốn.

C++
for (ll s = 0; s < 8; s++) {
    ll c[3], sign = 1;
    for (ll d = 0; d < 3; d++) {
        if (s >> d & 1) {
            c[d] = lo[d];
            sign = -sign;
        } else c[d] = hi[d] + 1;
    }
    res += sign * get(c[0], c[1], c[2]);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 130;
ll n, bit[N][N][N];

void update(ll x, ll y, ll z, ll val) {
    for (ll i = x; i <= n; i += i & -i)
        for (ll j = y; j <= n; j += j & -j)
            for (ll k = z; k <= n; k += k & -k) bit[i][j][k] += val;
}

ll get(ll x, ll y, ll z) {
    ll res = 0;
    for (ll i = x; i > 0; i -= i & -i)
        for (ll j = y; j > 0; j -= j & -j)
            for (ll k = z; k > 0; k -= k & -k) res += bit[i][j][k];
    return res;
}

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

    cin >> n;
    ll t;
    while (cin >> t && t != 3) {
        if (t == 1) {
            ll x, y, z, k;
            cin >> x >> y >> z >> k;
            update(x + 1, y + 1, z + 1, k);
        } else {
            ll lo[3], hi[3];
            cin >> lo[0] >> lo[1] >> lo[2] >> hi[0] >> hi[1] >> hi[2];
            ll res = 0;
            for (ll s = 0; s < 8; s++) {
                ll c[3], sign = 1;
                for (ll d = 0; d < 3; d++) {
                    if (s >> d & 1) {
                        c[d] = lo[d];
                        sign = -sign;
                    } else c[d] = hi[d] + 1;
                }
                res += sign * get(c[0], c[1], c[2]);
            }
            cout << res << '\n';
        }
    }
}
3
1 0 0 0 4
1 2 1 2 3
2 0 0 0 2 2 2
1 0 0 0 -1
2 0 0 0 0 0 0
3

Độ phức tạp: mỗi thao tác O(log⁡3N)≈343O(\log^3 N) \approx 343 bước, câu hỏi gấp 8.

Khi đi thi

  • Lưới tới khoảng 2000×20002000 \times 2000, cập nhật điểm và hỏi hình chữ nhật trực tuyến: Fenwick hai chiều. Lưới lớn hơn hoặc tọa độ tới 10910^9: nếu biết trước mọi thao tác thì quét một chiều với Fenwick một chiều (bài 39), hoặc chia để trị CDQ (bài 60).
  • Hình chữ nhật luôn là bốn tiền tố; viết hàm hỏi hình chữ nhật một lần và kiểm cẩn thận các cận x1−1x_1 - 1, y1−1y_1 - 1.
  • Max, min trên hình chữ nhật có gán ô không làm được bằng Fenwick: dùng cây phân đoạn hai chiều hay chia căn.

Tóm tắt

  • Fenwick hai chiều là hai vòng lặp lồng nhau, O(log⁡nlog⁡m)O(\log n \log m) mỗi thao tác.
  • Tổng hình chữ nhật bằng bốn tổng tiền tố với dấu xen kẽ.
  • Cập nhật hình chữ nhật, hỏi điểm: mảng hiệu bốn góc.
  • Cập nhật và hỏi hình chữ nhật: bốn cây theo khai triển, với XOR chỉ cần bốn cây theo tính chẵn lẻ.
  • Ba chiều là ba vòng lồng, tám tiền tố, bộ nhớ n3n^3.

Bài tập

Đọc thêm