Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Chuyên đề 9: Mảng cộng dồn 2 chiều và mảng hiệu 2 chiều trong C++ và Python: tổng hình chữ nhật con O(1), cộng vào cả hình chữ nhật, hình vuông k × k tổng lớn nhất

Chuyên đề 9: Mảng cộng dồn 2 chiều và mảng hiệu 2 chiều trong C++ và Python: tổng hình chữ nhật con O(1), cộng vào cả hình chữ nhật, hình vuông k × k tổng lớn nhất
Series: Chuyên đề nhập môn Độ khó: Dễ Chuyên đề 9 / Nhập môn lập trình Mảng cộng dồn 2 chiều
// Chuyên đề nhập môn · Chuyên đề 9
Mảng cộng dồn 2 chiều và mảng hiệu 2 chiều
Tính tổng một hình chữ nhật bất kỳ trên bảng một triệu ô chỉ bằng 4 phép tính, cộng thêm một số vào cả hình chữ nhật chỉ bằng 4 phép gán. Hai kỹ thuật nhỏ nhưng xuất hiện trong rất nhiều đề HSG.

Lời mở đầu

Ở bài 13 em đã biết mảng cộng dồn trên dãy: tính trước $P_i = a_1 + \cdots + a_i$ thì tổng đoạn $[l, r]$ chỉ là $P_r - P_{l-1}$, và mảng hiệu giúp cộng $v$ vào cả một đoạn trong $O(1)$. Nhưng đề thi rất thích cho dữ liệu là một bảng: bản đồ độ cao, cánh đồng, tấm ảnh, sân chơi… và hỏi tổng của một hình chữ nhật con, hoặc tưới nước, phủ sóng cho cả một vùng hình chữ nhật. Đó là lúc cần nâng hai kỹ thuật ấy lên hai chiều.

Bài này dùng nhiều mảng hai chiều đánh số từ 1, nên nếu em chưa vững hãy xem lại bài 7: Mảng 2 chiều. Ý tưởng then chốt chỉ là một hình vẽ bao hàm – loại trừ: cộng hai vùng chồng nhau thì phải trừ phần chung. Mình sẽ đi thật chậm, có mô phỏng bấm từng bước cho từng công thức, và chỉ ra những chỗ lệch chỉ số hay làm mất điểm.

Trong bài này bạn sẽ học: vì sao cộng trực tiếp từng ô quá chậm khi có nhiều truy vấn; cách xây bảng cộng dồn 2 chiều $S[i][j]$ bằng công thức bao hàm – loại trừ với chỉ số từ 1 và viền 0; truy vấn tổng hình chữ nhật trong $O(1)$ và những lỗi lệch chỉ số hay gặp; mảng hiệu 2 chiều: cộng $v$ vào cả hình chữ nhật bằng 4 ô góc rồi khôi phục bằng cộng dồn; các dạng đề thi: hình vuông $k \times k$ tổng lớn nhất, đếm hình chữ nhật toàn số 1, đặt trạm phủ sóng, kết hợp chặt nhị phân cạnh hình vuông. Có 2 khung chạy tay, 3 mô phỏng bấm từng bước và 8 bài tập kèm gợi ý.

Lộ trình bài viết, bấm vào từng phần để nhảy tới:

Phần 1Bài toán tổng hình chữ nhật con

Đề bài mẫu. Cho bảng số $a$ gồm $n$ hàng, $m$ cột ($n, m \le 1000$). Có $q$ câu hỏi ($q \le 10^5$), mỗi câu cho góc trên trái $(x_1, y_1)$ và góc dưới phải $(x_2, y_2)$; hãy tính tổng các ô $a[i][j]$ với $x_1 \le i \le x_2$, $y_1 \le j \le y_2$. Trong cả bài, $x$ luôn là chỉ số hàng, $y$ là chỉ số cột, đánh số từ 1.

Cách nghĩ đầu tiên: mỗi câu hỏi dùng hai vòng for cộng hết các ô trong hình chữ nhật. Một câu hỏi lớn nhất có $10^6$ ô, $10^5$ câu hỏi là $10^{11}$ phép cộng, chạy mất vài phút trong khi giới hạn thường là 1 giây (xem lại cách ước lượng ở bài 9).

Cách nghĩ thứ hai, tận dụng bài 13: làm cộng dồn 1 chiều cho từng hàng. Khi đó tổng của hàng $i$ từ cột $y_1$ tới $y_2$ tính được trong $O(1)$, và một câu hỏi chỉ cần cộng $x_2 - x_1 + 1$ hàng, tức $O(n)$. Tốt hơn nhiều, nhưng vẫn là $10^5 \cdot 10^3 = 10^8$ phép tính, vừa sát giới hạn với C++ và chắc chắn quá chậm với Python. Ta muốn mỗi câu hỏi chỉ tốn $O(1)$, giống hệt mảng một chiều.

Cách làmChuẩn bịMỗi câu hỏiTổng với $n = m = 1000$, $q = 10^5$
Cộng trực tiếp từng ôkhông$O(n \cdot m)$$10^{11}$: quá chậm
Cộng dồn 1 chiều trên từng hàng$O(n \cdot m)$$O(n)$$10^{8}$: sát giới hạn
Cộng dồn 2 chiều (bài này)$O(n \cdot m)$$O(1)$$10^6 + 10^5$: chớp mắt

Bảng 1. Ba cách trả lời câu hỏi tổng hình chữ nhật. Chi phí chuẩn bị chỉ trả một lần, còn chi phí mỗi câu hỏi nhân lên $q$ lần.

Ý tưởng của cộng dồn 2 chiều: tính trước một bảng $S$ cùng cỡ với $a$, trong đó

$$S[i][j] = \sum_{u=1}^{i} \sum_{v=1}^{j} a[u][v],$$

tức là tổng của hình chữ nhật có góc trên trái là ô $(1, 1)$ và góc dưới phải là ô $(i, j)$. Mọi hình chữ nhật khác sẽ được “cắt” ra từ bốn hình chữ nhật dạng này. Phần 2 trả lời câu hỏi xây $S$ thế nào cho nhanh, phần 3 trả lời câu hỏi dùng $S$ ra sao.

Ôn lại một chiều trong 10 giây
Với dãy $a_1..a_n$: $P_0 = 0$, $P_i = P_{i-1} + a_i$, tổng đoạn $[l, r]$ là $P_r - P_{l-1}$. Phần tử $P_0 = 0$ chính là “viền” giúp đoạn bắt đầu từ $l = 1$ không cần xử lý riêng. Hai chiều cũng vậy, chỉ là viền có cả hàng 0 lẫn cột 0.

Phần 2Xây bảng S bằng bao hàm – loại trừ

