- Tọa độ trên lưới
R × C: Phần tửa[i][j]nằm ở hàngi(0 ≤ i < R, tính từ trên xuống) và cộtj(0 ≤ j < C, tính từ trái sang phải). - Bẫy khởi tạo trong Python: Tuyệt đối không dùng
[[0] * m] * nvì cảnhàng sẽ trỏ chung vào một danh sách; phải viết[[0] * m for _ in range(n)]. - Công thức đường chéo trên ma trận vuông
n × n: Đường chéo chính thỏa mãni == j(hoặc hiệui - jkhông đổi); đường chéo phụ thỏa mãni + j == n - 1(hoặc tổngi + jkhông đổi). - Kỹ thuật mảng hướng
dx, dy: Giúp duyệt 4 ô kề cạnh hoặc 8 ô kề góc chỉ bằng một vòng lặp 4 hoặc 8 bước thay vì viết 8 câu lệnhifrời rạc.
Biểu diễn bảng lưới R × C và bẫy khởi tạo trong Python
Trong lập trình thuật toán, một bảng lưới gồm n hàng và m cột thực chất là một "mảng của các mảng 1 chiều", trong đó phần tử thứ i là một hàng gồm m cột:
- Trong C++: Cách an toàn và linh hoạt nhất là dùng
vector<vector<int>> a(n, vector<int>(m, 0));. Khi đóa.size()là số hàngn, và, khin > 0,a[0].size()là số cộtm. Không truy cập hàng 0 khi ma trận rỗng. - Trong Python: Ta dùng danh sách lồng nhau (nested list):
a = [[0] * m for _ in range(n)].
Cảnh báo lỗi kinh điển: Vì sao [[0] * m] * n trong Python làm hỏng toàn bộ ma trận?
Rất nhiều bạn mới học Python quen viết [0] * m để tạo mảng 1 chiều nên viết luôn a = [[0] * m] * n để tạo mảng 2 chiều. Đây là một lỗi cực kỳ nguy hiểm:
- Toán tử
* nở ngoài không tạo ranhàng mới, mà chỉ sao chépntham chiếu cùng trỏ tới đúng một danh sách hàng duy nhất trong bộ nhớ! - Hệ quả là khi bạn gán
a[0][0] = 9, thìa[1][0],a[2][0]... ở tất cả các hàng khác cũng tự động biến thành9! - Cách viết đúng bắt buộc phải dùng list comprehension:
a = [[0] * m for _ in range(n)]để mỗi vòng lặp của_tạo mới một hàng độc lập.
Duyệt theo hàng, theo cột và công thức hai đường chéo
Khi làm việc trên ma trận vuông kích thước n × n (đánh chỉ số từ 0 đến n - 1), bốn thao tác cơ bản nhất là tính toán theo từng hàng, từng cột và hai đường chéo:
| Hướng duyệt | Bất biến chỉ số (i, j) |
Ví dụ trên ma trận 3 × 3 |
|---|---|---|
Hàng thứ i |
Chỉ số hàng i cố định, cột j chạy từ 0 đến n - 1 |
Hàng 0: (0, 0), (0, 1), (0, 2) |
Cột thứ j |
Chỉ số cột j cố định, hàng i chạy từ 0 đến n - 1 |
Cột 1: (0, 1), (1, 1), (2, 1) |
| Đường chéo chính (trái trên ↘ phải dưới) | i == j (tổng quát mọi đường song song chéo chính có hiệu i - j là hằng số) |
(0, 0), (1, 1), (2, 2) |
| Đường chéo phụ (phải trên ↙ trái dưới) | i + j == n - 1 (tổng quát mọi đường song song chéo phụ có tổng i + j là hằng số) |
(0, 2), (1, 1), (2, 0) |
Nhận xét về tổng i + j và hiệu i - j cực kỳ hữu ích: chẳng hạn trong bài toán xếp n quân hậu trên bàn cờ vua (thuật toán quay lui), hai ô (i₁, j₁) và (i₂, j₂) nằm trên cùng một đường chéo khi và chỉ khi i₁ - j₁ == i₂ - j₂ hoặc i₁ + j₁ == i₂ + j₂.
Thứ tự duyệt hàng trước, cột sau để tối ưu bộ nhớ đệm CPU (Row-major order)
Mảng C++ hai chiều cố định lưu liên tiếp theo hàng (row-major order). Với vector<vector<int>>, từng hàng có vùng nhớ riêng; trong CPython, từng list hàng lưu liên tiếp các con trỏ, còn các đối tượng số có thể ở nơi khác. Vì vậy, khi dùng hai vòng lặp lồng nhau để quét toàn bộ ma trận, bạn nên đặt vòng lặp hàng i ở ngoài và vòng lặp cột j ở trong:
- Khi duyệt
a[i][0], a[i][1], a[i][2]..., bộ nhớ đệm L1/L2 Cache của CPU nạp sẵn cả dải ô nhớ kế tiếp nên truy cập cực nhanh. - Nếu đảo ngược cột
jở ngoài, hàngiở trong (a[0][j], a[1][j], a[2][j]...), việc truy cập lần lượt nhiều hàng có thể kém thuận lợi cho bộ nhớ đệm. Mức ảnh hưởng tùy cách lưu dữ liệu và máy; bài này không đưa ra số đo tốc độ.
#include <iostream>
#include <vector>
using namespace std;
// Tính tổng từng hàng, từng cột, đường chéo chính và đường chéo phụ của ma trận vuông n x n
void thong_ke_ma_tran_vuong(const vector<vector<int>>& a) {
int n = static_cast<int>(a.size());
vector<int> tong_hang(n, 0), tong_cot(n, 0);
int cheo_chinh = 0, cheo_phu = 0;
// Duyệt hàng i trước, cột j sau (tối ưu bộ nhớ đệm CPU - row-major order)
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
tong_hang[i] += a[i][j];
tong_cot[j] += a[i][j];
if (i == j) {
cheo_chinh += a[i][j];
}
if (i + j == n - 1) {
cheo_phu += a[i][j];
}
}
}
for (int i = 0; i < n; ++i) {
cout << tong_hang[i] << (i + 1 == n ? '\n' : ' ');
}
for (int j = 0; j < n; ++j) {
cout << tong_cot[j] << (j + 1 == n ? '\n' : ' ');
}
cout << cheo_chinh << " " << cheo_phu << "\n";
}
int main() {
vector<vector<int>> a = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
thong_ke_ma_tran_vuong(a);
return 0;
}
def tao_ma_tran_dung(n: int, m: int) -> list[list[int]]:
"""Tạo ma trận n hàng, m cột độc lập (KHÔNG dùng [[0] * m] * n)."""
return [[0] * m for _ in range(n)]
def thong_ke_ma_tran_vuong(a: list[list[int]]) -> tuple[list[int], list[int], int, int]:
"""Tính tổng từng hàng, từng cột, đường chéo chính và đường chéo phụ."""
n = len(a)
tong_hang = [0] * n
tong_cot = [0] * n
cheo_chinh = 0
cheo_phu = 0
for i in range(n):
for j in range(n):
tong_hang[i] += a[i][j]
tong_cot[j] += a[i][j]
if i == j:
cheo_chinh += a[i][j]
if i + j == n - 1:
cheo_phu += a[i][j]
return tong_hang, tong_cot, cheo_chinh, cheo_phu
if __name__ == "__main__":
# Kiểm chứng khởi tạo đúng: sửa 1 ô không làm đổi các hàng khác
mat = tao_ma_tran_dung(3, 3)
mat[0][0] = 99
assert mat[1][0] == 0 and mat[2][0] == 0
a = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
tong_hang, tong_cot, cheo_chinh, cheo_phu = thong_ke_ma_tran_vuong(a)
print(*tong_hang)
print(*tong_cot)
print(cheo_chinh, cheo_phu)
Với ma trận 3 × 3 gồm các số từ 1 đến 9, cả hai chương trình đều in ra:
- Tổng từng hàng:
6 15 24(1+2+3,4+5+6,7+8+9) - Tổng từng cột:
12 15 18(1+4+7,2+5+8,3+6+9) - Tổng đường chéo chính và đường chéo phụ:
15 15(1+5+9 = 15và3+5+7 = 15).
Chủ đề này nằm trong Lecture 06: 2D Arrays (Mảng 2 chiều) của Khóa Green. Xem đề cương
Kỹ thuật mảng hướng dx, dy để duyệt 4 hướng và 8 hướng trên lưới
Trong các bài toán trên bảng lưới (như đếm số vùng liên thông, tìm đỉnh núi cao hơn xung quanh, hay mô phỏng trò chơi Dò mìn), từ một ô trung tâm (r, c), ta thường xuyên phải xét các ô lân cận của nó:
Nếu bạn viết thủ công 4 hoặc 8 câu lệnh if cho từng ô a[r - 1][c], a[r + 1][c], a[r][c - 1], a[r][c + 1]..., đoạn code sẽ rất dài và dễ gõ nhầm dấu + thành -.
Thay vào đó, ta lưu độ lệch của hàng và cột vào hai mảng hằng số dx và dy:
- Khi xét 4 ô kề cạnh (Lên, Phải, Xuống, Trái):
DX4 = {-1, 0, 1, 0}vàDY4 = {0, 1, 0, -1}. - Khi xét 8 ô kề cạnh và kề góc:
DX8 = {-1, -1, -1, 0, 0, 1, 1, 1}vàDY8 = {-1, 0, 1, -1, 1, -1, 0, 1}.
Khi đó, chỉ cần một vòng lặp dir chạy qua các hướng, tọa độ ô lân cận mới luôn là:
nr = r + dx[dir] và nc = c + dy[dir]
Trước khi truy cập g[nr][nc], ta kiểm tra gọn điều kiện nằm trong biên lưới bằng hàm 0 <= nr < so_hang và 0 <= nc < so_cot:
#include <iostream>
#include <vector>
using namespace std;
// 4 hướng kề cạnh: Lên, Phải, Xuống, Trái
const int DX4[4] = {-1, 0, 1, 0};
const int DY4[4] = {0, 1, 0, -1};
// 8 hướng kề cạnh và kề góc
const int DX8[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
const int DY8[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
bool trong_luoi(int r, int c, int so_hang, int so_cot) {
return r >= 0 && r < so_hang && c >= 0 && c < so_cot;
}
// 1. Tính tổng các ô kề cạnh (4 hướng) của ô (r, c)
int tong_4_ke(const vector<vector<int>>& g, int r, int c) {
int so_hang = static_cast<int>(g.size());
int so_cot = static_cast<int>(g[0].size());
int tong = 0;
for (int dir = 0; dir < 4; ++dir) {
int nr = r + DX4[dir];
int nc = c + DY4[dir];
if (trong_luoi(nr, nc, so_hang, so_cot)) {
tong += g[nr][nc];
}
}
return tong;
}
// 2. Đếm số ô lớn hơn nghiêm ngặt tất cả các ô lân cận 8 hướng của nó
int dem_cuc_dai_8_huong(const vector<vector<int>>& g) {
int so_hang = static_cast<int>(g.size());
int so_cot = static_cast<int>(g[0].size());
int dem = 0;
for (int r = 0; r < so_hang; ++r) {
for (int c = 0; c < so_cot; ++c) {
bool cuc_dai = true;
for (int dir = 0; dir < 8; ++dir) {
int nr = r + DX8[dir];
int nc = c + DY8[dir];
if (trong_luoi(nr, nc, so_hang, so_cot) && g[nr][nc] >= g[r][c]) {
cuc_dai = false;
break;
}
}
if (cuc_dai) ++dem;
}
}
return dem;
}
int main() {
vector<vector<int>> g = {
{2, 5, 1, 4},
{3, 8, 6, 2},
{7, 4, 9, 5}
};
// Ô (1, 1) = 8 có 4 ô kề cạnh là 5, 6, 4, 3 -> tổng = 18
// Ô góc (0, 0) = 2 có 2 ô kề cạnh là 5, 3 -> tổng = 8
// Cực đại 8 hướng duy nhất là ô (2, 2) = 9 -> đếm = 1
cout << tong_4_ke(g, 1, 1) << " " << tong_4_ke(g, 0, 0) << " " << dem_cuc_dai_8_huong(g) << "\n";
return 0;
}
# 4 hướng kề cạnh: Lên, Phải, Xuống, Trái
DX4 = (-1, 0, 1, 0)
DY4 = (0, 1, 0, -1)
# 8 hướng kề cạnh và kề góc
DX8 = (-1, -1, -1, 0, 0, 1, 1, 1)
DY8 = (-1, 0, 1, -1, 1, -1, 0, 1)
def trong_luoi(r: int, c: int, so_hang: int, so_cot: int) -> bool:
return 0 <= r < so_hang and 0 <= c < so_cot
def tong_4_ke(g: list[list[int]], r: int, c: int) -> int:
"""Tính tổng các ô kề cạnh (4 hướng) hợp lệ của ô (r, c)."""
so_hang, so_cot = len(g), len(g[0])
tong = 0
for dx, dy in zip(DX4, DY4):
nr, nc = r + dx, c + dy
if trong_luoi(nr, nc, so_hang, so_cot):
tong += g[nr][nc]
return tong
def dem_cuc_dai_8_huong(g: list[list[int]]) -> int:
"""Đếm số ô lớn hơn nghiêm ngặt tất cả các ô lân cận 8 hướng của nó."""
so_hang, so_cot = len(g), len(g[0])
dem = 0
for r in range(so_hang):
for c in range(so_cot):
cuc_dai = True
for dx, dy in zip(DX8, DY8):
nr, nc = r + dx, c + dy
if trong_luoi(nr, nc, so_hang, so_cot) and g[nr][nc] >= g[r][c]:
cuc_dai = False
break
if cuc_dai:
dem += 1
return dem
if __name__ == "__main__":
g = [
[2, 5, 1, 4],
[3, 8, 6, 2],
[7, 4, 9, 5],
]
print(tong_4_ke(g, 1, 1), tong_4_ke(g, 0, 0), dem_cuc_dai_8_huong(g))
Hãy đối chiếu kết quả chạy trên lưới 3 × 4 trong code:
- Tại ô
(1, 1)(giá trị8), 4 ô kề cạnh đều nằm trong lưới là(0, 1) = 5,(1, 2) = 6,(2, 1) = 4,(1, 0) = 3→ tổng bằng5 + 6 + 4 + 3 =18. - Tại ô góc
(0, 0)(giá trị2), hướng Lên(-1, 0)và Trái(0, -1)vượt ra ngoài biên nên tự động bị loại, chỉ còn 2 ô hợp lệ là(0, 1) = 5và(1, 0) = 3→ tổng bằng8. - Khi tìm các ô cực đại 8 hướng (lớn hơn nghiêm ngặt mọi ô xung quanh nó), ô
(1, 1) = 8bị loại vì chạm góc với ô(2, 2) = 9; chỉ có duy nhất1ô thỏa mãn là(2, 2) = 9.
Độ phức tạp khi xử lý mảng 2 chiều
Với bảng lưới có R hàng và C cột, tổng số ô trên lưới là N = R × C (xem thêm cách ước lượng tại bài độ phức tạp thuật toán Big-O):
- Thời gian duyệt toàn bộ lưới: O(R × C). Dù ở mỗi ô bạn có kiểm tra thêm 4 hướng hay 8 hướng bằng mảng
dx, dy, số phép tính tại mỗi ô vẫn là hằng số (4hoặc8bước), nên tổng thời gian quét toàn bảng vẫn làO(R × C). - Bộ nhớ lưu trữ: O(R × C). Nếu đề bài yêu cầu tính tổng hình chữ nhật con hàng trăm nghìn lần trên lưới tĩnh, bạn có thể kết hợp thêm kỹ thuật mảng cộng dồn 2 chiều để trả lời mỗi truy vấn trong
O(1).
Lỗi hay gặp
- Nhầm hệ trục
(x, y)của toán học với chỉ số(hàng, cột)của ma trận: Trong mặt phẳng tọa độ Descartes,xlà trục ngang vàylà trục dọc hướng lên trên. Ngược lại, trong mảng 2 chiềua[r][c], chỉ số thứ nhấtrlà hàng (dọc từ trên xuống) và chỉ số thứ haiclà cột (ngang từ trái sang). Để không bao giờ nhầm, hãy đặt tên biến là(r, c)(row, col) thay vì(x, y). - Truy cập
a[nr][nc]trước khi kiểm tra biên: Trong điều kiệnif (trong_luoi(nr, nc, R, C) && a[nr][nc] == 1), phép kiểm tratrong_luoibắt buộc phải đứng bên trái toán tử&&(hoặcand) để tận dụng cơ chế ngắt mạch (short-circuit evaluation); nếu viết ngược lại sẽ gây lỗi tràn bộ nhớ (Out of Bounds / Segmentation Fault). - Dùng
[[0] * m] * ntrong Python: Như đã phân tích ở mục 1, luôn dùng[[0] * m for _ in range(n)].
Luyện ngay trên Big-O Coding
Thực hành ngay các thao tác duyệt hàng, cột, đường chéo và mảng hướng trên bảng lưới trên hệ thống Big-O Coding:
Câu hỏi thường gặp
Khi nào nên trải phẳng (flatten) mảng 2 chiều thành mảng 1 chiều?
Khi cần tối ưu tối đa tốc độ cấp phát bộ nhớ cho ma trận kích thước động, bạn có thể tạo một mảng 1 chiều kích thước R * C và quy đổi tọa độ (r, c) thành chỉ số 1 chiều theo công thức idx = r * C + c (ngược lại từ idx suy ra r = idx / C và c = idx % C).
Làm sao xoay một ma trận vuông n × n một góc 90 độ theo chiều kim đồng hồ tại chỗ?
Cách gọn nhất gồm 2 bước: bước 1 chuyển vị ma trận (hoán đổi a[i][j] với a[j][i] cho mọi cặp i < j); bước 2 đảo ngược từng hàng từ trái sang phải. Cả quá trình chạy trong O(n²) thời gian và chỉ tốn O(1) bộ nhớ phụ.
Mảng hướng dx, dy có dùng cho quân Mã trên bàn cờ vua được không?
Hoàn toàn được! Quân Mã đi theo hình chữ L (1 ô theo một chiều và 2 ô theo chiều kia), nên bạn chỉ cần khai báo mảng 8 bước nhảy của quân Mã: DX_MA = {-2, -2, -1, -1, 1, 1, 2, 2} và DY_MA = {-1, 1, -2, 2, -2, 2, -1, 1}.
Khóa Green cho người mới, sinh viên năm 1–2: xây nền tảng lập trình và thuật toán vững chắc, biết dùng AI hỗ trợ. 16 buổi online, học thử buổi 1 miễn phí.


