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() {
int matrix[3][4];
int data[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
int zeros[3][3] = {0};
cout << "Khai bao thanh cong!\n";
return 0;
}
Output:
Khai bao thanh cong!
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 a[2][3] = {
{10, 20, 30},
{40, 50, 60}
};
cout << "a[0][0]: " << a[0][0] << '\n';
cout << "a[0][2]: " << a[0][2] << '\n';
cout << "a[1][1]: " << a[1][1] << '\n';
cout << "a[1][2]: " << a[1][2] << '\n';
a[0][0] = 100;
cout << "a[0][0] sau thay doi: " << a[0][0] << '\n';
return 0;
}
Output:
a[0][0]: 10
a[0][2]: 30
a[1][1]: 50
a[1][2]: 60
a[0][0] sau thay doi: 100
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 H = 3;
const int C = 4;
int a[H][C] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
cout << "Ma tran:\n";
for (int i = 0; i < H; ++i) {
for (int j = 0; j < C; ++j) {
cout << a[i][j] << "\t";
}
cout << '\n';
}
return 0;
}
Output:
Ma tran:
1 2 3 4
5 6 7 8
9 10 11 12
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 << '\n';
đượ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 H = 2;
const int C = 3;
int a[H][C];
cout << "Nhap ma tran (" << H << "x" << C << "):\n";
for (int i = 0; i < H; ++i) {
for (int j = 0; j < C; ++j) {
cout << "Nhap a[" << i << "][" << j << "]: ";
cin >> a[i][j];
}
}
cout << "\nMa tran vua nhap:\n";
for (int i = 0; i < H; ++i) {
for (int j = 0; j < C; ++j) {
cout << a[i][j] << "\t";
}
cout << '\n';
}
return 0;
}
Simulated Input:
1
2
3
4
5
6
Simulated Output:
Nhap ma tran (2x3):
Nhap a[0][0]: 1
Nhap a[0][1]: 2
Nhap a[0][2]: 3
Nhap a[1][0]: 4
Nhap a[1][1]: 5
Nhap a[1][2]: 6
Ma tran vua nhap:
1 2 3
4 5 6
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 H = 3;
const int C = 4;
int a[H][C] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
long long tong = 0;
int so_pt = H * C;
for (int i = 0; i < H; ++i) {
for (int j = 0; j < C; ++j) {
tong += a[i][j];
}
}
double tbc = 0.0;
if (so_pt > 0) {
tbc = static_cast<double>(tong) / so_pt;
}
cout << "Tong: " << tong << '\n';
cout << "TBC: " << tbc << '\n';
return 0;
}
Output:
Tong: 78
TBC: 6.5
Giải thích:
- Biến
tong
được khởi tạo bằng 0. - Biến
so_pt
để đếm số phần tử, cũng có thể tính đơn giản bằngH * C
. - Bên trong vòng lặp lồng nhau,
tong += a[i][j];
cộng giá trị của phần tử hiện tại vàotong
. - Sau khi duyệt xong,
tbc
được tính bằngtong
chia choso_pt
. Chúng ta dùngstatic_cast<double>(tong)
để đả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ữ max_val
và min_val
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ụ: a[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 max_val
và min_val
, 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>
int main() {
const int H = 3;
const int C = 4;
int a[H][C] = {
{1, 20, 3, 4},
{5, -6, 7, 8},
{9, 10, 11, 12}
};
int max_val = a[0][0];
int min_val = a[0][0];
for (int i = 0; i < H; ++i) {
for (int j = 0; j < C; ++j) {
if (a[i][j] > max_val) {
max_val = a[i][j];
}
if (a[i][j] < min_val) {
min_val = a[i][j];
}
}
}
cout << "Lon nhat: " << max_val << '\n';
cout << "Nho nhat: " << min_val << '\n';
return 0;
}
Output:
Lon nhat: 20
Nho nhat: -6
Giải thích:
max_val
vàmin_val
được khởi tạo bằnga[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ử
a[i][j]
được so sánh vớimax_val
vàmin_val
hiện tại. - Nếu
a[i][j]
lớn hơnmax_val
,max_val
được cập nhật thànha[i][j]
. - Nếu
a[i][j]
nhỏ hơnmin_val
,min_val
được cập nhật thànha[i][j]
. - Sau khi duyệt hết mảng,
max_val
vàmin_val
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 H = 3;
const int C = 4;
int a[H][C] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int hang_muc_tieu = 1;
long long tong_hang = 0;
if (hang_muc_tieu >= 0 && hang_muc_tieu < H) {
for (int j = 0; j < C; ++j) {
tong_hang += a[hang_muc_tieu][j];
}
cout << "Tong hang " << hang_muc_tieu << ": " << tong_hang << '\n';
} else {
cout << "Hang " << hang_muc_tieu << " khong hop le!\n";
}
return 0;
}
Output:
Tong hang 1: 26
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 a[hang_muc_tieu][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 H = 3;
const int C = 4;
int a[H][C] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int cot_muc_tieu = 2;
long long tong_cot = 0;
if (cot_muc_tieu >= 0 && cot_muc_tieu < C) {
for (int i = 0; i < H; ++i) {
tong_cot += a[i][cot_muc_tieu];
}
cout << "Tong cot " << cot_muc_tieu << ": " << tong_cot << '\n';
} else {
cout << "Cot " << cot_muc_tieu << " khong hop le!\n";
}
return 0;
}
Output:
Tong cot 2: 21
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 a[i][cot_muc_tieu]
. Việc kiểm tra chỉ số (hang_muc_tieu
, cot_muc_tieu
) 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 H = 2;
const int C = 3;
int a[H][C] = {
{1, 2, 3},
{4, 5, 6}
};
int b[C][H];
for (int i = 0; i < H; ++i) {
for (int j = 0; j < C; ++j) {
b[j][i] = a[i][j];
}
}
cout << "Ma tran goc:\n";
for (int i = 0; i < H; ++i) {
for (int j = 0; j < C; ++j) {
cout << a[i][j] << "\t";
}
cout << '\n';
}
cout << "\nMa tran chuyen vi:\n";
for (int i = 0; i < C; ++i) {
for (int j = 0; j < H; ++j) {
cout << b[i][j] << "\t";
}
cout << '\n';
}
return 0;
}
Output:
Ma tran goc:
1 2 3
4 5 6
Ma tran chuyen vi:
1 4
2 5
3 6
Giải thích:
- Chúng ta khai báo một ma trận mới
b
với số hàng và số cột đảo ngược so vớia
. - 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ử
a[i][j]
vào vị trí đảo ngược chỉ sốb[j][i]
. - Khi in ma trận chuyển vị, chúng ta cần sử dụng kích thước mới (
C
vàH
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.
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
'\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.
#include <iostream>
int main() {
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cout << i + j + 2;
if (j < n - 1) {
cout << " ";
}
}
cout << '\n';
}
return 0;
}
Output với Input là 4
:
2 3 4 5
3 4 5 6
4 5 6 7
5 6 7 8
Comments