Bài 26.1: Các phép toán cơ bản trên ma trận trong C++

Ma trận không chỉ là khái niệm trừu tượng trong toán học, chúng còn là một công cụ cực kỳ mạnh mẽquan trọng trong rất nhiều lĩnh vực của khoa học máy tính hiện đại. Từ xử lý ảnh, đồ họa máy tính 3D, giải hệ phương trình tuyến tính, cho đến "trái tim" của Machine Learning và AI - tất cả đều sử dụng ma trận một cách rộng rãi.

Để làm việc hiệu quả với ma trận trong lập trình, đặc biệt là với một ngôn ngữ mạnh mẽ như C++, chúng ta cần nắm vững cách biểu diễn ma trận và thực hiện các phép toán cơ bản trên chúng. Trong bài viết này, chúng ta sẽ cùng nhau đi sâu vào thế giới của ma trận trong C++, tìm hiểu cách lưu trữ và triển khai các phép toán cốt lõi như cộng, trừ, nhân (với vô hướng và ma trận) và chuyển vị.

Biểu diễn Ma trận trong C++

Trước khi thực hiện các phép toán, câu hỏi đặt ra là: Làm sao để "lưu trữ" một ma trận trong C++?

Cách phổ biến và linh hoạt nhất hiện nay là sử dụng vector<vector<T>>, trong đó T là kiểu dữ liệu của các phần tử trong ma trận (ví dụ: int, double, float).

Tại sao lại là vector<vector<T>>?

  • vector cho phép kích thước động, rất tiện lợi khi bạn không biết trước kích thước ma trận hoặc cần làm việc với ma trận có kích thước thay đổi.
  • Nó dễ dàng mô phỏng cấu trúc "hàng và cột" của ma trận: vector ngoài cùng biểu diễn các hàng, và mỗi vector bên trong biểu diễn các phần tử của hàng đó.

Ví dụ: Một ma trận 2x3 (2 hàng, 3 cột) chứa số nguyên có thể được biểu diễn như sau:

#include <vector>
#include <iostream> // Cần cho nhập xuất

// Sử dụng namespace std cho ngắn gọn trong ví dụ blog
using namespace std;

int main() {
    // Khai báo một ma trận 2x3 chứa số nguyên
    vector<vector<int>> matrix = {
        {1, 2, 3},
        {4, 5, 6}
    };

    // Truy cập phần tử ở hàng 0, cột 1 (giá trị 2)
    cout << "Phan tu o [0][1]: " << matrix[0][1] << endl;

    // Kích thước ma trận
    cout << "So hang: " << matrix.size() << endl;         // Số hàng
    cout << "So cot: " << matrix[0].size() << endl;    // Số cột (giả định ma trận không rỗng và các hàng có cùng kích thước)

    return 0;
}
  • Giải thích: Chúng ta khai báo một vector của vector<int>. Mỗi vector<int> bên trong là một hàng. matrix.size() trả về số lượng hàng, và matrix[0].size() trả về số lượng cột của hàng đầu tiên (thường giả định là tất cả các hàng có cùng số cột).

Hiển thị Ma trận

Để kiểm tra kết quả của các phép toán, việc hiển thị ma trận là rất cần thiết. Chúng ta sẽ xây dựng một hàm nhỏ để làm điều này.

#include <vector>
#include <iostream>
#include <iomanip> // Cần cho setw

using namespace std;

// Hàm hiển thị ma trận
void displayMatrix(const vector<vector<int>>& matrix) {
    if (matrix.empty()) {
        cout << "Ma tran rong" << endl;
        return;
    }

    for (const auto& row : matrix) { // Duyệt qua từng hàng
        for (int element : row) {    // Duyệt qua từng phần tử trong hàng
            cout << setw(4) << element << " "; // setw(4) giúp căn chỉnh cho đẹp mắt
        }
        cout << endl; // Kết thúc một hàng, xuống dòng
    }
}

int main() {
    vector<vector<int>> myMatrix = {
        {10, 20, 30},
        {40, 50, 60},
        {70, 80, 90}
    };

    cout << "Ma tran cua toi:" << endl;
    displayMatrix(myMatrix);

    return 0;
}
  • Giải thích: Hàm displayMatrix nhận một ma trận (kiểu const& để tránh sao chép và đảm bảo không thay đổi ma trận gốc). Nó kiểm tra ma trận có rỗng không. Sau đó, nó dùng vòng lặp for range-based để duyệt qua từng hàng, rồi duyệt qua từng phần tử trong hàng đó và in ra. setw(4) từ <iomanip> giúp căn chỉnh các số thẳng hàng, làm cho đầu ra dễ đọc hơn.