Tính từng $S[i][j]$ bằng hai vòng lặp thì mỗi ô lại tốn $O(n \cdot m)$, cả bảng thành $O(n^2 m^2)$. Ta phải dùng lại những ô $S$ đã tính. Nhìn hình dưới: vùng $(1, 1) \to (i, j)$ gồm ô $a[i][j]$ cộng với phần còn lại, và phần còn lại được phủ bởi hai vùng quen thuộc:

  • $S[i-1][j]$: các hàng phía trên (hàng $1..i-1$, cột $1..j$);
  • $S[i][j-1]$: các cột bên trái (hàng $1..i$, cột $1..j-1$).

Nhưng hai vùng này chồng lên nhau ở hình chữ nhật $(1, 1) \to (i-1, j-1)$, nên cộng cả hai thì phần chung bị tính hai lần. Trừ nó đi một lần là xong:

$$S[i][j] = a[i][j] + S[i-1][j] + S[i][j-1] - S[i-1][j-1].$$

123456712345(i,j)phần chungXanh dương: S[i−1][j]hàng 1..i−1, cột 1..jTím (viền đứt): S[i][j−1]hàng 1..i, cột 1..j−1Đỏ: S[i−1][j−1] bị cộng 2 lầnnằm trong cả hai vùng trênVàng: ô a[i][j] còn thiếuS[i][j] = a[i][j] + S[i−1][j] + S[i][j−1] − S[i−1][j−1]

Hình 1. Bao hàm – loại trừ: cộng vùng xanh dương và vùng tím thì vùng đỏ bị cộng hai lần, nên trừ đi $S[i-1][j-1]$, cuối cùng cộng ô vàng $a[i][j]$.

Chỉ số từ 1 và viền 0. Với $i = 1$ thì công thức cần $S[0][j]$, với $j = 1$ cần $S[i][0]$. Ta cho hàng 0 và cột 0 của $S$ đều bằng 0 (“không có ô nào thì tổng bằng 0”). Trong C++, khai báo $S$ là mảng toàn cục thì mọi phần tử tự bằng 0; trong Python, tạo bảng $(n + 1) \times (m + 1)$ toàn số 0. Khi đó công thức đúng cho mọi ô, không cần một câu if nào. Ví dụ với bảng $4 \times 5$ dưới đây (đây cũng là dữ liệu mặc định của mô phỏng):

a: cột 12345S: cột 12345
hàng 131415348914
hàng 2926531215253139
hàng 3589791728476077
hàng 4323842033557697

Bảng 2. Ma trận $a$ (trái) và bảng cộng dồn $S$ (phải, đã bỏ hàng 0, cột 0 toàn số 0). Chẳng hạn $S[3][3] = 9 + 25 + 28 - 15 = 47$; ô cuối $S[4][5] = 97$ là tổng cả bảng.

Em bấm Tiếp để xem từng ô của $S$ được tính từ đúng 3 ô đã có: hai ô xanh dương (được cộng) và một ô đỏ (bị trừ). Thử sửa ma trận, kể cả số âm, để tự kiểm tra:

MÔ PHỎNGXây bảng cộng dồn 2 chiều S từng ôSửa ma trận a (tối đa 5 × 6, mỗi hàng một dòng) rồi bấm Tiếp / Tự chạy. Mỗi bước tính một ô của S từ 3 ô đã có.
a[i][j] đang xéthai ô S được cộngô S bị trừ (phần chung)S[i][j] vừa tínhviền 0 / chưa tính
Khuôn mẫu cộng dồn 2 chiều
long long S[1005][1005];          // toàn cục: hàng 0, cột 0 tự bằng 0

for(int i = 1; i <= n; i++)
    for(int j = 1; j <= m; j++)
        S[i][j] = a[i][j] + S[i-1][j] + S[i][j-1] - S[i-1][j-1];

// tổng hình chữ nhật (x1, y1) -> (x2, y2)
long long sum(int x1, int y1, int x2, int y2){
    return S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1];
}
Thuộc lòng hai dòng công thức này: dòng xây bảng có một dấu trừ, dòng truy vấn có hai dấu trừ, và chỗ nào có “trừ 1” thì luôn đi với $x_1$, $y_1$ (góc trên trái).
Không cần giữ mảng a
Nếu chỉ cần hỏi tổng, em có thể đọc từng số rồi tính ngay $S[i][j]$, không cần lưu $a$ (code ở phần 3 làm như vậy). Thứ tự đọc từng hàng, trong hàng từ trái sang phải đúng là thứ tự cần để ba ô $S[i-1][j]$, $S[i][j-1]$, $S[i-1][j-1]$ đã có sẵn. Mẹo cho Python: hai vòng for lồng nhau trên $10^6$ ô khá chậm. Để ý $S[i][j] = S[i-1][j] + (a[i][1] + \cdots + a[i][j])$: hàng $i$ của $S$ bằng hàng $i - 1$ cộng với cộng dồn 1 chiều của hàng $i$. Code Python trong bài viết đúng như vậy bằng itertools.accumulate và zip, nhanh hơn vài lần (xem thêm chuyên đề 8: Python khi đi thi).

Phần 3Truy vấn tổng hình chữ nhật trong O(1)

Cần tổng vùng $D$ có góc trên trái $(x_1, y_1)$, góc dưới phải $(x_2, y_2)$. Hình chữ nhật lớn $(1, 1) \to (x_2, y_2)$ được chia thành 4 vùng như hình dưới: $A$ là góc trên trái, $B$ là dải phía trên $D$, $C$ là dải bên trái $D$. Mỗi tổ hợp vùng đều đọc được từ bảng $S$:

12345678123456ABCD(x1, y1)(x2, y2)S[x2][y2] = A + B + C + DS[x1−1][y2] = A + BS[x2][y1−1] = A + CS[x1−1][y1−1] = AD = S[x2][y2] − S[x1−1][y2] − S[x2][y1−1] + S[x1−1][y1−1]

Hình 2. $S[x_2][y_2]$ chứa cả 4 vùng. Bỏ dải trên ($A + B$) và dải trái ($A + C$) thì vùng $A$ bị trừ hai lần, nên cộng lại $A = S[x_1 - 1][y_1 - 1]$.

$$\text{tổng}(x_1, y_1, x_2, y_2) = S[x_2][y_2] - S[x_1 - 1][y_2] - S[x_2][y_1 - 1] + S[x_1 - 1][y_1 - 1].$$

Thử với Bảng 2, hình chữ nhật $(2, 2) \to (3, 4)$ gồm các số $2, 6, 5, 8, 9, 7$ có tổng 37. Theo công thức: $S[3][4] - S[1][4] - S[3][1] + S[1][1] = 60 - 9 - 17 + 3 = 37$. Đúng!

Mô phỏng dưới đây tô màu theo số lần mỗi ô đang được tính sau từng phép cộng, trừ. Em để ý ô đỏ (bị trừ thừa) xuất hiện ở bước thứ ba và biến mất ở bước cuối. Hãy thử cả trường hợp $x_1 = 1$ hoặc $y_1 = 1$ để thấy viền 0 làm việc:

