Bài 25.3: Các bài toán cơ bản trên mảng 2 chiều trong C++

Chào mừng các bạn đã quay trở lại với series học lập trình C++ cùng FullhouseDev!

Nếu như mảng một chiều (1D array) giúp chúng ta tổ chức dữ liệu theo một danh sách tuyến tính, thì mảng hai chiều (2D array), hay còn gọi là ma trận (matrix), nâng khả năng lưu trữ dữ liệu của chúng ta lên một cấp độ mới: theo dạng lưới hoặc bảng biểu. Tưởng tượng một bảng tính Excel, một bàn cờ vua, hay một bức ảnh kỹ thuật số - tất cả đều có thể được biểu diễn một cách tự nhiên bằng mảng 2 chiều.

Mảng 2 chiều trong C++ về cơ bản là một "mảng của các mảng". Nó có hai chỉ số: một cho hàng (row) và một cho cột (column). Thao tác với mảng 2 chiều đòi hỏi chúng ta phải làm quen với việc quản lý cả hai chiều này cùng lúc. Trong bài viết này, chúng ta sẽ đi sâu vào các bài toán cơ bảnthao tác thiết yếu khi làm việc với mảng 2 chiều trong C++.

Hãy cùng bắt đầu!

1. Khai báo và Khởi tạo mảng 2 chiều

Trước khi thực hiện bất kỳ bài toán nào, chúng ta cần biết cách "tạo ra" một mảng 2 chiều.

Cú pháp khai báo mảng 2 chiều tĩnh (kích thước cố định khi viết code) khá giống mảng 1 chiều, nhưng có thêm một cặp dấu ngoặc vuông cho chỉ số cột:

kiểu_dữ_liệu tên_mảng[số_hàng][số_cột];

Ví dụ:

#include <iostream>

int main() {
    // Khai báo một ma trận số nguyên 3 hàng, 4 cột
    int matrix[3][4];

    // Khai báo và khởi tạo ngay lập tức
    int data[2][3] = {
        {1, 2, 3},  // Hàng 0
        {4, 5, 6}   // Hàng 1
    };

    // Mảng với tất cả các phần tử bằng 0 (chỉ cần khởi tạo phần tử đầu tiên)
    int zeros[3][3] = {0};

    cout << "Khai bao thanh cong!\n";

    return 0;
}

Giải thích:

  • int matrix[3][4]; khai báo một mảng tên matrix có 3 hàng và 4 cột, lưu trữ các số nguyên. Kích thước là 3x4 = 12 phần tử.
  • int data[2][3] = {{1, 2, 3}, {4, 5, 6}}; vừa khai báo vừa gán giá trị ban đầu. Các cặp {} bên trong đại diện cho từng hàng.
  • int zeros[3][3] = {0}; là một cách viết tắt phổ biến để khởi tạo tất cả các phần tử trong mảng bằng 0.

Lưu ý: Với mảng tĩnh, kích thước (số hàng và số cột) phải là một hằng số hoặc biết được tại thời điểm biên dịch. Nếu bạn cần mảng có kích thước thay đổi lúc chạy chương trình, hãy tìm hiểu về vector<vector<kiểu_dữ_liệu>>.

2. Truy cập các phần tử

Để truy cập một phần tử cụ thể trong mảng 2 chiều, chúng ta sử dụng cả hai chỉ số: chỉ số hàng và chỉ số cột, theo cú pháp tên_mảng[chỉ_số_hàng][chỉ_số_cột]. Cần nhớ rằng, giống như mảng 1 chiều, chỉ số trong C++ bắt đầu từ 0.

  • Phần tử ở góc trên cùng bên trái là tên_mảng[0][0].
  • Phần tử ở cuối hàng đầu tiên là tên_mảng[0][số_cột - 1].
  • Phần tử ở đầu hàng cuối cùng là tên_mảng[số_hàng - 1][0].
  • Phần tử ở góc dưới cùng bên phải là tên_mảng[số_hàng - 1][số_cột - 1].

Ví dụ:

#include <iostream>

int main() {
    int matrix[2][3] = {
        {10, 20, 30},
        {40, 50, 60}
    };

    // Truy cập và in ra một số phần tử
    cout << "Phan tu o [0][0]: " << matrix[0][0] << endl; // Output: 10
    cout << "Phan tu o [0][2]: " << matrix[0][2] << endl; // Output: 30
    cout << "Phan tu o [1][1]: " << matrix[1][1] << endl; // Output: 50
    cout << "Phan tu o [1][2]: " << matrix[1][2] << endl; // Output: 60

    // Thay đổi giá trị của một phần tử
    matrix[0][0] = 100;
    cout << "Phan tu o [0][0] sau khi thay doi: " << matrix[0][0] << endl; // Output: 100

    return 0;
}

