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>
using namespace std;
int main() {
vector<vector<int>> m = {
{1, 2, 3},
{4, 5, 6}
};
cout << "Phan tu o [0][1]: " << m[0][1] << endl;
cout << "So hang: " << m.size() << endl;
cout << "So cot: " << m[0].size() << endl;
return 0;
}
Phan tu o [0][1]: 2
So hang: 2
So cot: 3
- 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>
using namespace std;
void in_mt(const vector<vector<int>>& m) {
if (m.empty()) {
cout << "Ma tran rong" << endl;
return;
}
for (const auto& h : m) {
for (int e : h) {
cout << setw(4) << e << " ";
}
cout << endl;
}
}
int main() {
vector<vector<int>> m = {
{10, 20, 30},
{40, 50, 60},
{70, 80, 90}
};
cout << "Ma tran cua toi:" << endl;
in_mt(m);
return 0;
}
Ma tran cua toi:
10 20 30
40 50 60
70 80 90
- 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>
using namespace std;
vector<vector<int>> cong_mt(const vector<vector<int>>& a, const vector<vector<int>>& b) {
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 h = a.size();
int c = a[0].size();
vector<vector<int>> kq(h, vector<int>(c));
for (int i = 0; i < h; ++i) {
for (int j = 0; j < c; ++j) {
kq[i][j] = a[i][j] + b[i][j];
}
}
return kq;
}
void in_mt(const vector<vector<int>>& m) {
if (m.empty()) {
cout << "Ma tran rong" << endl;
return;
}
for (const auto& h : m) {
for (int e : h) {
cout << e << " ";
}
cout << endl;
}
}
int main() {
vector<vector<int>> mt_a = {
{1, 2},
{3, 4}
};
vector<vector<int>> mt_b = {
{5, 6},
{7, 8}
};
try {
vector<vector<int>> tong_mt = cong_mt(mt_a, mt_b);
cout << "Tong cua hai ma tran:" << endl;
in_mt(tong_mt);
} catch (const runtime_error& e) {
cerr << e.what() << endl;
}
return 0;
}
Tong cua hai ma tran:
6 8
10 12
- 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;
vector<vector<int>> tru_mt(const vector<vector<int>>& a, const vector<vector<int>>& b) {
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 h = a.size();
int c = a[0].size();
vector<vector<int>> kq(h, vector<int>(c));
for (int i = 0; i < h; ++i) {
for (int j = 0; j < c; ++j) {
kq[i][j] = a[i][j] - b[i][j];
}
}
return kq;
}
void in_mt(const vector<vector<int>>& m) {
if (m.empty()) {
cout << "Ma tran rong" << endl;
return;
}
for (const auto& h : m) {
for (int e : h) {
cout << e << " ";
}
cout << endl;
}
}
int main() {
vector<vector<int>> mt_a = {
{10, 20},
{30, 40}
};
vector<vector<int>> mt_b = {
{5, 6},
{7, 8}
};
try {
vector<vector<int>> hieu_mt = tru_mt(mt_a, mt_b);
cout << "Hieu cua hai ma tran:" << endl;
in_mt(hieu_mt);
} catch (const runtime_error& e) {
cerr << e.what() << endl;
}
return 0;
}
Hieu cua hai ma tran:
5 14
23 32
- 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;
vector<vector<int>> nhan_vo_huong_mt(const vector<vector<int>>& m, int k) {
if (m.empty()) {
return {};
}
int h = m.size();
int c = m[0].size();
vector<vector<int>> kq(h, vector<int>(c));
for (int i = 0; i < h; ++i) {
for (int j = 0; j < c; ++j) {
kq[i][j] = m[i][j] * k;
}
}
return kq;
}
void in_mt(const vector<vector<int>>& m) {
if (m.empty()) {
cout << "Ma tran rong" << endl;
return;
}
for (const auto& h : m) {
for (int e : h) {
cout << e << " ";
}
cout << endl;
}
}
int main() {
vector<vector<int>> m = {
{1, 2, 3},
{4, 5, 6}
};
int k = 5;
vector<vector<int>> kq_mt = nhan_vo_huong_mt(m, k);
cout << "Ma tran sau khi nhan voi vo huong " << k << ":" << endl;
in_mt(kq_mt);
return 0;
}
Ma tran sau khi nhan voi vo huong 5:
5 10 15
20 25 30
- 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;
vector<vector<int>> nhan_mt(const vector<vector<int>>& a, const vector<vector<int>>& b) {
int h1 = a.size();
int c1 = (a.empty()) ? 0 : a[0].size();
int h2 = b.size();
int c2 = (b.empty()) ? 0 : b[0].size();
if (c1 != h2) {
throw runtime_error("Loi: So cot cua ma tran dau tien phai bang so hang cua ma tran thu hai de nhan.");
}
vector<vector<int>> kq(h1, vector<int>(c2, 0));
for (int i = 0; i < h1; ++i) {
for (int j = 0; j < c2; ++j) {
for (int k = 0; k < c1; ++k) {
kq[i][j] += a[i][k] * b[k][j];
}
}
}
return kq;
}
void in_mt(const vector<vector<int>>& m) {
if (m.empty()) {
cout << "Ma tran rong" << endl;
return;
}
for (const auto& h : m) {
for (int e : h) {
cout << e << " ";
}
cout << endl;
}
}
int main() {
vector<vector<int>> mt_a = {
{1, 2, 3},
{4, 5, 6}
};
vector<vector<int>> mt_b = {
{7, 8},
{9, 10},
{11, 12}
};
try {
vector<vector<int>> tich_mt = nhan_mt(mt_a, mt_b);
cout << "Tich cua hai ma tran:" << endl;
in_mt(tich_mt);
} catch (const runtime_error& e) {
cerr << e.what() << endl;
}
return 0;
}
Tich cua hai ma tran:
58 64
139 154
- 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;
vector<vector<int>> chuyen_vi_mt(const vector<vector<int>>& m) {
if (m.empty()) {
return {};
}
int h = m.size();
int c = m[0].size();
vector<vector<int>> kq(c, vector<int>(h));
for (int i = 0; i < h; ++i) {
for (int j = 0; j < c; ++j) {
kq[j][i] = m[i][j];
}
}
return kq;
}
void in_mt(const vector<vector<int>>& m) {
if (m.empty()) {
cout << "Ma tran rong" << endl;
return;
}
for (const auto& h : m) {
for (int e : h) {
cout << e << " ";
}
cout << endl;
}
}
int main() {
vector<vector<int>> m = {
{1, 2, 3},
{4, 5, 6}
};
vector<vector<int>> mt_chuyen_vi = chuyen_vi_mt(m);
cout << "Ma tran goc:" << endl;
in_mt(m);
cout << "Ma tran sau khi chuyen vi:" << endl;
in_mt(mt_chuyen_vi);
return 0;
}
Ma tran goc:
1 2 3
4 5 6
Ma tran sau khi chuyen vi:
1 4
2 5
3 6
- 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