MÔ PHỎNGTruy vấn tổng hình chữ nhật bằng 4 ô của bảng SNhập ma trận và 4 số x1 y1 x2 y2. Mỗi bước cộng hoặc trừ một ô S; màu cho biết mỗi ô của a đang được tính mấy lần.
được tính 1 lầntính 0 lần (đã bỏ)bị trừ thừa (−1 lần)chưa đụng tới
Xem code C++ / Python: cộng dồn 2 chiều và trả lời q truy vấn tổng hình chữ nhật (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

long long S[1005][1005];     // S[i][j] = tổng hình chữ nhật (1, 1) -> (i, j); hàng 0, cột 0 là viền 0

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, q;
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            long long x;
            cin >> x;
            S[i][j] = x + S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];
        }
    while(q--){
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1] << "\n";
    }
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, q = int(data[0]), int(data[1]), int(data[2])
    S = [[0] * (m + 1)]                          # hàng 0 là viền 0
    p = 3
    for i in range(1, n + 1):
        row = accumulate(map(int, data[p:p + m]), initial=0)   # 0, a[i][1], a[i][1] + a[i][2], ...
        p += m
        S.append([u + r for u, r in zip(S[i - 1], row)])        # S[i][j] = S[i-1][j] + tổng hàng i tới cột j
    out = []
    for _ in range(q):
        x1, y1, x2, y2 = int(data[p]), int(data[p + 1]), int(data[p + 2]), int(data[p + 3])
        p += 4
        out.append(S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1])
    print('\n'.join(map(str, out)))

main()

Khung chạy tay dưới đây chạy đúng chương trình trên (bản rút gọn, có lưu $a$) với dữ liệu nhỏ. Em xem bảng $S$ được lấp dần, rồi mỗi truy vấn chỉ nhìn vào 4 ô của $S$:

C++▶ Chạy tay từng dòng codeXây S từng ô (ô vàng) từ hai ô được cộng (viền tím) và một ô bị trừ (ô đỏ); mỗi truy vấn chỉ dùng 4 ô của S.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4long long a[105][105], S[105][105];
5
6int main(){
7 int n, m, q;
8 cin >> n >> m >> q;
9 for(int i = 1; i <= n; i++)
10 for(int j = 1; j <= m; j++) cin >> a[i][j];
11 for(int i = 1; i <= n; i++)
12 for(int j = 1; j <= m; j++)
13 S[i][j] = a[i][j] + S[i-1][j] + S[i][j-1] - S[i-1][j-1];
14 while(q--){
15 int x1, y1, x2, y2;
16 cin >> x1 >> y1 >> x2 >> y2;
17 long long res = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1];
18 cout << res << "\n";
19 }
20 return 0;
21}

Những lỗi lệch chỉ số hay gặp. Công thức ngắn nhưng chỉ cần sai một ký tự là sai cả bài, mà ví dụ trong đề thường vẫn ra đúng! Bảng dưới gom những lỗi mình gặp nhiều nhất khi chấm bài học sinh:

LỗiHậu quảCách tránh
Viết S[x1][y2] thay vì S[x1-1][y2]mất cả hàng $x_1$ của hình chữ nhật“trừ 1” luôn đi với góc trên trái
Đánh số từ 0 nhưng vẫn dùng x1-1C++ đọc S[-1] ra rác; Python S[-1] là hàng cuối, sai mà không báo lỗiđọc vào xong cộng 1 cho cả 4 chỉ số
Khai báo S[n][m] vừa khíttràn mảng vì cần chỉ số $n$, $m$ (và $n + 1$, $m + 1$ ở mảng hiệu)khai báo dư: S[1005][1005]
Dùng int cho $S$$10^6$ ô, mỗi ô tới $10^9$ thì tổng tới $10^{15}$: tràn sốlong long cho $S$ và kết quả
Đề cho hai góc bất kỳ, không theo thứ tựcó thể $x_1 > x_2$, ra số âm vô nghĩaif(x1 > x2) swap(x1, x2); tương tự với $y$
Nhầm $x$ là cột (như toạ độ trên mặt phẳng)tính sai vùng, hoặc tràn mảng khi $n \ne m$đọc kỹ đề: “hàng”, “cột” hay “hoành độ”, “tung độ”

Bảng 3. Lỗi hay gặp với cộng dồn 2 chiều. Cách thử nhanh: luôn có test hình chữ nhật chạm hàng 1, cột 1, chạm hàng $n$, cột $m$, và test bảng $1 \times 1$.

Muốn đánh số từ 0 thì sao?
Nếu đề cho chỉ số từ 0, đừng sửa công thức, hãy cộng 1 vào cả 4 chỉ số ngay khi đọc vào. Hoặc lưu $a$ từ 0 nhưng $S$ vẫn lệch một: $S[i+1][j+1] = a[i][j] + S[i][j+1] + S[i+1][j] - S[i][j]$. Trộn hai cách đánh số trong cùng một bài là nguồn gốc của rất nhiều lần “sai test ẩn”.

Phần 4Mảng hiệu 2 chiều: cộng v vào cả hình chữ nhật

Bài toán ngược lại: có $q$ thao tác, mỗi thao tác cộng $v$ vào mọi ô của hình chữ nhật $(x_1, y_1) \to (x_2, y_2)$. Sau tất cả các thao tác, in ra bảng. Cộng trực tiếp tốn $O(n \cdot m)$ mỗi thao tác. Ở bài 13, mảng hiệu một chiều làm việc này bằng 2 phép gán: D[l] += v; D[r + 1] -= v; rồi cộng dồn. Hai chiều thì cần 4 ô góc:

Câu lệnhÝ nghĩa sau khi cộng dồn
D[x1][y1] += v“bật”: mọi ô $(i, j)$ với $i \ge x_1$, $j \ge y_1$ được cộng $v$ (cả góc dưới phải vô tận)
D[x1][y2 + 1] -= v“tắt” phần bên phải: các ô có $j > y_2$
D[x2 + 1][y1] -= v“tắt” phần phía dưới: các ô có $i > x_2$
D[x2 + 1][y2 + 1] += vvùng $i > x_2$, $j > y_2$ bị tắt hai lần nên bật lại một lần (lại là bao hàm – loại trừ!)

Bảng 4. Bốn ô góc của mảng hiệu 2 chiều. Chỉ số có thể tới $n + 1$ và $m + 1$, nên mảng $D$ phải khai báo dư.

Sau khi ghi hết các thao tác, ta cộng dồn 2 chiều mảng $D$ (đúng công thức của phần 2). Giá trị tại mỗi ô $(i, j)$ lúc đó bằng tổng các $D[u][v]$ với $u \le i$, $v \le j$: thao tác nào có góc trên trái nằm ở phía trên bên trái ô $(i, j)$ thì được tính, trừ khi ô đã vượt quá hàng $x_2$ hay cột $y_2$ của thao tác đó. Mỗi thao tác tốn $O(1)$, khôi phục tốn $O(n \cdot m)$ một lần duy nhất.

