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

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ản và thao 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ênmatrix
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 đếnROWS - 1
. Biếni
đó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 đếnCOLS - 1
cho mỗi hàngi
. Biếnj
đóng vai trò là chỉ số cột hiện tại. matrix[i][j]
truy cập phần tử tại hàngi
, cộtj
.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ằngROWS * 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àosum
. - Sau khi duyệt xong,
average
được tính bằngsum
chia chocount
. Chúng ta dùngstatic_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ữ maxVal
và minVal
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 maxVal
và minVal
, 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:
maxVal
vàminVal
được khởi tạo bằngmatrix[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ớimaxVal
vàminVal
hiện tại. - Nếu
matrix[i][j]
lớn hơnmaxVal
,maxVal
được cập nhật thànhmatrix[i][j]
. - Nếu
matrix[i][j]
nhỏ hơnminVal
,minVal
được cập nhật thànhmatrix[i][j]
. - Sau khi duyệt hết mảng,
maxVal
vàminVal
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ớioriginalMatrix
. - 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 (
COLS
vàROWS
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>
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ộtj
(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
i
vàj
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ộtj
(vớii
,j
bắt đầu từ 0) lài + j + 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ụngcin
). - 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ý.
- Bạn cần đọc vào số nguyên dương
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ứci + 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.
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
cout
vàcin
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ơnendl
vìendl
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.
Comments