Giải thích: Chúng ta dùng matrix[i][j] để lấy hoặc gán giá trị cho phần tử tại hàng i và cột j. Chỉ số i chạy từ 0 đến số_hàng - 1, và chỉ số j chạy từ 0 đến số_cột - 1.

3. Duyệt (Traversal) mảng 2 chiều

Đây là thao tác cốt lõi cho hầu hết các bài toán trên mảng 2 chiều. Để xử lý mọi phần tử trong mảng 2 chiều, chúng ta cần sử dụng hai vòng lặp lồng nhau. Vòng lặp ngoài thường dùng để duyệt qua các hàng, và vòng lặp trong dùng để duyệt qua các cột trong hàng hiện tại.

Cấu trúc chung như sau:

for (chỉ_số_hàng = 0; chỉ_số_hàng < số_hàng; ++chỉ_số_hàng) {
    for (chỉ_số_cột = 0; chỉ_số_cột < số_cột; ++chỉ_số_cột) {
        // Xử lý phần tử tại matrix[chỉ_số_hàng][chỉ_số_cột]
    }
}

Ví dụ: Duyệt và in tất cả các phần tử:

#include <iostream>

int main() {
    const int ROWS = 3;
    const int COLS = 4;
    int matrix[ROWS][COLS] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    cout << "Ma tran gom cac phan tu:\n";

    // Duyệt qua từng hàng
    for (int i = 0; i < ROWS; ++i) {
        // Duyệt qua từng cột trong hàng hien tai
        for (int j = 0; j < COLS; ++j) {
            cout << matrix[i][j] << "\t"; // In phần tử và dùng tab để cách
        }
        cout << endl; // Kết thúc một hàng, xuống dòng
    }

    return 0;
}

Giải thích:

  • Vòng lặp ngoài for (int i = 0; i < ROWS; ++i) đi qua các hàng từ 0 đến ROWS - 1. Biến i đóng vai trò là chỉ số hàng hiện tại.
  • Vòng lặp trong for (int j = 0; j < COLS; ++j) đi qua các cột từ 0 đến COLS - 1 cho mỗi hàng i. Biến j đóng vai trò là chỉ số cột hiện tại.
  • matrix[i][j] truy cập phần tử tại hàng i, cột j.
  • cout << matrix[i][j] << "\t"; in giá trị của phần tử, theo sau là một ký tự tab (\t) để định dạng giống bảng.
  • cout << endl; được đặt sau vòng lặp trong, đảm bảo sau khi in hết các phần tử của một hàng, con trỏ xuống dòng để in hàng tiếp theo.

Cấu trúc lồng nhau này là nền tảng để giải quyết hầu hết các bài toán cơ bản về mảng 2 chiều.

4. Các Bài toán Cơ bản

Dựa trên kỹ năng khai báo, truy cập và duyệt mảng 2 chiều, chúng ta có thể giải quyết nhiều bài toán thông dụng.

Bài toán 1: Nhập dữ liệu cho mảng 2 chiều từ bàn phím

Tương tự như in, việc nhập dữ liệu cũng sử dụng hai vòng lặp lồng nhau, nhưng thay vì cout, chúng ta dùng cin.

#include <iostream>

int main() {
    const int ROWS = 2;
    const int COLS = 3;
    int matrix[ROWS][COLS];

    cout << "Nhap cac phan tu cho ma tran (" << ROWS << "x" << COLS << "):\n";

    // Duyệt qua từng hàng để nhập dữ liệu
    for (int i = 0; i < ROWS; ++i) {
        // Duyệt qua từng cột trong hàng hien tai
        for (int j = 0; j < COLS; ++j) {
            cout << "Nhap phan tu tai [" << i << "][" << j << "]: ";
            cin >> matrix[i][j];
        }
    }

    cout << "\nMa tran vua nhap la:\n";
    // In ma trận để kiểm tra (sử dụng lại code in ở mục 3)
    for (int i = 0; i < ROWS; ++i) {
        for (int j = 0; j < COLS; ++j) {
            cout << matrix[i][j] << "\t";
        }
        cout << endl;
    }

    return 0;
}

Giải thích: Cấu trúc lặp giống như khi in. Bên trong vòng lặp trong, chúng ta dùng cin >> matrix[i][j]; để đọc giá trị từ bàn phím và lưu vào phần tử tại vị trí [i][j]. Dòng cout trước cin giúp người dùng biết đang cần nhập phần tử nào.