Mô phỏng dưới đây cho thấy giá trị “lan ra” thế nào. Để dễ nhìn, mình cộng dồn theo từng hàng trước rồi theo từng cột; kết quả giống hệt dùng công thức bao hàm – loại trừ cho từng ô (vì cộng dồn 2 chiều chính là cộng dồn theo hàng rồi cộng dồn theo cột):

MÔ PHỎNGMảng hiệu 2 chiều: ghi 4 góc rồi cộng dồn cho lan raNhập kích thước và các thao tác (mỗi dòng: x1 y1 x2 y2 v). Xem 4 ô góc được ghi, rồi giá trị lan theo hàng, theo cột.
góc được cộng vgóc bị trừ vhàng / cột đang cộng dồnbằng 0 hoặc ô thừa
Xem code C++ / Python: mảng hiệu 2 chiều: q lần cộng v vào hình chữ nhật rồi in bảng (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

long long a[1005][1005], D[1005][1005];   // D cần tới chỉ số n + 1, m + 1

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, q;
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++) cin >> a[i][j];
    while(q--){
        int x1, y1, x2, y2;
        long long v;
        cin >> x1 >> y1 >> x2 >> y2 >> v;
        D[x1][y1] += v;              // bật
        D[x1][y2 + 1] -= v;          // tắt bên phải
        D[x2 + 1][y1] -= v;          // tắt phía dưới
        D[x2 + 1][y2 + 1] += v;      // góc bị tắt hai lần -> bật lại
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            D[i][j] += D[i - 1][j] + D[i][j - 1] - D[i - 1][j - 1];   // cộng dồn 2 chiều tại chỗ
            cout << a[i][j] + D[i][j] << " ";
        }
        cout << "\n";
    }
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, q = int(data[0]), int(data[1]), int(data[2])
    p = 3
    a = []
    for i in range(n):
        a.append([int(x) for x in data[p:p + m]])
        p += m
    D = [[0] * (m + 2) for _ in range(n + 2)]    # cần tới chỉ số n + 1, m + 1
    for _ in range(q):
        x1, y1, x2, y2, v = (int(x) for x in data[p:p + 5])
        p += 5
        D[x1][y1] += v              # bật
        D[x1][y2 + 1] -= v          # tắt bên phải
        D[x2 + 1][y1] -= v          # tắt phía dưới
        D[x2 + 1][y2 + 1] += v      # góc bị tắt hai lần -> bật lại
    out = []
    for i in range(1, n + 1):
        # cộng dồn 2 chiều: D[i][j] = D[i-1][j] (đã cộng dồn) + tổng D[i][0..j]
        D[i] = cur = [u + r for u, r in zip(D[i - 1], accumulate(D[i]))]
        row = a[i - 1]
        out.append(' '.join(str(row[j - 1] + cur[j]) for j in range(1, m + 1)))
    print('\n'.join(out))

main()

Khung chạy tay dưới đây ghi 4 ô góc cho từng thao tác rồi cộng dồn tại chỗ, in luôn giá trị từng ô:

C++▶ Chạy tay từng dòng codeMỗi thao tác chỉ sửa 4 ô góc của D; sau đó cộng dồn 2 chiều tại chỗ thì D[i][j] trở thành giá trị thật của ô (i, j).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4long long D[105][105];
5
6int main(){
7 int n, m, q;
8 cin >> n >> m >> q;
9 while(q--){
10 int x1, y1, x2, y2; long long v;
11 cin >> x1 >> y1 >> x2 >> y2 >> v;
12 D[x1][y1] += v;
13 D[x1][y2 + 1] -= v;
14 D[x2 + 1][y1] -= v;
15 D[x2 + 1][y2 + 1] += v;
16 }
17 for(int i = 1; i <= n; i++){
18 for(int j = 1; j <= m; j++){
19 D[i][j] += D[i-1][j] + D[i][j-1] - D[i-1][j-1];
20 cout << D[i][j] << " ";
21 }
22 cout << "\n";
23 }
24 return 0;
25}
Ba lỗi kinh điển của mảng hiệu 2 chiều
1. Khai báo $D$ cỡ $n + 1$ là thiếu: cần dùng tới chỉ số $n + 1$, $m + 1$, nên ít nhất $(n + 2) \times (m + 2)$.
2. Quên ô thứ tư D[x2+1][y2+1] += v: vùng góc dưới phải sẽ bị trừ $v$ oan.
3. Có thao tác xen giữa câu hỏi (“cộng xong rồi hỏi ô này bằng bao nhiêu”, rồi lại cộng tiếp): mảng hiệu chỉ hợp khi mọi thao tác cộng làm trước, mọi câu hỏi làm sau. Trộn lẫn thì cần cấu trúc dữ liệu mạnh hơn như cây Fenwick 2 chiều (bài 31).
Hỏi tổng sau khi cộng
Nếu sau các thao tác cộng, đề còn hỏi tổng nhiều hình chữ nhật, em khôi phục bảng bằng mảng hiệu, rồi xây thêm một lần bảng cộng dồn $S$ trên bảng vừa khôi phục. Hai lần cộng dồn liên tiếp, mỗi lần $O(n \cdot m)$.

Phần 5Các dạng bài trong đề thi

Biết hai công thức rồi, cái khó trong đề thi là nhận ra chỗ cần dùng chúng. Dưới đây là những dạng mình gặp nhiều nhất; mỗi dạng đều có một bài tập tương ứng ở cuối bài.

Dạng 1: hình vuông (hình chữ nhật) kích thước cố định có tổng lớn nhất. “Chọn mảnh đất $k \times k$ thu hoạch được nhiều nhất”, “đặt tấm ảnh $k \times k$ sao cho tổng độ sáng lớn nhất”… Duyệt mọi vị trí đặt: có $(n - k + 1)(m - k + 1)$ vị trí, mỗi vị trí tính tổng trong $O(1)$. Mẹo nhỏ: duyệt theo góc dưới phải $(i, j)$ với $i \ge k$, $j \ge k$, khi đó góc trên trái là $(i - k + 1, j - k + 1)$ và tổng là $S[i][j] - S[i-k][j] - S[i][j-k] + S[i-k][j-k]$, không cần lo chỉ số ra ngoài bảng.

Xem code C++ / Python: hình vuông k × k có tổng lớn nhất (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

long long S[1005][1005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, k;
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            long long x;
            cin >> x;
            S[i][j] = x + S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];
        }
    long long best = LLONG_MIN;
    int bx = 0, by = 0;
    for(int i = k; i <= n; i++)                  // (i, j) là góc DƯỚI PHẢI của hình vuông
        for(int j = k; j <= m; j++){
            long long s = S[i][j] - S[i - k][j] - S[i][j - k] + S[i - k][j - k];
            if(s > best){ best = s; bx = i - k + 1; by = j - k + 1; }   // đổi về góc trên trái
        }
    cout << best << "\n" << bx << " " << by;
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, k = int(data[0]), int(data[1]), int(data[2])
    S = [[0] * (m + 1)]                          # hàng 0 là viền 0
    p = 3
    for i in range(1, n + 1):
        row = accumulate(map(int, data[p:p + m]), initial=0)   # cộng dồn hàng i
        p += m
        S.append([u + r for u, r in zip(S[i - 1], row)])
    best, bx, by = None, 0, 0
    for i in range(k, n + 1):               # (i, j) là góc DƯỚI PHẢI của hình vuông
        cur, up = S[i], S[i - k]
        # vals[t] = tổng hình vuông có góc dưới phải (i, t + k), tức góc trên trái (i - k + 1, t + 1)
        vals = [a - b - c + d for a, b, c, d in zip(cur[k:], up[k:], cur, up)]
        v = max(vals)
        if best is None or v > best:
            best, bx, by = v, i - k + 1, vals.index(v) + 1
    print(best)
    print(bx, by)

