Chương 4·Bài 45

2D Fenwick Tree

Fenwick (bài 16) cộng một điểm và hỏi tổng tiền tố trong O(log⁡n)O(\log n). Trên lưới, tổng một hình chữ nhật cũng chỉ là bốn tổng tiền tố hai chiều, và Fenwick làm được hai chiều bằng cách lồng hai vòng lặp: vòng ngoài nhảy theo hàng như Fenwick thường, vòng trong nhảy theo cột. Mỗi thao tác tốn O(log⁡nlog⁡m)O(\log n \log m). Kỹ thuật mảng hiệu đổi vai cập nhật và câu hỏi, cho cập nhật hình chữ nhật và hỏi điểm; thêm vài cây theo tính chẵn lẻ của tọa độ là cập nhật và hỏi hình chữ nhật cùng lúc.

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.

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. Fenwick hai chiều: một cây Fenwick theo hàng, mỗi nút của nó lại là một cây Fenwick theo cột.

Bước 1: cập nhật. Vòng ngoài đi theo các nút của hàng xx (cộng bit thấp nhất), vòng trong đi theo các nút của cột yy. Ô bit[i][j] giữ tổng một hình chữ nhật con, ghép từ hai đoạn Fenwick một chiều.

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

Bước 2: tổng tiền tố. Tổng trên [1..x]×[1..y][1..x] \times [1..y], cùng khuôn hai vòng nhưng trừ bit thấp nhất.

C++
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ước 3: tổng hình chữ nhật. 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.

C++
l++, b++, r++, tp++;
cout << get(r, tp) - get(l - 1, tp) - get(r, b - 1) + get(l - 1, b - 1) << '\n';

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

Mỗi lệnh O(log⁡2S)≈100O(\log^2 S) \approx 100 bước.

Ví dụRừng Động

Đề. Rừng n×nn \times n với n≤1000n \le 1000, ô có cây hoặc trống. q≤2⋅105q \le 2 \cdot 10^5 thao tác: đảo trạng thái một ô, hoặc đếm cây trong một hình chữ nhật.

Lời giải. Fenwick hai chiều đếm cây, update và get giống hệt ví dụ trước.

Bước 1: dựng ban đầu. Mỗi ô có cây là một lần cộng 1; mảng a giữ trạng thái hiện tại của ô. Trường hợp xấu nhất n2log⁡2n≈108n^2 \log^2 n \approx 10^8 phép nhẹ, vẫn kịp; nếu cần nhanh hơn thì dựng từ tổng tiền tố trong O(n2)O(n^2).

C++
if (s[j - 1] == '*') {
    a[i][j] = 1;
    update(i, j, 1);
}

Bước 2: đảo ô. Ô đang có cây thì cộng −1-1, trống thì cộng +1+1, rồi đảo a. Đề cho tọa độ theo (hàng, cột) là y, x; giữ đúng thứ tự đó cho cả cập nhật lẫn câu hỏi.

C++
update(y, x, a[y][x] ? -1 : 1);
a[y][x] ^= 1;

Bước 3: đếm cây. Bốn tổng tiền tố như ví dụ trước.

C++
cout << get(y2, x2) - get(y1 - 1, x2) - get(y2, x1 - 1) + get(y1 - 1, x1 - 1) << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1005;
ll n, q, a[N][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);

    cin >> n >> q;
    for (ll i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (ll j = 1; j <= n; j++)
            if (s[j - 1] == '*') {
                a[i][j] = 1;
                update(i, j, 1);
            }
    }
    while (q--) {
        ll t;
        cin >> t;
        if (t == 1) {
            ll y, x;
            cin >> y >> x;
            update(y, x, a[y][x] ? -1 : 1);
            a[y][x] ^= 1;
        } else {
            ll y1, x1, y2, x2;
            cin >> y1 >> x1 >> y2 >> x2;
            cout << get(y2, x2) - get(y1 - 1, x2) - get(y2, x1 - 1) + get(y1 - 1, x1 - 1) << '\n';
        }
    }
}
3 4
*..
.*.
..*
2 1 1 3 3
1 2 2
2 1 1 2 2
2 3 3 3 3

Mỗi thao tác O(log⁡2n)O(\log^2 n).

Cập nhật hình chữ nhật, 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

Mỗi lệnh O(log⁡2n)O(\log^2 n), mỗi test thêm O(n2)O(n^2) để xóa.

Cập nhật và hỏi hình chữ nhật

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

Mỗi thao tác O(log⁡2n)O(\log^2 n).

Thêm một chiều nữa

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

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