Mảng & Xâu ký tự
Mảng (array) và xâu ký tự (string) là hai cấu trúc dữ liệu nền tảng nhất trong lập trình thi đấu: gần như mọi bài toán đều bắt đầu từ việc đọc dữ liệu vào một mảng hoặc một xâu. Trang này tổng hợp cách dùng chúng trong C++/Python, và quan trọng nhất là hai kỹ thuật tăng tốc kinh điển — tổng tiền tố (prefix sum) và mảng hiệu (difference array) — giúp trả lời truy vấn tổng đoạn hoặc cập nhật đoạn trong thay vì mỗi lần, đưa nhiều bài từ về .
Mảng (Array)
Mảng lưu trữ các phần tử cùng kiểu liên tiếp trong bộ nhớ. Nhờ liên tiếp, địa chỉ phần tử thứ tính được bằng một phép nhân–cộng, nên truy cập theo chỉ số là .
C++
int a[100005]; // mảng tĩnh, đủ lớn, khai báo ngoài main để nằm vùng nhớ global
vector<int> v = {1, 2, 3}; // mảng động
v.push_back(6); // thêm cuối: O(1) khấu trừ (amortized)
v.pop_back(); // xóa cuối: O(1)
v[i]; // truy cập: O(1)
int n = v.size();
vector<vector<int>> mat(n, vector<int>(m, 0)); // ma trận n x m, khởi tạo 0
Python
a = [1, 2, 3]
a.append(6)
a.pop()
a[i]
mat = [[0] * m for _ in range(n)] # KHÔNG dùng [[0]*m]*n (các hàng trỏ chung 1 list!)
Xâu ký tự (String)
C++
string s = "hello";
s.length(); // độ dài
s[i]; // ký tự thứ i (char)
s.substr(l, len); // xâu con bắt đầu ở l, dài len
s.find("ell"); // vị trí xuất hiện đầu tiên, hoặc string::npos nếu không có
s += " world";
for (char c : s) { /* ... */ }
Python
s = "hello"
len(s); s[i]; s[l:r]
s.find("ell")
s.split()
",".join(lst)
Ý tưởng / Trực giác
Tổng tiền tố. Giả sử ta phải trả lời nhiều truy vấn "tổng các phần tử từ đến ". Tính trực tiếp mỗi truy vấn mất , truy vấn là — quá chậm. Quan sát then chốt: nếu đặt (tổng của phần tử đầu, với ), thì tổng đoạn chính là Lý do: gồm mọi phần tử tới chỉ số , còn gồm mọi phần tử trước ; trừ đi thì phần đầu triệt tiêu, đúng bằng đoạn cần. Mỗi truy vấn chỉ còn một phép trừ . Đây là ý tưởng "tính trước phần dùng chung, sau đó tái sử dụng".
Mảng hiệu là bài toán đối ngẫu: thay vì truy vấn đoạn, ta cần cộng một giá trị vào toàn bộ đoạn nhiều lần, rồi cuối cùng mới đọc mảng. Thay vì sửa từng phần tử ( mỗi lần), ta chỉ ghi nhận "tại bắt đầu tăng thêm , tại thì hết tăng": , . Sau khi xử lý hết, lấy tổng tiền tố của sẽ ra mảng kết quả. Trực giác: tổng tiền tố và mảng hiệu là hai phép toán nghịch đảo của nhau (giống đạo hàm và tích phân rời rạc).
Ví dụ chạy tay (tổng tiền tố)
Mảng (0-indexed). Tính :
i : 0 1 2 3 4 5
a : 3 1 4 1 5
pre: 0 3 4 8 9 14
^cộng dồn: pre[i+1] = pre[i] + a[i]
Truy vấn tổng đoạn (tức ):
pre[4] - pre[1] = 9 - 3 = 6 ✔
└────────┘
bỏ đi 3 phần tử trước l=1, giữ tới r=3
Ví dụ chạy tay (mảng hiệu)
Ban đầu . Thực hiện hai thao tác cộng đoạn: cộng vào , rồi cộng vào .
Bước 1: +2 vào [1,3] -> diff[1]+=2, diff[4]-=2
Bước 2: +5 vào [0,2] -> diff[0]+=5, diff[3]-=5
idx : 0 1 2 3 4
diff: 5 2 0 -5 -2
^op2 ^op1 ^op2 ^op1
(đóng)(đóng)
Lấy tổng tiền tố của diff để khôi phục a:
a[0]=5
a[1]=5+2=7
a[2]=7+0=7
a[3]=7-5=2
a[4]=2-2=0
=> a = [5, 7, 7, 2, 0] ✔ (đúng: [0,1,2] +5, [1,2,3] +2)
Cài đặt
Tổng tiền tố 1D (C++):
vector<long long> pre(n + 1, 0); // long long: tổng có thể tràn int
for (int i = 0; i < n; i++)
pre[i + 1] = pre[i] + a[i]; // pre[i+1] gồm i+1 phần tử đầu
// tổng đoạn [l, r] (0-indexed, gồm cả r):
long long sum = pre[r + 1] - pre[l];
Mảng hiệu 1D (C++):
vector<long long> diff(n + 1, 0);
// cộng v vào mọi phần tử trong [l, r]:
diff[l] += v;
if (r + 1 < (int)diff.size()) diff[r + 1] -= v;
// khôi phục mảng cuối cùng bằng tổng tiền tố:
vector<long long> a(n);
long long run = 0;
for (int i = 0; i < n; i++) { run += diff[i]; a[i] = run; }
Tổng tiền tố 2D (cho ma trận, dùng trong bài chụp ảnh ):
// pre[i][j] = tổng vùng từ (0,0) tới (i-1,j-1)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
pre[i][j] = g[i-1][j-1] + pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1];
// tổng hình chữ nhật góc trên-trái (r1,c1), góc dưới-phải (r2,c2) (0-indexed):
long long s = pre[r2+1][c2+1] - pre[r1][c2+1] - pre[r2+1][c1] + pre[r1][c1];
Python (tổng tiền tố 1D):
pre = [0] * (n + 1)
for i in range(n):
pre[i + 1] = pre[i] + a[i]
total = pre[r + 1] - pre[l]
Độ phức tạp
- Tổng tiền tố: dựng mảng duyệt 1 lần ; mỗi truy vấn đoạn (một phép trừ). Tổng truy vấn: thay cho . Bộ nhớ cho mảng phụ.
- Tổng tiền tố 2D: dựng , mỗi truy vấn hình chữ nhật (bốn phép cộng/trừ). Bộ nhớ .
- Mảng hiệu: mỗi thao tác cộng đoạn (sửa 2 ô); khôi phục cuối cùng . Với thao tác: . Bộ nhớ .
Lý do mỗi truy vấn: công thức chỉ đụng tới một số hằng ô của mảng phụ, không phụ thuộc độ dài đoạn.
⚠️ Lỗi thường gặp
- Tràn số (overflow). Tổng của tới phần tử mỗi phần tử cỡ vượt xa giới hạn
int(). Luôn để mảng kiểulong longtrong C++; quên điều này cho kết quả âm/sai khó debug. - Lệch chỉ số (off-by-one). Quy ước = tổng phần tử đầu nên tổng là , không phải . Nhầm sẽ thiếu hoặc thừa đúng một phần tử ở biên.
- Sai biên mảng hiệu. Khi cộng đoạn phải trừ tại ; nếu bằng (ngoài mảng gốc) cần cấp mảng kích thước , nếu không sẽ ghi tràn (out-of-bounds) hoặc bỏ sót việc "đóng" đoạn.
- Khởi tạo ma trận Python sai.
[[0]*m]*ntạo tham chiếu tới cùng một list — sửa một hàng làm mọi hàng đổi theo. Dùng[[0]*m for _ in range(n)]. - Nối xâu trong vòng lặp . Trong C++
s += clặp lại nhiều lần với chuỗi dài, hay trong Pythons = s + c(str là immutable), đều tốn . Dùngstringstream/vector<char>ở C++ và"".join(list)ở Python. - Quên modulo khi dùng tổng tiền tố để đếm theo số dư. Khi đếm dãy con có tổng chia hết cho , ta so sánh theo số dư; nhớ lấy để tránh số dư âm nếu mảng có giá trị âm.
Biến thể / Mở rộng
- Tổng tiền tố trên số dư (đếm dãy con có tổng chia hết cho ): xem bài
div7ở dưới. - Tổng tiền tố 2D cho truy vấn tổng hình chữ nhật.
- Khi cần truy vấn đoạn xen kẽ với cập nhật điểm, tổng tiền tố không còn đủ — chuyển sang Cây Fenwick hoặc Cây phân đoạn.
Bài tập luyện
- Biên Độ Điểm Quidditch (quidrange) — (Sơ cấp) Đọc mảng và tìm max − min: làm quen thao tác duyệt mảng cơ bản.
- Ký Tự Phổ Biến (charfreq) — (Học việc) Đếm tần suất ký tự bằng mảng đếm 26 phần tử trên một xâu.
- Chụp Ảnh (photos26) — (Học việc) Tổng tiền tố 2D để lấy tổng mọi vùng nhanh.
- Đếm giống bò (bcount) — (Trung cấp) Dùng tổng tiền tố theo từng loại để trả lời truy vấn đếm trên đoạn trong .
- Dãy con chia hết cho 7 (div7) — (Trung cấp) Tổng tiền tố kết hợp số dư modulo để tìm dãy con dài nhất chia hết cho 7.
- Toa xe Hogwarts Express (hogexpress) — (Trung cấp) Mảng hiệu: cộng vào nhiều đoạn rồi tìm vị trí có giá trị lớn nhất.