Các Phép Toán Cơ bản trên Ma trận

Giờ là lúc chúng ta đi vào phần chính: các phép toán!

1. Phép Cộng Ma trận

Để cộng hai ma trận $A$ và $B$, chúng phải có cùng kích thước (số hàng và số cột bằng nhau). Ma trận kết quả $C$ sẽ có cùng kích thước đó, và mỗi phần tử $C_{ij}$ sẽ là tổng của các phần tử tương ứng $A_{ij} + B_{ij}$.

#include <vector>
#include <iostream>
#include <stdexcept> // Cần cho exception

using namespace std;

// Hàm cộng hai ma trận
vector<vector<int>> addMatrices(const vector<vector<int>>& A, const vector<vector<int>>& B) {
    // Kiểm tra kích thước
    if (A.empty() || B.empty() || A.size() != B.size() || A[0].size() != B[0].size()) {
        throw runtime_error("Loi: Hai ma tran phai co cung kich thuoc de cong.");
    }

    int rows = A.size();
    int cols = A[0].size();
    vector<vector<int>> result(rows, vector<int>(cols)); // Khởi tạo ma trận kết quả

    // Thực hiện phép cộng
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            result[i][j] = A[i][j] + B[i][j];
        }
    }

    return result; // Trả về ma trận kết quả
}

// Hàm hiển thị (sử dụng lại từ phần trên)
void displayMatrix(const vector<vector<int>>& matrix) {
    if (matrix.empty()) {
        cout << "Ma tran rong" << endl;
        return;
    }
    for (const auto& row : matrix) {
        for (int element : row) {
            cout << element << " ";
        }
        cout << endl;
    }
}

int main() {
    vector<vector<int>> matrixA = {
        {1, 2},
        {3, 4}
    };

    vector<vector<int>> matrixB = {
        {5, 6},
        {7, 8}
    };

    try {
        vector<vector<int>> matrixSum = addMatrices(matrixA, matrixB);
        cout << "Tong cua hai ma tran:" << endl;
        displayMatrix(matrixSum);
    } catch (const runtime_error& e) {
        cerr << e.what() << endl; // In ra thông báo lỗi nếu có
    }

    return 0;
}
  • Giải thích: Hàm addMatrices trước hết kiểm tra điều kiện kích thước. Nếu không thỏa mãn, nó ném ra một runtime_error để báo lỗi. Nếu hợp lệ, nó tạo một ma trận result có cùng kích thước. Sau đó, dùng hai vòng lặp lồng nhau để duyệt qua từng phần tử và thực hiện phép cộng. Kết quả được trả về là một ma trận mới. Phần main minh họa cách sử dụng và bắt lỗi.
2. Phép Trừ Ma trận

Tương tự như phép cộng, để trừ hai ma trận $A$ và $B$, chúng cũng phải có cùng kích thước. Ma trận kết quả $C$ sẽ có cùng kích thước đó, và mỗi phần tử $C_{ij}$ sẽ là hiệu của các phần tử tương ứng $A_{ij} - B_{ij}$.

#include <vector>
#include <iostream>
#include <stdexcept>

using namespace std;

// Hàm trừ hai ma trận
vector<vector<int>> subtractMatrices(const vector<vector<int>>& A, const vector<vector<int>>& B) {
     // Kiểm tra kích thước (giống phép cộng)
    if (A.empty() || B.empty() || A.size() != B.size() || A[0].size() != B[0].size()) {
        throw runtime_error("Loi: Hai ma tran phai co cung kich thuoc de tru.");
    }

    int rows = A.size();
    int cols = A[0].size();
    vector<vector<int>> result(rows, vector<int>(cols)); // Khởi tạo ma trận kết quả

    // Thực hiện phép trừ
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            result[i][j] = A[i][j] - B[i][j];
        }
    }

    return result; // Trả về ma trận kết quả
}

// Hàm hiển thị (sử dụng lại)
void displayMatrix(const vector<vector<int>>& matrix) {
    if (matrix.empty()) {
        cout << "Ma tran rong" << endl;
        return;
    }
    for (const auto& row : matrix) {
        for (int element : row) {
            cout << element << " ";
        }
        cout << endl;
    }
}

int main() {
    vector<vector<int>> matrixA = {
        {10, 20},
        {30, 40}
    };

    vector<vector<int>> matrixB = {
        {5, 6},
        {7, 8}
    };

    try {
        vector<vector<int>> matrixDiff = subtractMatrices(matrixA, matrixB);
        cout << "Hieu cua hai ma tran:" << endl;
        displayMatrix(matrixDiff);
    } catch (const runtime_error& e) {
        cerr << e.what() << endl;
    }

    return 0;
}
  • Giải thích: Hàm subtractMatrices có cấu trúc rất giống với addMatrices, chỉ khác ở phép toán bên trong vòng lặp là phép trừ (-). Điều kiện kiểm tra kích thước vẫn giữ nguyên.