main()
Khởi tạo giá trị lớn nhất
Bảng có thể toàn số âm, nên best phải khởi tạo bằng LLONG_MIN (hoặc bằng tổng của vị trí đầu tiên), không được khởi tạo bằng 0. Đây là lỗi “ví dụ đúng, test ẩn sai” rất phổ biến.

Dạng 2: đếm hình chữ nhật thoả điều kiện khi bảng nhỏ. Bảng 0/1 cỡ $40 \times 40$, hỏi có bao nhiêu hình chữ nhật con toàn số 1. Số hình chữ nhật con là $\frac{n(n+1)}{2} \cdot \frac{m(m+1)}{2} \approx 6{,}7 \cdot 10^5$, đủ ít để duyệt hết. Với mỗi hình, nó toàn số 1 khi và chỉ khi tổng bằng diện tích: $\text{tổng} = (x_2 - x_1 + 1)(y_2 - y_1 + 1)$. Nhờ cộng dồn 2 chiều, kiểm tra mỗi hình chỉ tốn $O(1)$ thay vì $O(n \cdot m)$. Cùng ý tưởng: “toàn số 0” là tổng bằng 0, “có đúng $k$ ô đen” là tổng bằng $k$ (đếm ô đen bằng cách coi ô đen là 1, ô trắng là 0).

Dạng 3: phủ sóng, tưới nước, rải phân bón. Mỗi trạm phát sóng đặt ở ô $(x, y)$ phủ hình vuông bán kính $r$, tức các ô có $|i - x| \le r$ và $|j - y| \le r$. Hỏi bao nhiêu ô không có sóng, hay ô nào bắt được nhiều trạm nhất. Mỗi trạm là một thao tác mảng hiệu trên hình chữ nhật $(\max(1, x - r), \max(1, y - r)) \to (\min(n, x + r), \min(m, y + r))$. Chú ý phải cắt hình vuông vào trong bảng bằng max, min trước khi ghi 4 góc, nếu không sẽ ghi ra chỉ số âm.

Dạng 4: chặt nhị phân cạnh hình vuông. “Tìm hình vuông lớn nhất có tổng không vượt quá $T$” với các số không âm. Nếu có một hình vuông cạnh $k$ thoả mãn thì hình vuông cạnh $k - 1$ nằm bên trong nó cũng thoả (bớt ô đi thì tổng không tăng). Vậy tính chất “có hình vuông cạnh $k$ thoả” là đơn điệu: đúng với mọi $k$ nhỏ, sai với mọi $k$ lớn. Đó đúng là điều kiện để chặt nhị phân đáp án (bài 16): mỗi lần thử một $k$, kiểm tra bằng Dạng 1 trong $O(n \cdot m)$, tổng cộng $O(n \cdot m \log \min(n, m))$.

Dạng 5: nén hai hàng thành một dãy. Khi đề hỏi về mọi hình chữ nhật (chẳng hạn đếm số hình chữ nhật có tổng đúng bằng $T$), cố định hàng trên $x_1$ và hàng dưới $x_2$, tổng từng cột trong khoảng hàng đó tạo thành một dãy $m$ số, và bài toán trở về dãy một chiều quen thuộc của bài 13 (đếm đoạn con có tổng $T$ bằng cộng dồn và map). Độ phức tạp $O(n^2 m)$.

DạngKỹ thuậtĐộ phức tạpBài tập
Tổng nhiều hình chữ nhậtcộng dồn 2 chiều$O(nm + q)$Bài 1
Hình vuông $k \times k$ tốt nhất, cửa sổ quanh mỗi ôcộng dồn 2 chiều + duyệt vị trí$O(nm)$Bài 2, 3
Cộng vào nhiều hình chữ nhật, phủ sóngmảng hiệu 2 chiều$O(nm + q)$Bài 4, 5
Đếm hình chữ nhật trên bảng nhỏduyệt 4 góc + kiểm tra $O(1)$$O(n^2 m^2)$Bài 6
Hình vuông lớn nhất thoả điều kiện đơn điệuchặt nhị phân cạnh + cộng dồn$O(nm \log n)$Bài 7
Đếm hình chữ nhật có tổng $T$cố định 2 hàng, nén thành dãy$O(n^2 m)$Bài 8

Bảng 5. Bản đồ các dạng bài dùng cộng dồn 2 chiều và mảng hiệu 2 chiều.

Dấu hiệu nhận biết
Đề có bảng + hỏi tổng (hay số lượng ô thoả điều kiện) trên hình chữ nhật nhiều lần → cộng dồn 2 chiều. Đề có bảng + cộng thêm cho cả hình chữ nhật nhiều lần, chỉ hỏi kết quả ở cuối → mảng hiệu 2 chiều. Đếm theo điều kiện thì đổi mỗi ô thành 0/1 trước rồi mới cộng dồn.
Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Mảng cộng dồn.

Bài tậpLuyện cộng dồn 2 chiều và mảng hiệu 2 chiều

Bài 1Tổng hình chữ nhậtDễ

Cho bảng $n \times m$ số nguyên ($1 \le n, m \le 1000$, $|a_{ij}| \le 10^9$) và $q$ câu hỏi ($q \le 10^5$). Mỗi câu hỏi gồm 4 số $x_1, y_1, x_2, y_2$ ($1 \le x_1 \le x_2 \le n$, $1 \le y_1 \le y_2 \le m$). Với mỗi câu hỏi, in ra tổng các ô từ hàng $x_1$ tới $x_2$, cột $y_1$ tới $y_2$.

Dòng đầu: $n$, $m$, $q$. Tiếp theo là $n$ dòng của bảng, cuối cùng là $q$ dòng câu hỏi.

Input
4 5 4
3 1 4 1 5
9 2 6 5 3
5 8 9 7 9
3 2 3 8 4
2 2 3 4
1 1 4 5
3 1 3 1
4 1 4 5
Output
37
97
5
20

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đúng code ở phần 3. Câu đầu: $S[3][4] - S[1][4] - S[3][1] + S[1][1] = 60 - 9 - 17 + 3 = 37$. Câu thứ hai là cả bảng nên đáp án là $S[4][5] = 97$. Tổng có thể tới $10^{15}$, phải dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

