- Khai báo mảng cộng dồn
prefcó kích thước n + 1 vớipref[0] = 0giúp công thức tổng đoạn[l, r]luôn làpref[r + 1] - pref[l], không cần viếtif (l == 0)riêng. - Tiền xử lý mất O(n), mỗi truy vấn tổng đoạn mất O(1); tổng thời gian cho
qtruy vấn giảm từO(n × q)xuống O(n + q). - Ngược lại với truy vấn tổng đoạn, khi cần cộng thêm một giá trị vào nhiều đoạn
[l, r], ta dùng mảng hiệu (Difference Array) để cập nhật mỗi đoạn trongO(1).
Bài toán mở đầu: Vì sao cộng bằng vòng lặp bị quá thời gian?
Giả sử bạn có một mảng a gồm n = 100.000 phần tử ghi doanh thu mỗi ngày của cửa hàng, và cần trả lời q = 100.000 câu hỏi dạng: "Tổng doanh thu từ ngày thứ l đến ngày thứ r là bao nhiêu?".
Nếu với mỗi câu hỏi bạn đều chạy vòng lặp for từ l đến r để cộng từng số:
- Một câu hỏi hỏi từ đầu mảng đến cuối mảng tốn
nphép cộng. - Thực hiện
qcâu hỏi như vậy sẽ tốn khoảngn × q = 10¹⁰phép tính. Như đã phân tích trong bài Độ phức tạp thuật toán Big-O, 10 tỷ phép tính mất hàng chục giây và chắc chắn bị báo lỗi chạy quá thời gian (TLE).
Nguyên nhân gây lãng phí là những đoạn giao nhau bị cộng đi cộng lại hàng chục nghìn lần. Mảng cộng dồn (Prefix Sum) giải quyết triệt để việc này bằng cách cộng dồn một lần duy nhất ngay từ đầu.
Ý tưởng và công thức chuẩn không sợ lệch chỉ số
Xét mảng a có n phần tử đánh chỉ số từ 0 đến n - 1. Thay vì tạo mảng cộng dồn có cùng độ dài n, cách viết sạch nhất và ít lỗi nhất là tạo mảng pref có độ dài n + 1, trong đó:
pref[0] = 0(tổng của 0 phần tử đầu tiên).pref[k]là tổng củakphần tử đầu tiên trong mảnga(tức làa[0] + a[1] + ... + a[k - 1]).- Công thức xây dựng nối tiếp:
pref[i + 1] = pref[i] + a[i].
Khi cần tính tổng các phần tử từ vị trí l đến vị trí r (tức a[l] + a[l + 1] + ... + a[r]), ta chỉ việc lấy tổng từ đầu mảng đến hết vị trí r (chính là pref[r + 1]), rồi bớt đi phần dư ở phía trước vị trí l (là a[0] + ... + a[l - 1], tức pref[l]):
Tổng đoạn [l..r] = pref[r + 1] − pref[l]
Hãy xem ví dụ với mảng 7 phần tử a = [3, 1, 4, 1, 5, 9, 2] khi cần lấy tổng đoạn từ l = 2 đến r = 5:
| Chỉ số k | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| Phần tử a[k] | 3 | 1 | 4 | 1 | 5 | 9 | 2 | — |
| pref[k] | 0 | 3 | 4 | 8 | 9 | 14 | 23 | 25 |
- Tổng đoạn
[2, 5](4 + 1 + 5 + 9):pref[5 + 1] - pref[2] = pref[6] - pref[2] = 23 - 4 = 19. - Tổng đoạn
[0, 3](3 + 1 + 4 + 1):pref[3 + 1] - pref[0] = pref[4] - pref[0] = 9 - 0 = 9. Nhờ cópref[0] = 0, ngay cả khil = 0công thức vẫn đúng tuyệt đối mà không cần rẽ nhánhif.
Code mảng cộng dồn 1 chiều bằng C++ và Python
Lưu ý trong C++, dù các phần tử a[i] vừa trong kiểu int (tới 109), tổng của nhiều phần tử cộng lại rất dễ vượt quá 2 × 109, nên mảng pref luôn phải khai báo kiểu long long:
#include <iostream>
#include <vector>
using namespace std;
// Xây dựng mảng cộng dồn độ dài n + 1 (pref[0] = 0)
vector<long long> taoMangCongDon(const vector<int>& a) {
int n = (int)a.size();
vector<long long> pref(n + 1, 0);
for (int i = 0; i < n; ++i) {
pref[i + 1] = pref[i] + a[i];
}
return pref;
}
// Trả về tổng đoạn [l, r] (chỉ số 0-based) trong O(1)
long long tongDoan(const vector<long long>& pref, int l, int r) {
return pref[r + 1] - pref[l];
}
int main() {
vector<int> a = {3, 1, 4, 1, 5, 9, 2};
vector<long long> pref = taoMangCongDon(a);
cout << tongDoan(pref, 2, 5) << "\n"; // 19 (4 + 1 + 5 + 9)
cout << tongDoan(pref, 0, 3) << "\n"; // 9 (3 + 1 + 4 + 1)
return 0;
}
def tao_mang_cong_don(a):
"""Xây dựng mảng cộng dồn độ dài n + 1 (pref[0] = 0)."""
pref = [0] * (len(a) + 1)
for i, x in enumerate(a):
pref[i + 1] = pref[i] + x
return pref
def tong_doan(pref, l, r):
"""Trả về tổng đoạn [l, r] (chỉ số 0-based) trong O(1)."""
return pref[r + 1] - pref[l]
a = [3, 1, 4, 1, 5, 9, 2]
pref = tao_mang_cong_don(a)
print(tong_doan(pref, 2, 5)) # 19
print(tong_doan(pref, 0, 3)) # 9
Chủ đề này nằm trong Lecture 03 - Prefix Sum (Mảng cộng dồn) của Khóa Blue 2.0. Xem đề cương
Bài toán ngược: Cập nhật nhiều đoạn nhanh bằng Mảng hiệu (Difference Array)
Mảng cộng dồn giúp lấy tổng đoạn trong O(1). Nhưng nếu đề bài đổi ngược lại: cho mảng ban đầu toàn số 0, thực hiện q thao tác cộng thêm giá trị v vào mọi phần tử thuộc đoạn [l, r], rồi sau khi thực hiện xong tất cả các thao tác mới hỏi mảng cuối cùng là gì?
Lúc này ta dùng phép toán ngược của mảng cộng dồn, gọi là mảng hiệu (Difference Array) diff:
- Muốn cộng thêm
vvào đoạn[l, r], ta chỉ sửa đúng 2 vị trí trongO(1):diff[l] += v(bắt đầu từ vị tríl, giá trị cộng dồn tăng thêmv).diff[r + 1] -= v(từ vị trír + 1trở đi, triệt tiêu lượng tăng thêmvđó).
- Sau khi xử lý xong toàn bộ
qthao tác cập nhật, ta chỉ cần cộng dồn mảngdiffmột lần duy nhất từ trái sang phải trongO(n)là thu được giá trị cuối cùng của mọi phần tử!
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n = 5;
vector<long long> diff(n + 1, 0);
// Cộng một giá trị v vào mọi phần tử thuộc đoạn [l, r] trong O(1)
auto congDoan = [&](int l, int r, int v) {
diff[l] += v;
diff[r + 1] -= v;
};
congDoan(1, 3, 5); // [0, 5, 5, 5, 0]
congDoan(2, 4, 10); // [0, 5, 15, 15, 10]
congDoan(0, 2, 2); // [2, 7, 17, 15, 10]
// Cộng dồn mảng diff một lần để khôi phục mảng kết quả
vector<long long> a(n);
long long cur = 0;
for (int i = 0; i < n; ++i) {
cur += diff[i];
a[i] = cur;
cout << a[i] << (i + 1 == n ? "\n" : " "); // 2 7 17 15 10
}
return 0;
}
n = 5
diff = [0] * (n + 1)
def cong_doan(l, r, v):
diff[l] += v
diff[r + 1] -= v
cong_doan(1, 3, 5) # [0, 5, 5, 5, 0]
cong_doan(2, 4, 10) # [0, 5, 15, 15, 10]
cong_doan(0, 2, 2) # [2, 7, 17, 15, 10]
a = []
cur = 0
for i in range(n):
cur += diff[i]
a.append(cur)
print(*a) # 2 7 17 15 10
Mở rộng sang Mảng cộng dồn 2 chiều (trên bảng lưới)
Khi dữ liệu là một bảng hình chữ nhật m hàng, n cột, ta định nghĩa pref[i][j] là tổng tất cả các ô nằm trong góc hình chữ nhật từ ô đầu bảng đến hết hàng i, cột j. Theo nguyên lý bao hàm — loại trừ:
- Khi xây dựng bảng:
pref[i][j] = pref[i - 1][j] + pref[i][j - 1] - pref[i - 1][j - 1] + a[i - 1][j - 1](vì phần góc trên bên tráipref[i - 1][j - 1]được cộng hai lần nên phải trừ bớt một lần). - Khi tính tổng hình chữ nhật con có góc trên trái
(r1, c1)và góc dưới phải(r2, c2)(0-based):Tổng = pref[r2 + 1][c2 + 1] - pref[r1][c2 + 1] - pref[r2 + 1][c1] + pref[r1][c1].
Mọi truy vấn tổng hình chữ nhật con có kích thước bất kỳ đều chỉ tốn đúng 3 phép cộng trừ trong O(1).
Khi nào dùng Mảng cộng dồn, khi nào cần cấu trúc dữ liệu khác?
| Yêu cầu của đề bài | Công cụ phù hợp nhất | Độ phức tạp |
|---|---|---|
Mảng cố định không đổi, hỏi tổng đoạn [l, r] nhiều lần |
Mảng cộng dồn (Prefix Sum) | Tiền xử lý O(n), mỗi truy vấn O(1) |
Cập nhật cộng thêm vào nhiều đoạn [l, r], chỉ lấy mảng kết quả ở cuối cùng |
Mảng hiệu (Difference Array) | Mỗi cập nhật O(1), khôi phục cuối O(n) |
Tìm đoạn con có tổng bằng k trên mảng toàn số dương |
Kỹ thuật hai con trỏ / Cửa sổ trượt | O(n) thời gian, O(1) bộ nhớ |
| Vừa thay đổi giá trị phần tử, vừa hỏi tổng đoạn xen kẽ nhau liên tục | Cây Fenwick (BIT) hoặc Cây phân đoạn (Segment Tree) | O(log n) cho cả cập nhật và truy vấn |
Lỗi hay gặp
- Tràn số nguyên 32-bit (
int) trong C++. Đây là lỗi phổ biến số một khi làm bài mảng cộng dồn. Mảng10^5phần tử mà mỗi phần tử bằng10^5thì tổng đã lên tới10^10(vượt quá giới hạn2.14 × 10^9củaint). Hãy luôn dùngvector<long long> pref. - Truy cập
pref[l - 1]khil = 0trên mảng 0-based. Nếu bạn dùng mảngprefcó cùng độ dàinvà viếtpref[r] - pref[l - 1], khil = 0chỉ số trở thành-1(truy cập ngoài vùng nhớ trong C++, hoặc lấy nhầm phần tử cuối mảng trong Python!). Hãy dùng mảngprefđộ dàin + 1với công thứcpref[r + 1] - pref[l]. - Dùng mảng cộng dồn khi mảng bị sửa đổi xen kẽ với truy vấn. Mỗi khi một phần tử
a[i]thay đổi, mọi giá trịpref[k]phía sauiđều phải tính lại mấtO(n). Với bài toán vừa sửa vừa hỏi xen kẽ, mảng cộng dồn không còn nhanh hơn vòng lặp thường.
Luyện ngay trên Big-O Coding
Các bài tập dưới đây trên hệ thống Big-O Coding sử dụng trực tiếp mảng cộng dồn 1 chiều, 2 chiều và mảng hiệu:
Câu hỏi thường gặp
Mảng cộng dồn có dùng được khi mảng có số âm không?
Hoàn toàn dùng được. Phép cộng và phép trừ pref[r + 1] - pref[l] đúng với mọi số nguyên âm, dương hay số 0. Lưu ý duy nhất là khi có số âm, mảng pref không còn tăng dần đơn điệu nữa nên bạn không thể áp dụng tìm kiếm nhị phân trực tiếp trên mảng pref.
Có thể dùng ý tưởng cộng dồn cho phép nhân hoặc phép XOR không?
Được với phép XOR (vì x ^ x = 0, nên XOR đoạn [l, r] bằng prefXor[r + 1] ^ prefXor[l]). Với phép nhân, bạn chỉ chia ngược được khi trên đoạn không có số 0 và các giá trị không bị tràn số (hoặc dùng nghịch đảo modulo). Còn với phép tìm min / max trên đoạn, bạn không thể "trừ ngược" được mà phải dùng Bảng thưa (Sparse Table) hoặc Cây phân đoạn.
Đếm số phần tử chẵn trong đoạn [l, r] bằng mảng cộng dồn như thế nào?
Rất đơn giản: bạn coi mỗi số chẵn có giá trị đóng góp là 1, mỗi số lẻ có giá trị đóng góp là 0, rồi lập mảng cộng dồn trên dãy 0/1 đó. Số lượng số chẵn trong đoạn [l, r] chính là prefChan[r + 1] - prefChan[l].
Luyện phỏng vấn Google, Meta, Amazon theo Interview Patterns: 16 buổi online live, lớp 7–15 người, C++/Python/Java. Học thử buổi 1 miễn phí.