3. Phép Nhân Ma trận với Vô hướng (Scalar Multiplication)

Nhân một ma trận $A$ với một số vô hướng $k$ (một số đơn lẻ) không yêu cầu điều kiện về kích thước của ma trận. Ma trận kết quả $C$ sẽ có cùng kích thước với ma trận $A$, và mỗi phần tử $C_{ij}$ sẽ là tích của $k$ với phần tử tương ứng $A_{ij}$.

#include <vector>
#include <iostream>

using namespace std;

// Hàm nhân ma trận với một số vô hướng
vector<vector<int>> scalarMultiplyMatrix(const vector<vector<int>>& matrix, int scalar) {
    if (matrix.empty()) {
        return {}; // Trả về ma trận rỗng nếu ma trận đầu vào rỗng
    }

    int rows = matrix.size();
    int cols = matrix[0].size();
    vector<vector<int>> result(rows, vector<int>(cols)); // Khởi tạo ma trận kết quả

    // Thực hiện phép nhân với vô hướng
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            result[i][j] = matrix[i][j] * scalar;
        }
    }

    return result; // Trả về ma trận kết quả
}

// Hàm hiển thị (sử dụng lại)
void displayMatrix(const vector<vector<int>>& matrix) {
    if (matrix.empty()) {
        cout << "Ma tran rong" << endl;
        return;
    }
    for (const auto& row : matrix) {
        for (int element : row) {
            cout << element << " ";
        }
        cout << endl;
    }
}

int main() {
    vector<vector<int>> myMatrix = {
        {1, 2, 3},
        {4, 5, 6}
    };
    int myScalar = 5;

    vector<vector<int>> resultMatrix = scalarMultiplyMatrix(myMatrix, myScalar);

    cout << "Ma tran sau khi nhan voi vo huong " << myScalar << ":" << endl;
    displayMatrix(resultMatrix);

    return 0;
}
  • Giải thích: Hàm scalarMultiplyMatrix chỉ cần một vòng lặp lồng nhau để duyệt qua tất cả các phần tử của ma trận đầu vào và nhân chúng với scalar. Không cần kiểm tra kích thước phức tạp ở đây.
4. Phép Nhân Hai Ma trận

Đây là phép toán phức tạp nhất trong các phép cơ bản và cũng là một trong những phép toán quan trọng nhất. Để nhân ma trận $A$ với ma trận $B$ ($A \times B$), số cột của ma trận $A$ phải bằng số hàng của ma trận $B$. Nếu ma trận $A$ có kích thước $m \times n$ (m hàng, n cột) và ma trận $B$ có kích thước $n \times p$ (n hàng, p cột), thì ma trận kết quả $C$ sẽ có kích thước $m \times p$ (m hàng, p cột). Mỗi phần tử $C_{ij}$ của ma trận kết quả là tổng của tích các phần tử tương ứng của hàng thứ i của ma trận $A$ và cột thứ j của ma trận $B$. Tức là: $C_{ij} = \sum_{k=0}^{n-1} A_{ik} \times B_{kj}$

Công thức này cho thấy chúng ta sẽ cần đến ba vòng lặp lồng nhau để tính toán.

#include <vector>
#include <iostream>
#include <stdexcept>

using namespace std;

// Hàm nhân hai ma trận
vector<vector<int>> multiplyMatrices(const vector<vector<int>>& A, const vector<vector<int>>& B) {
    // Lấy kích thước
    int A_rows = A.size();
    int A_cols = (A.empty()) ? 0 : A[0].size();
    int B_rows = B.size();
    int B_cols = (B.empty()) ? 0 : B[0].size();

    // Kiểm tra điều kiện nhân ma trận: So cot cua A phai bang so hang cua B
    if (A_cols != B_rows) {
        throw runtime_error("Loi: So cot cua ma tran dau tien phai bang so hang cua ma tran thu hai de nhan.");
    }

    // Kích thước ma trận kết quả: A_rows x B_cols
    vector<vector<int>> result(A_rows, vector<int>(B_cols, 0)); // Khởi tạo ma trận kết quả với giá trị 0

    // Thực hiện phép nhân
    for (int i = 0; i < A_rows; ++i) { // Duyệt qua các hàng của ma trận kết quả (và ma trận A)
        for (int j = 0; j < B_cols; ++j) { // Duyệt qua các cột của ma trận kết quả (và ma trận B)
            // Tính phần tử C[i][j]
            for (int k = 0; k < A_cols; ++k) { // Duyệt qua các phần tử để tính tổng tích
                result[i][j] += A[i][k] * B[k][j];
            }
        }
    }

    return result; // Trả về ma trận kết quả
}