long long S[1005][1005];     // S[i][j] = tổng hình chữ nhật (1, 1) -> (i, j); hàng 0, cột 0 là viền 0

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, q;
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            long long x;
            cin >> x;
            S[i][j] = x + S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];
        }
    while(q--){
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1] << "\n";
    }
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, q = int(data[0]), int(data[1]), int(data[2])
    S = [[0] * (m + 1)]                          # hàng 0 là viền 0
    p = 3
    for i in range(1, n + 1):
        row = accumulate(map(int, data[p:p + m]), initial=0)   # 0, a[i][1], a[i][1] + a[i][2], ...
        p += m
        S.append([u + r for u, r in zip(S[i - 1], row)])        # S[i][j] = S[i-1][j] + tổng hàng i tới cột j
    out = []
    for _ in range(q):
        x1, y1, x2, y2 = int(data[p]), int(data[p + 1]), int(data[p + 2]), int(data[p + 3])
        p += 4
        out.append(S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1])
    print('\n'.join(map(str, out)))

main()
Bài 2Mảnh đất màu mỡ nhấtDễ

Cánh đồng là bảng $n \times m$ ($1 \le n, m \le 1000$), ô $(i, j)$ cho sản lượng $a_{ij}$ ($|a_{ij}| \le 10^9$, số âm nghĩa là phải bỏ tiền cải tạo). Cần chọn một mảnh đất hình vuông $k \times k$ ($1 \le k \le \min(n, m)$) có tổng sản lượng lớn nhất. In ra tổng đó, rồi in toạ độ góc trên trái của mảnh đất. Nếu có nhiều đáp án, chọn hàng nhỏ nhất, rồi cột nhỏ nhất.

Input
4 5 2
3 1 4 1 5
9 2 6 5 3
5 8 9 7 9
3 2 3 8 4
Output
28
3 4

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Dạng 1 của phần 5: duyệt góc dưới phải $(i, j)$ từ $(k, k)$, tổng là $S[i][j] - S[i-k][j] - S[i][j-k] + S[i-k][j-k]$, rồi đổi về góc trên trái $(i - k + 1, j - k + 1)$. Duyệt hàng tăng dần, trong hàng cột tăng dần, và chỉ cập nhật khi lớn hơn hẳn thì tự nhiên chọn được hàng nhỏ nhất, cột nhỏ nhất. Mẫu: hình vuông $(3, 4) \to (4, 5)$ gồm $7 + 9 + 8 + 4 = 28$. Khởi tạo best bằng LLONG_MIN vì có thể toàn số âm.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

long long S[1005][1005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, k;
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            long long x;
            cin >> x;
            S[i][j] = x + S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];
        }
    long long best = LLONG_MIN;
    int bx = 0, by = 0;
    for(int i = k; i <= n; i++)                  // (i, j) là góc DƯỚI PHẢI của hình vuông
        for(int j = k; j <= m; j++){
            long long s = S[i][j] - S[i - k][j] - S[i][j - k] + S[i - k][j - k];
            if(s > best){ best = s; bx = i - k + 1; by = j - k + 1; }   // đổi về góc trên trái
        }
    cout << best << "\n" << bx << " " << by;
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, k = int(data[0]), int(data[1]), int(data[2])
    S = [[0] * (m + 1)]                          # hàng 0 là viền 0
    p = 3
    for i in range(1, n + 1):
        row = accumulate(map(int, data[p:p + m]), initial=0)   # cộng dồn hàng i
        p += m
        S.append([u + r for u, r in zip(S[i - 1], row)])
    best, bx, by = None, 0, 0
    for i in range(k, n + 1):               # (i, j) là góc DƯỚI PHẢI của hình vuông
        cur, up = S[i], S[i - k]
        # vals[t] = tổng hình vuông có góc dưới phải (i, t + k), tức góc trên trái (i - k + 1, t + 1)
        vals = [a - b - c + d for a, b, c, d in zip(cur[k:], up[k:], cur, up)]
        v = max(vals)
        if best is None or v > best:
            best, bx, by = v, i - k + 1, vals.index(v) + 1
    print(best)
    print(bx, by)

main()
Bài 3Làm mờ ảnhDễ

Ảnh đen trắng là bảng $n \times m$ điểm ảnh ($1 \le n, m \le 500$), mỗi điểm có độ sáng từ 0 đến 255. Phép làm mờ bán kính $r$ ($0 \le r \le 1000$) thay mỗi điểm $(i, j)$ bằng phần nguyên của trung bình cộng độ sáng các điểm $(u, v)$ nằm trong ảnh với $|u - i| \le r$ và $|v - j| \le r$. In ra ảnh sau khi làm mờ.

Input
3 4 1
10 20 30 40
50 60 70 80
90 100 110 120
Output
35 40 50 55
55 60 70 75
75 80 90 95

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Cửa sổ quanh ô $(i, j)$ là hình chữ nhật $(\max(1, i - r), \max(1, j - r)) \to (\min(n, i + r), \min(m, j + r))$: cắt vào trong ảnh bằng max, min. Tổng lấy bằng cộng dồn 2 chiều, số điểm là diện tích hình chữ nhật đã cắt (không phải $(2r + 1)^2$!). Mẫu: góc $(1, 1)$ chỉ có 4 điểm $10, 20, 50, 60$, trung bình 35; ô $(2, 2)$ có đủ 9 điểm, tổng 540, trung bình 60.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