Bài toán 2: Tính tổng và trung bình cộng của tất cả các phần tử

Để tính tổng, chúng ta cần một biến để lưu trữ tổng, khởi tạo nó bằng 0 trước khi duyệt. Sau đó, trong vòng lặp lồng nhau, cộng giá trị của mỗi phần tử vào biến tổng đó. Trung bình cộng là tổng chia cho tổng số phần tử (số hàng * số cột).

#include <iostream>

int main() {
    const int ROWS = 3;
    const int COLS = 4;
    int matrix[ROWS][COLS] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    long long sum = 0; // Dùng long long để tránh tràn số nếu mảng lớn
    int count = 0;   // Hoặc count = ROWS * COLS;

    // Duyệt và tính tổng
    for (int i = 0; i < ROWS; ++i) {
        for (int j = 0; j < COLS; ++j) {
            sum += matrix[i][j];
            count++; // Tăng bộ đếm phần tử (nếu không tính bằng ROWS * COLS)
        }
    }

    // Tính trung bình cộng
    double average = 0.0;
    if (count > 0) {
        average = static_cast<double>(sum) / count; // Ép kiểu để kết quả là số thực
    }

    cout << "Tong cac phan tu: " << sum << endl;
    cout << "Trung binh cong cac phan tu: " << average << endl;

    return 0;
}

Giải thích:

  • Biến sum được khởi tạo bằng 0.
  • Biến count để đếm số phần tử, cũng có thể tính đơn giản bằng ROWS * COLS.
  • Bên trong vòng lặp lồng nhau, sum += matrix[i][j]; cộng giá trị của phần tử hiện tại vào sum.
  • Sau khi duyệt xong, average được tính bằng sum chia cho count. Chúng ta dùng static_cast<double>(sum) để đảm bảo phép chia là chia số thực, tránh mất phần thập phân.
Bài toán 3: Tìm phần tử lớn nhất và nhỏ nhất

Để tìm giá trị lớn nhất (max) và nhỏ nhất (min), chúng ta cần hai biến để lưu trữ maxValminVal tìm thấy cho đến thời điểm hiện tại. Cách tốt nhất là khởi tạo chúng bằng giá trị của phần tử đầu tiên của mảng (ví dụ: matrix[0][0]). Sau đó, duyệt qua tất cả các phần tử còn lại, so sánh từng phần tử với maxValminVal, và cập nhật nếu tìm thấy giá trị mới lớn hơn hoặc nhỏ hơn.

#include <iostream>
#include <limits> // Cần cho numeric_limits nếu không muốn init bằng matrix[0][0]

int main() {
    const int ROWS = 3;
    const int COLS = 4;
    int matrix[ROWS][COLS] = {
        {1, 20, 3, 4},
        {5, -6, 7, 8},
        {9, 10, 11, 12}
    };

    // Khởi tạo maxVal và minVal
    int maxVal = matrix[0][0];
    int minVal = matrix[0][0];
    // Cách khác (an toàn hơn với mảng rỗng, nhưng ở đây giả định mảng không rỗng):
    // int maxVal = numeric_limits<int>::min();
    // int minVal = numeric_limits<int>::max();


    // Duyệt và tìm max/min
    for (int i = 0; i < ROWS; ++i) {
        for (int j = 0; j < COLS; ++j) {
            // So sánh với maxVal
            if (matrix[i][j] > maxVal) {
                maxVal = matrix[i][j]; // Cập nhật maxVal nếu tìm thấy phần tử lớn hơn
            }
            // So sánh với minVal
            if (matrix[i][j] < minVal) {
                minVal = matrix[i][j]; // Cập nhật minVal nếu tìm thấy phần tử nhỏ hơn
            }
        }
    }

    cout << "Phan tu lon nhat: " << maxVal << endl;
    cout << "Phan tu nho nhat: " << minVal << endl;

    return 0;
}

Giải thích:

  • maxValminVal được khởi tạo bằng matrix[0][0]. Điều này giả định mảng có ít nhất một phần tử.
  • Trong vòng lặp lồng nhau, mỗi phần tử matrix[i][j] được so sánh với maxValminVal hiện tại.
  • Nếu matrix[i][j] lớn hơn maxVal, maxVal được cập nhật thành matrix[i][j].
  • Nếu matrix[i][j] nhỏ hơn minVal, minVal được cập nhật thành matrix[i][j].
  • Sau khi duyệt hết mảng, maxValminVal sẽ giữ giá trị lớn nhất và nhỏ nhất trong toàn bộ mảng.