// Hàm hiển thị (sử dụng lại)
void displayMatrix(const vector<vector<int>>& matrix) {
    if (matrix.empty()) {
        cout << "Ma tran rong" << endl;
        return;
    }
    for (const auto& row : matrix) {
        for (int element : row) {
            cout << element << " ";
        }
        cout << endl;
    }
}

int main() {
    vector<vector<int>> matrixA = {
        {1, 2, 3},
        {4, 5, 6}
    }; // Kích thước 2x3

    vector<vector<int>> matrixB = {
        {7, 8},
        {9, 10},
        {11, 12}
    }; // Kích thước 3x2

    // Kích thước hợp lệ để nhân: 3 cột của A == 3 hàng của B. Kết quả sẽ là 2x2.

    try {
        vector<vector<int>> matrixProd = multiplyMatrices(matrixA, matrixB);
        cout << "Tich cua hai ma tran:" << endl;
        displayMatrix(matrixProd);
    } catch (const runtime_error& e) {
        cerr << e.what() << endl;
    }

    return 0;
}
  • Giải thích: Hàm multiplyMatrices kiểm tra điều kiện cực kỳ quan trọng là số cột của A bằng số hàng của B. Sau đó, nó tạo ma trận result với kích thước đúng (số hàng của A x số cột của B) và khởi tạo các phần tử bằng 0 (vì chúng ta sẽ cộng dồn tích vào đó). Ba vòng lặp: vòng ngoài cùng cho hàng i của kết quả (và A), vòng giữa cho cột j của kết quả (và B), và vòng trong cùng cho chỉ số k chạy dọc theo cột của A và hàng của B để tính tổng tích.
5. Phép Chuyển vị Ma trận (Transpose)

Phép chuyển vị ma trận $A$ (ký hiệu $A^T$) là tạo ra một ma trận mới bằng cách hoán đổi các hàng thành các cột và các cột thành các hàng. Nếu ma trận $A$ có kích thước $m \times n$, ma trận chuyển vị $A^T$ sẽ có kích thước $n \times m$. Phần tử ở vị trí $(i, j)$ trong $A^T$ chính là phần tử ở vị trí $(j, i)$ trong ma trận gốc $A$.

#include <vector>
#include <iostream>

using namespace std;

// Hàm chuyển vị ma trận
vector<vector<int>> transposeMatrix(const vector<vector<int>>& matrix) {
    if (matrix.empty()) {
        return {}; // Trả về ma trận rỗng nếu ma trận đầu vào rỗng
    }

    int rows = matrix.size();
    int cols = matrix[0].size();

    // Kích thước ma trận chuyển vị: cols x rows
    vector<vector<int>> result(cols, vector<int>(rows)); // Khởi tạo ma trận kết quả

    // Thực hiện phép chuyển vị
    for (int i = 0; i < rows; ++i) { // Duyệt qua các hàng của ma trận gốc
        for (int j = 0; j < cols; ++j) { // Duyệt qua các cột của ma trận gốc
            result[j][i] = matrix[i][j]; // Hoán đổi chỉ số hàng và cột
        }
    }

    return result; // Trả về ma trận kết quả
}

// Hàm hiển thị (sử dụng lại)
void displayMatrix(const vector<vector<int>>& matrix) {
    if (matrix.empty()) {
        cout << "Ma tran rong" << endl;
        return;
    }
    for (const auto& row : matrix) {
        for (int element : row) {
            cout << element << " ";
        }
        cout << endl;
    }
}

int main() {
    vector<vector<int>> myMatrix = {
        {1, 2, 3},
        {4, 5, 6}
    }; // Kích thước 2x3

    vector<vector<int>> transposedMatrix = transposeMatrix(myMatrix); // Kết quả sẽ là 3x2

    cout << "Ma tran goc:" << endl;
    displayMatrix(myMatrix);

    cout << "Ma tran sau khi chuyen vi:" << endl;
    displayMatrix(transposedMatrix);

    return 0;
}
  • Giải thích: Hàm transposeMatrix xác định kích thước của ma trận gốc và tạo ma trận result với kích thước ngược lại (số cột của gốc thành số hàng của kết quả, và ngược lại). Vòng lặp lồng nhau duyệt qua ma trận gốc, nhưng khi gán giá trị vào ma trận kết quả, chúng ta hoán đổi chỉ số hàng và cột: result[j][i] = matrix[i][j].

Comments

There are no comments at the moment.