long long S[505][505];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, r;
    cin >> n >> m >> r;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            long long x;
            cin >> x;
            S[i][j] = x + S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];
        }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            int x1 = max(1, i - r), y1 = max(1, j - r);       // cắt cửa sổ vào trong ảnh
            int x2 = min(n, i + r), y2 = min(m, j + r);
            long long s = S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1];
            long long cnt = 1LL * (x2 - x1 + 1) * (y2 - y1 + 1);
            cout << s / cnt << " ";
        }
        cout << "\n";
    }
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, r = int(data[0]), int(data[1]), int(data[2])
    S = [[0] * (m + 1)]                          # hàng 0 là viền 0
    p = 3
    for i in range(1, n + 1):
        row = accumulate(map(int, data[p:p + m]), initial=0)   # cộng dồn hàng i
        p += m
        S.append([u + r for u, r in zip(S[i - 1], row)])
    out = []
    for i in range(1, n + 1):
        x1, x2 = max(1, i - r), min(n, i + r)          # cắt cửa sổ vào trong ảnh
        A, B = S[x2], S[x1 - 1]
        h = x2 - x1 + 1
        row = []
        for j in range(1, m + 1):
            y1, y2 = max(1, j - r), min(m, j + r)
            s = A[y2] - B[y2] - A[y1 - 1] + B[y1 - 1]
            row.append(s // (h * (y2 - y1 + 1)))
        out.append(' '.join(map(str, row)))
    print('\n'.join(out))

main()
Bài 4Tưới vườnTrung bình

Khu vườn là bảng $n \times m$ ô ($1 \le n, m \le 1000$), ban đầu mọi ô khô (lượng nước 0). Có $q$ lần tưới ($q \le 10^5$), lần tưới thứ $t$ đổ thêm $v_t$ lít ($1 \le v_t \le 10^9$) vào mọi ô của hình chữ nhật $(x_1, y_1) \to (x_2, y_2)$. Cây chỉ sống được ở ô nhận ít nhất $K$ lít ($1 \le K \le 10^{18}$). Hỏi có bao nhiêu ô cây sống được?

Dòng đầu: $n$, $m$, $q$, $K$. Tiếp theo $q$ dòng, mỗi dòng $x_1, y_1, x_2, y_2, v$.

Input
4 5 3 5
1 1 2 3 2
2 2 4 5 3
1 4 4 4 4
Output
5

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Mảng hiệu 2 chiều ở phần 4: mỗi lần tưới ghi 4 ô góc, cuối cùng cộng dồn 2 chiều rồi đếm ô có giá trị $\ge K$. Lượng nước tới $10^5 \cdot 10^9 = 10^{14}$ nên dùng long long. Mẫu, sau 3 lần tưới bảng là: hàng 1: 2 2 2 4 0; hàng 2: 2 5 5 7 3; hàng 3 và 4: 0 3 3 7 3. Có 5 ô đạt ít nhất 5 lít.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

long long D[1005][1005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, q;
    long long K;
    cin >> n >> m >> q >> K;
    while(q--){
        int x1, y1, x2, y2;
        long long v;
        cin >> x1 >> y1 >> x2 >> y2 >> v;
        D[x1][y1] += v;
        D[x1][y2 + 1] -= v;
        D[x2 + 1][y1] -= v;
        D[x2 + 1][y2 + 1] += v;
    }
    int cnt = 0;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            D[i][j] += D[i - 1][j] + D[i][j - 1] - D[i - 1][j - 1];
            if(D[i][j] >= K) cnt++;
        }
    cout << cnt;
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, q, K = int(data[0]), int(data[1]), int(data[2]), int(data[3])
    D = [[0] * (m + 2) for _ in range(n + 2)]
    p = 4
    for _ in range(q):
        x1, y1, x2, y2, v = (int(x) for x in data[p:p + 5])
        p += 5
        D[x1][y1] += v
        D[x1][y2 + 1] -= v
        D[x2 + 1][y1] -= v
        D[x2 + 1][y2 + 1] += v
    cnt = 0
    for i in range(1, n + 1):
        D[i] = cur = [u + r for u, r in zip(D[i - 1], accumulate(D[i]))]   # cộng dồn 2 chiều
        cnt += sum(x >= K for x in cur[1:m + 1])
    print(cnt)

main()
Bài 5Đặt trạm phủ sóngTrung bình

Một vùng đất được chia thành bảng $n \times m$ ô ($1 \le n, m \le 1000$). Có $k$ trạm phát sóng ($k \le 10^5$), trạm thứ $t$ đặt ở ô $(x_t, y_t)$ với bán kính $r_t$ ($0 \le r_t \le 2000$) phủ sóng mọi ô $(i, j)$ trong bảng có $|i - x_t| \le r_t$ và $|j - y_t| \le r_t$. In ra hai số: số ô không có sóng, và số trạm lớn nhất cùng phủ lên một ô.

Input
5 6 2
2 2 1
3 4 1
Output
14
2

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Mỗi trạm là thao tác “cộng 1 vào hình chữ nhật” với hình chữ nhật đã cắt vào trong bảng: $(\max(1, x - r), \max(1, y - r)) \to (\min(n, x + r), \min(m, y + r))$. Cộng dồn xong, ô có giá trị 0 là ô không có sóng, giá trị lớn nhất là đáp án thứ hai. Mẫu: trạm 1 phủ hàng 1–3, cột 1–3; trạm 2 phủ hàng 2–4, cột 3–5; hai vùng chung nhau 2 ô $(2, 3)$, $(3, 3)$ nên có $9 + 9 - 2 = 16$ ô có sóng, $30 - 16 = 14$ ô không có.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int D[1005][1005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, k;
    cin >> n >> m >> k;
    while(k--){
        int x, y, r;
        cin >> x >> y >> r;
        int x1 = max(1, x - r), y1 = max(1, y - r);   // cắt vào trong bảng
        int x2 = min(n, x + r), y2 = min(m, y + r);
        D[x1][y1]++;
        D[x1][y2 + 1]--;
        D[x2 + 1][y1]--;
        D[x2 + 1][y2 + 1]++;
    }
    int none = 0, best = 0;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            D[i][j] += D[i - 1][j] + D[i][j - 1] - D[i - 1][j - 1];
            if(D[i][j] == 0) none++;
            best = max(best, D[i][j]);
        }
    cout << none << "\n" << best;
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, k = int(data[0]), int(data[1]), int(data[2])
    D = [[0] * (m + 2) for _ in range(n + 2)]
    p = 3
    for _ in range(k):
        x, y, r = int(data[p]), int(data[p + 1]), int(data[p + 2])
        p += 3
        x1, y1 = max(1, x - r), max(1, y - r)       # cắt vào trong bảng
        x2, y2 = min(n, x + r), min(m, y + r)
        D[x1][y1] += 1
        D[x1][y2 + 1] -= 1
        D[x2 + 1][y1] -= 1
        D[x2 + 1][y2 + 1] += 1
    none = best = 0
    for i in range(1, n + 1):
        D[i] = cur = [u + r for u, r in zip(D[i - 1], accumulate(D[i]))]   # cộng dồn 2 chiều
        row = cur[1:m + 1]
        none += row.count(0)
        best = max(best, max(row))
    print(none)
    print(best)

main()
Bài 6Đếm hình chữ nhật toàn số 1Trung bình

Cho bảng $n \times m$ ($1 \le n, m \le 40$) chỉ gồm chữ số 0 và 1, mỗi hàng là một xâu viết liền. Đếm số hình chữ nhật con (các cạnh song song với cạnh bảng, gồm ít nhất một ô) mà mọi ô đều là 1.

Input
3 4
1101
1111
0111
Output
32

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Dạng 2 của phần 5: duyệt hàng trên $x_1$, hàng dưới $x_2$, cột trái $y_1$, cột phải $y_2$; hình chữ nhật toàn số 1 khi tổng của nó bằng diện tích $(x_2 - x_1 + 1)(y_2 - y_1 + 1)$. Có thể cắt bớt: với $x_1, x_2, y_1$ cố định, khi tăng $y_2$ mà gặp hình chữ nhật có số 0 thì mọi hình rộng hơn cũng chứa số 0, dừng vòng $y_2$ luôn. Mẫu: hàng 2 toàn 1 nên riêng hàng 2 đã có $4 \cdot 5 / 2 = 10$ hình chữ nhật; cộng các hình ở hàng 1, hàng 3 và các hình cao 2, 3 hàng được 32.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int S[45][45];

int main(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++){
        string s;
        cin >> s;
        for(int j = 1; j <= m; j++)
            S[i][j] = (s[j - 1] - '0') + S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];
    }
    long long cnt = 0;
    for(int x1 = 1; x1 <= n; x1++)
        for(int x2 = x1; x2 <= n; x2++)
            for(int y1 = 1; y1 <= m; y1++)
                for(int y2 = y1; y2 <= m; y2++){
                    int s = S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1];
                    if(s != (x2 - x1 + 1) * (y2 - y1 + 1)) break;   // có số 0: rộng hơn cũng hỏng
                    cnt++;
                }
    cout << cnt;
    return 0;
}
import sys

