Nhập môn

Mảng 2 chiều và ma trận: cách duyệt hàng, cột, đường chéo và mảng hướng dx, dy (C++, Python)

Mảng 2 chiều (two-dimensional array), thường gọi là ma trận hay bảng lưới, là cấu trúc gồm R hàng và C cột, trong đó mỗi ô được xác định bởi cặp chỉ số a[i][j] (hàng i, cột j). Nắm vững cách khởi tạo ma trận an toàn, công thức chỉ số của hai đường chéo và kỹ thuật mảng hướng dx, dy là nền tảng bắt buộc trước khi bạn giải các bài toán loang trên lưới, tìm đường đi trong mê cung hay quy hoạch động 2 chiều.

Ý chính
  • Tọa độ trên lưới R × C: Phần tử a[i][j] nằm ở hàng i (0 ≤ i < R, tính từ trên xuống) và cột j (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] * n vì cả n hà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ãn i == j (hoặc hiệu i - j không đổi); đường chéo phụ thỏa mãn i + j == n - 1 (hoặc tổng i + j khô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ệnh if rờ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àng n, và, khi n > 0, a[0].size() là số cột m. 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 ra n hàng mới, mà chỉ sao chép n tham 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ành 9!
  • 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àng i ở 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;
}

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 = 15 và 3+5+7 = 15).
Học cùng giảng viên

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ó:

(-1, -1) Chéo trên (-1, 0) Lên (-1, +1) Chéo trên (0, -1) Trái a[r][c] Tâm (0, 0) (0, +1) Phải (+1, -1) Chéo dưới (+1, 0) Xuống (+1, +1) Chéo dưới Ô trung tâm: a[r][c] Hàng r (từ trên xuống), cột c (trái sang) Màu xanh: 4 ô kề cạnh DX4 = {-1, 0, 1, 0}, DY4 = {0, 1, 0, -1} Màu cam: 4 ô kề góc Gộp xanh + cam = 8 hướng lân cận Tọa độ ô lân cận thứ dir: nr = r + dx[dir], nc = c + dy[dir]
(-1, -1) Kề góc (-1, 0) Lên (-1, +1) Kề góc (0, -1) Trái a[r][c] Tâm (0, +1) Phải (+1, -1) Kề góc (+1, 0) Xuống (+1, +1) Kề góc Xanh: 4 hướng kề cạnh | Cam: 4 hướng kề góc Ô lân cận: nr = r + dx[dir], nc = c + dy[dir]
Hình 1. Bảng tọa độ độ lệch (dx, dy) quanh ô trung tâm a[r][c]: 4 ô màu xanh là lân cận kề cạnh, cộng thêm 4 ô màu cam ở góc tạo thành 8 hướng lân cậ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;
}

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ằng 5 + 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) = 5 và (1, 0) = 3 → tổng bằng 8.
  • 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) = 8 bị loại vì chạm góc với ô (2, 2) = 9; chỉ có duy nhất 1 ô 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ố (4 hoặc 8 bướ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

Cẩn thận
  • 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, x là trục ngang và y là trục dọc hướng lên trên. Ngược lại, trong mảng 2 chiều a[r][c], chỉ số thứ nhất r là hàng (dọc từ trên xuống) và chỉ số thứ hai c là 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ện if (trong_luoi(nr, nc, R, C) && a[nr][nc] == 1), phép kiểm tra trong_luoi bắt buộc phải đứng bên trái toán tử && (hoặc and) để 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] * n trong 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}.

Muốn học bài bản cùng giảng viên?

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í.

Xem Khóa Green

Nguồn tham khảo

Chia sẻ: Facebook LinkedIn
Nhắn Zalo tư vấnZalo Gọi 0937 401 483