Bài toán 4: Tính tổng các phần tử trên một hàng hoặc một cột cụ thể

Thay vì duyệt toàn bộ mảng, chúng ta chỉ cần giữ cố định một chỉ số (hàng hoặc cột) và duyệt qua chỉ số còn lại.

a) Tổng các phần tử trên hàng k: Giữ chỉ số hàng i = k cố định và duyệt chỉ số cột j từ 0 đến COLS - 1.

#include <iostream>

int main() {
    const int ROWS = 3;
    const int COLS = 4;
    int matrix[ROWS][COLS] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    int targetRow = 1; // Muốn tính tổng hàng 1 (hàng thứ 2)
    long long rowSum = 0;

    if (targetRow >= 0 && targetRow < ROWS) { // Kiểm tra chỉ số hợp lệ
        // Duyệt qua các cột trên hàng targetRow
        for (int j = 0; j < COLS; ++j) {
            rowSum += matrix[targetRow][j];
        }
        cout << "Tong cac phan tu tren hang " << targetRow << ": " << rowSum << endl;
    } else {
        cout << "Chi so hang " << targetRow << " khong hop le!\n";
    }


    return 0;
}

Giải thích: Vòng lặp ngoài không cần thiết. Chúng ta chỉ cần một vòng lặp duy nhất đi qua các cột (j) và truy cập các phần tử tại matrix[targetRow][j].

b) Tổng các phần tử trên cột k: Giữ chỉ số cột j = k cố định và duyệt chỉ số hàng i từ 0 đến ROWS - 1.

#include <iostream>

int main() {
    const int ROWS = 3;
    const int COLS = 4;
    int matrix[ROWS][COLS] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    int targetCol = 2; // Muốn tính tổng cột 2 (cột thứ 3)
    long long colSum = 0;

    if (targetCol >= 0 && targetCol < COLS) { // Kiểm tra chỉ số hợp lệ
        // Duyệt qua cac hang tren cot targetCol
        for (int i = 0; i < ROWS; ++i) {
            colSum += matrix[i][targetCol];
        }
        cout << "Tong cac phan tu tren cot " << targetCol << ": " << colSum << endl;
    } else {
        cout << "Chi so cot " << targetCol << " khong hop le!\n";
    }

    return 0;
}

Giải thích: Tương tự như tổng hàng, chúng ta dùng một vòng lặp duy nhất đi qua các hàng (i) và truy cập các phần tử tại matrix[i][targetCol]. Việc kiểm tra chỉ số (targetRow, targetCol) là quan trọng để tránh lỗi truy cập ngoài phạm vi mảng.

Bài toán 5: Chuyển vị (Transpose) ma trận

Chuyển vị một ma trận là tạo ra một ma trận mới mà các hàng của ma trận gốc trở thành các cột của ma trận mới, và ngược lại. Nếu ma trận gốc có kích thước M x N, ma trận chuyển vị sẽ có kích thước N x M. Phần tử tại matrix[i][j] trong ma trận gốc sẽ ở vị trí transposed_matrix[j][i] trong ma trận chuyển vị.

#include <iostream>

int main() {
    const int ROWS = 2;
    const int COLS = 3;
    int originalMatrix[ROWS][COLS] = {
        {1, 2, 3},
        {4, 5, 6}
    };

    // Ma trận chuyển vị sẽ có kích thước COLS x ROWS
    int transposedMatrix[COLS][ROWS];

    // Thực hiện chuyển vị
    for (int i = 0; i < ROWS; ++i) {
        for (int j = 0; j < COLS; ++j) {
            transposedMatrix[j][i] = originalMatrix[i][j]; // Đảo ngược chỉ số
        }
    }

    cout << "Ma tran goc:\n";
    for (int i = 0; i < ROWS; ++i) {
        for (int j = 0; j < COLS; ++j) {
            cout << originalMatrix[i][j] << "\t";
        }
        cout << endl;
    }

    cout << "\nMa tran chuyen vi:\n";
    // In ma trận chuyển vị (lưu ý kích thước COLS x ROWS)
    for (int i = 0; i < COLS; ++i) { // Vòng lặp ngoài duyệt theo số cột gốc (giờ là số hàng mới)
        for (int j = 0; j < ROWS; ++j) { // Vòng lặp trong duyệt theo số hàng gốc (giờ là số cột mới)
            cout << transposedMatrix[i][j] << "\t";
        }
        cout << endl;
    }

    return 0;
}

Giải thích:

  • Chúng ta khai báo một ma trận mới transposedMatrix với số hàng và số cột đảo ngược so với originalMatrix.
  • Sử dụng hai vòng lặp lồng nhau để duyệt qua ma trận gốc.
  • Trong mỗi lần lặp, gán giá trị của phần tử originalMatrix[i][j] vào vị trí đảo ngược chỉ số transposedMatrix[j][i].
  • Khi in ma trận chuyển vị, chúng ta cần sử dụng kích thước mới (COLSROWS bị đổi vai trò trong vòng lặp in).

Bài tập ví dụ: C++ Bài 11.A3: Ma trận quy luật

Viết chương tạo và thông báo mảng hai chiều kích thước \(n\) dòng \(n\) cột theo quy luật sau.

INPUT FORMAT

Dòng đầu là hai số nguyên dương \(n\) \((1 \leq n \leq 1000)\)

OUTPUT FORMAT

In ra ma trận tương ứng.

Ví dụ:

Input
4
Output
2 3 4 5
3 4 5 6
4 5 6 7
5 6 7 8
Giải thích ví dụ mẫu
Tạo ma trận với mỗi giá trị là tổng của chỉ số hàng và chỉ số cột, bắt đầu từ 2.

<br>

  1. Phân tích quy luật: Quan sát ví dụ mẫu, ta thấy giá trị ở vị trí hàng i, cột j (giả sử đánh số từ 0) có vẻ tuân theo một công thức đơn giản.

    • Ở vị trí (0, 0), giá trị là 2.
    • Ở vị trí (0, 1), giá trị là 3.
    • Ở vị trí (1, 0), giá trị là 3.
    • Ở vị trí (1, 1), giá trị là 4.
    • ...
    • Ở vị trí (3, 3) với n=4, giá trị là 8.

    Công thức đơn giản nhất liên quan đến ij cho ra kết quả này chính là i + j + 2.

    • (0, 0) -> 0 + 0 + 2 = 2
    • (0, 1) -> 0 + 1 + 2 = 3
    • (1, 0) -> 1 + 0 + 2 = 3
    • (1, 1) -> 1 + 1 + 2 = 4
    • (3, 3) -> 3 + 3 + 2 = 8

    Vậy, giá trị tại ô ở hàng i và cột j (với i, j bắt đầu từ 0) là i + j + 2.

  2. Cấu trúc chương trình:

    • Bạn cần đọc vào số nguyên dương n từ đầu vào chuẩn (sử dụng cin).
    • Bạn cần tạo ra một ma trận kích thước n x n. Tuy nhiên, với quy luật này, bạn không nhất thiết phải lưu trữ toàn bộ ma trận vào bộ nhớ. Bạn có thể tính toán và in ra giá trị của từng ô ngay khi xử lý.
  3. Thuật toán:

    • Sử dụng hai vòng lặp lồng nhau để duyệt qua từng phần tử của ma trận.
    • Vòng lặp ngoài sẽ duyệt qua các hàng, từ hàng 0 đến hàng n-1. Gọi biến đếm là i.
    • Vòng lặp trong sẽ duyệt qua các cột cho mỗi hàng, từ cột 0 đến cột n-1. Gọi biến đếm là j.
    • Bên trong vòng lặp trong (ứng với mỗi cặp (i, j)), tính giá trị của ô đó theo công thức i + j + 2.
    • In giá trị vừa tính ra màn hình chuẩn (sử dụng cout).
    • Sau khi in giá trị của một ô, bạn cần in thêm một khoảng trắng để tách các số trên cùng một dòng, trừ số cuối cùng của dòng đó.
    • Sau khi vòng lặp trong kết thúc (đã duyệt xong tất cả các cột của một hàng), bạn cần in ký tự xuống dòng (sử dụng endl hoặc '\n') để chuyển sang in hàng tiếp theo.
  4. Lưu ý về in ấn:

    • Đảm bảo in đúng số khoảng trắng giữa các số và in xuống dòng sau mỗi hàng. Cách phổ biến là in số, sau đó kiểm tra xem nó có phải là số cuối cùng của hàng không. Nếu không, in một khoảng trắng.
    • Sử dụng coutcin từ thư viện <iostream>.
    • Sử dụng endl hoặc '\n' cho việc xuống dòng. '\n' thường hiệu quả hơn endlendl còn ép bộ đệm xuất, có thể làm chậm chương trình với lượng dữ liệu lớn, nhưng với bài này thì cả hai đều được chấp nhận.

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.