def main():
    data = sys.stdin.read().split()
    n, m = int(data[0]), int(data[1])
    S = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        s = data[1 + i]
        up, cur = S[i - 1], S[i]
        for j in range(1, m + 1):
            cur[j] = (s[j - 1] == '1') + up[j] + cur[j - 1] - up[j - 1]
    cnt = 0
    for x1 in range(1, n + 1):
        B = S[x1 - 1]
        for x2 in range(x1, n + 1):
            A, h = S[x2], x2 - x1 + 1
            for y1 in range(1, m + 1):
                for y2 in range(y1, m + 1):
                    if A[y2] - B[y2] - A[y1 - 1] + B[y1 - 1] != h * (y2 - y1 + 1):
                        break               # có số 0: rộng hơn cũng hỏng
                    cnt += 1
    print(cnt)

main()
Bài 7Hình vuông lớn nhấtKhá

Cho bảng $n \times m$ số nguyên không âm ($1 \le n, m \le 300$, $0 \le a_{ij} \le 10^9$) và số $T$ ($0 \le T \le 10^{18}$). Tìm cạnh $k$ lớn nhất sao cho có ít nhất một hình vuông con $k \times k$ có tổng không vượt quá $T$. Nếu mọi ô đều lớn hơn $T$ thì in 0.

Input
4 5 40
3 1 4 1 5
9 2 6 5 3
5 8 9 7 9
3 2 3 8 4
Output
2

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Dạng 4 của phần 5. Vì các số không âm, có hình vuông cạnh $k$ thoả thì cũng có hình vuông cạnh $k - 1$ thoả (lấy hình vuông nhỏ nằm bên trong). Chặt nhị phân $k$ trên $[0, \min(n, m)]$, hàm kiểm tra duyệt mọi vị trí như bài 2 và trả về ngay khi gặp tổng $\le T$. Mẫu: hình vuông $2 \times 2$ nhỏ nhất có tổng $13$, hình vuông $3 \times 3$ nhỏ nhất có tổng $43 > 40$, nên đáp án là 2.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, m;
long long T, S[305][305];

bool ok(int k){                       // có hình vuông k x k tổng <= T không?
    if(k == 0) return true;
    for(int i = k; i <= n; i++)
        for(int j = k; j <= m; j++)
            if(S[i][j] - S[i - k][j] - S[i][j - k] + S[i - k][j - k] <= T) return true;
    return false;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> m >> T;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            long long x;
            cin >> x;
            S[i][j] = x + S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];
        }
    int lo = 0, hi = min(n, m);       // ok(lo) luôn đúng
    while(lo < hi){
        int mid = (lo + hi + 1) / 2;
        if(ok(mid)) lo = mid;
        else hi = mid - 1;
    }
    cout << lo;
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, T = int(data[0]), int(data[1]), int(data[2])
    S = [[0] * (m + 1)]                          # hàng 0 là viền 0
    p = 3
    for i in range(1, n + 1):
        row = accumulate(map(int, data[p:p + m]), initial=0)   # cộng dồn hàng i
        p += m
        S.append([u + r for u, r in zip(S[i - 1], row)])

    def ok(k):                         # có hình vuông k x k tổng <= T không?
        if k == 0:
            return True
        for i in range(k, n + 1):
            A, B = S[i], S[i - k]
            for j in range(k, m + 1):
                if A[j] - B[j] - A[j - k] + B[j - k] <= T:
                    return True
        return False

    lo, hi = 0, min(n, m)              # ok(lo) luôn đúng
    while lo < hi:
        mid = (lo + hi + 1) // 2
        if ok(mid):
            lo = mid
        else:
            hi = mid - 1
    print(lo)

main()
Bài 8Hình chữ nhật có tổng TKhá

Cho bảng $n \times m$ số nguyên ($1 \le n, m \le 100$, $|a_{ij}| \le 1000$, có thể âm) và số $T$ ($|T| \le 10^7$). Đếm số hình chữ nhật con có tổng đúng bằng $T$.

Input
2 3 3
1 2 0
2 1 3
Output
7

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Dạng 5 của phần 5. Cố định hàng trên $x_1$ và hàng dưới $x_2$; gọi $c_j$ là tổng cột $j$ từ hàng $x_1$ tới $x_2$ (khi tăng $x_2$ chỉ việc cộng thêm hàng $x_2$ vào từng $c_j$, hoặc lấy $S[x_2][j] - S[x_1 - 1][j] - \ldots$ từ bảng cộng dồn). Bài toán trở thành đếm đoạn con của dãy $c$ có tổng $T$: duyệt cộng dồn $P$, đếm số tiền tố trước đó bằng $P - T$ bằng map (dict). Tổng $O(n^2 m \log m)$. Mẫu có 7 hình: $[1\ 2]$, $[1\ 2\ 0]$, $[2\ 1]$ ở hàng 2, ô $3$, và ba cột $1 + 2$, $2 + 1$, $0 + 3$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int a[105][105];
long long c[105];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m;
    long long T;
    cin >> n >> m >> T;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++) cin >> a[i][j];
    long long ans = 0;
    for(int x1 = 1; x1 <= n; x1++){
        for(int j = 1; j <= m; j++) c[j] = 0;
        for(int x2 = x1; x2 <= n; x2++){
            for(int j = 1; j <= m; j++) c[j] += a[x2][j];   // c[j] = tổng cột j từ hàng x1 tới x2
            map<long long, int> cnt;                       // đếm tiền tố của dãy c
            cnt[0] = 1;
            long long P = 0;
            for(int j = 1; j <= m; j++){
                P += c[j];
                auto it = cnt.find(P - T);
                if(it != cnt.end()) ans += it->second;
                cnt[P]++;
            }
        }
    }
    cout << ans;
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, m, T = int(data[0]), int(data[1]), int(data[2])
    a = [[int(x) for x in data[3 + i * m: 3 + (i + 1) * m]] for i in range(n)]
    ans = 0
    for x1 in range(n):
        c = [0] * m
        for x2 in range(x1, n):
            row = a[x2]
            for j in range(m):
                c[j] += row[j]             # c[j] = tổng cột j từ hàng x1 tới x2
            cnt = {0: 1}                   # đếm tiền tố của dãy c
            P = 0
            for x in c:
                P += x
                ans += cnt.get(P - T, 0)
                cnt[P] = cnt.get(P, 0) + 1
    print(ans)

main()
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét