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

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ẽ và 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ỗivector
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ủavector<int>
. Mỗivector<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ểuconst&
để 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ặpfor 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ộtruntime_error
để báo lỗi. Nếu hợp lệ, nó tạo một ma trậnresult
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ầnmain
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ớiaddMatrices
, 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ớiscalar
. 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ậnresult
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àngi
của kết quả (và A), vòng giữa cho cộtj
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ậnresult
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