Bài 27.4: Bài tập thực hành ma trận xoắn ốc trong C++

Bài 27.4: Bài tập thực hành ma trận xoắn ốc trong C++
Chào mừng bạn đến với bài viết tiếp theo trong series Khám phá Lập trình C++! Hôm nay, chúng ta sẽ cùng nhau chinh phục một bài toán khá thú vị và kinh điển liên quan đến ma trận (mảng 2 chiều) - bài toán điền ma trận theo hình xoắn ốc (Spiral Matrix). Đây là một bài tập thực hành tuyệt vời để củng cố kỹ năng xử lý mảng và phát triển tư duy thuật toán của bạn.
Bài Toán Ma Trận Xoắn Ốc là gì?
Bài toán đặt ra là: cho trước kích thước của một ma trận vuông N x N (hoặc tổng quát hơn là M x N), hãy điền các số nguyên liên tiếp từ 1 đến NN (hoặc MN) vào ma trận đó theo một đường đi hình xoắn ốc từ ngoài vào trong.
Ví dụ, với ma trận 3x3, kết quả mong muốn sẽ là:
1 2 3
8 9 4
7 6 5
Với ma trận 4x4:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
Nghe có vẻ đơn giản, nhưng để viết code một cách chính xác và hiệu quả, chúng ta cần có một thuật toán rõ ràng.
Ý Tưởng Thuật Toán
Làm thế nào để mô phỏng đường đi "xoắn ốc" này bằng code? Hãy quan sát đường đi của các số trong ví dụ:
- Chúng ta bắt đầu từ góc trên bên trái (0,0).
- Điền các số theo hướng phải cho đến khi gặp biên.
- Sau đó, chuyển hướng xuống dưới, điền các số theo hướng xuống cho đến khi gặp biên.
- Tiếp theo, chuyển hướng sang trái, điền các số theo hướng trái cho đến khi gặp biên.
- Cuối cùng, chuyển hướng lên trên, điền các số theo hướng lên cho đến khi gặp biên.
Sau khi hoàn thành một vòng (phải -> xuống -> trái -> lên), chúng ta đã điền được lớp "vỏ" ngoài cùng của hình xoắn ốc. Vùng chưa điền lúc này là một ma trận nhỏ hơn nằm bên trong. Chúng ta chỉ cần lặp lại quá trình tương tự với ma trận nhỏ hơn này cho đến khi toàn bộ ma trận được điền đầy.
Để quản lý các "biên" của ma trận nhỏ dần này, chúng ta có thể sử dụng bốn biến:
top
: Chỉ số hàng trên cùng của vùng hiện tại.bottom
: Chỉ số hàng dưới cùng của vùng hiện tại.left
: Chỉ số cột bên trái nhất của vùng hiện tại.right
: Chỉ số cột bên phải nhất của vùng hiện tại.
Ban đầu, top = 0
, bottom = N-1
, left = 0
, right = N-1
(hoặc M-1 và N-1 cho ma trận M x N). Sau mỗi khi hoàn thành một hướng di chuyển, chúng ta sẽ cập nhật một trong các biến biên này để "thu nhỏ" vùng làm việc lại.
Các Bước Thực Hiện Chi Tiết
- Khởi tạo một ma trận M x N với tất cả các phần tử bằng 0 (hoặc giá trị mặc định nào đó).
- Khởi tạo biến
count = 1
(số sẽ điền vào ô tiếp theo). - Khởi tạo
top = 0
,bottom = M-1
,left = 0
,right = N-1
. - Sử dụng một vòng lặp chính tiếp tục chạy miễn là
top <= bottom
vàleft <= right
(điều kiện này đảm bảo vẫn còn vùng để điền). Bên trong vòng lặp chính, thực hiện 4 bước điền theo 4 hướng:
- Điền từ trái sang phải (hàng
top
): Duyệt cộtj
từleft
đếnright
. Điềnmatrix[top][j] = count++
. Sau khi điền xong hàng này, tăngtop
lên 1 (top++
) vì hàng trên cùng đã được xử lý. - Kiểm tra điều kiện thoát: Sau mỗi bước điền theo một hướng, quan trọng là phải kiểm tra lại điều kiện
top <= bottom
vàleft <= right
. Nếu điều kiện này không còn đúng, tức là ma trận đã được điền đầy hoặc chỉ còn lại một hàng/cột đơn lẻ đã được xử lý ở bước trước đó, thoát khỏi vòng lặp chính. - Điền từ trên xuống dưới (cột
right
): Duyệt hàngi
từtop
đếnbottom
. Điềnmatrix[i][right] = count++
. Sau khi điền xong cột này, giảmright
đi 1 (right--
). - Kiểm tra điều kiện thoát: Lại kiểm tra
top <= bottom
vàleft <= right
. - Điền từ phải sang trái (hàng
bottom
): Duyệt cộtj
từright
xuốngleft
. Điềnmatrix[bottom][j] = count++
. Sau khi điền xong hàng này, giảmbottom
đi 1 (bottom--
). - Kiểm tra điều kiện thoát: Lại kiểm tra
top <= bottom
vàleft <= right
. - Điền từ dưới lên trên (cột
left
): Duyệt hàngi
từbottom
xuốngtop
. Điềnmatrix[i][left] = count++
. Sau khi điền xong cột này, tăngleft
lên 1 (left++
). - Kiểm tra điều kiện thoát: Lại kiểm tra
top <= bottom
vàleft <= right
.
- Điền từ trái sang phải (hàng
Khi vòng lặp chính kết thúc, ma trận đã được điền đầy đủ theo hình xoắn ốc.
Code Minh Họa (Ví dụ 1: Ma trận vuông)
Chúng ta sẽ sử dụng vector<vector<int>>
để biểu diễn ma trận và cout
để in kết quả. Để kết quả in ra thẳng hàng dễ nhìn, chúng ta có thể dùng thêm setw
và fixed
từ thư viện <iomanip>
.
#include <iostream>
#include <vector>
#include <iomanip> // For setw and fixed
int main() {
int n;
cout << "Nhap kich thuoc ma tran vuong N: ";
cin >> n;
// Tao ma tran N x N va khoi tao tat ca phan tu bang 0
vector<vector<int>> matrix(n, vector<int>(n, 0));
int count = 1; // So bat dau dien
int top = 0;
int bottom = n - 1;
int left = 0;
int right = n - 1;
// Vong lap chinh tiep tuc khi con vung de dien
while (top <= bottom && left <= right) {
// 1. Dien tu trai sang phai (Hang top)
for (int j = left; j <= right; ++j) {
matrix[top][j] = count++;
}
top++; // Tang top vi hang tren cung da xong
// Kiem tra dieu kien sau khi tang top
if (top > bottom || left > right) break;
// 2. Dien tu tren xuong duoi (Cot right)
for (int i = top; i <= bottom; ++i) {
matrix[i][right] = count++;
}
right--; // Giam right vi cot ben phai da xong
// Kiem tra dieu kien sau khi giam right
if (top > bottom || left > right) break;
// 3. Dien tu phai sang trai (Hang bottom)
for (int j = right; j >= left; --j) {
matrix[bottom][j] = count++;
}
bottom--; // Giam bottom vi hang duoi cung da xong
// Kiem tra dieu kien sau khi giam bottom
if (top > bottom || left > right) break;
// 4. Dien tu duoi len tren (Cot left)
for (int i = bottom; i >= top; --i) {
matrix[i][left] = count++;
}
left++; // Tang left vi cot ben trai da xong
// Kiem tra dieu kien sau khi tang left
if (top > bottom || left > right) break;
}
// In ma tran ra man hinh
cout << "\nMa tran xoan oc " << n << "x" << n << " la:\n";
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
// Dung setw de can le va in so
cout << setw(4) << matrix[i][j];
}
cout << endl; // Xuong dong sau moi hang
}
return 0;
}
Giải Thích Code (Ví dụ 1)
- Chúng ta khai báo biến
n
để lấy kích thước ma trận vuông từ người dùng. vector<vector<int>> matrix(n, vector<int>(n, 0));
tạo ra một ma trậnn x n
và khởi tạo tất cả các ô bằng 0. Đây là cách phổ biến để làm việc với mảng 2 chiều động trong C++.- Các biến
count
,top
,bottom
,left
,right
được khởi tạo như đã phân tích ở phần ý tưởng. - Vòng lặp
while (top <= bottom && left <= right)
là vòng lặp chính. Nó sẽ tiếp tục chạy cho đến khi các biêntop
vượt quabottom
hoặcleft
vượt quaright
, điều này xảy ra khi toàn bộ ma trận đã được điền. - Bên trong vòng lặp
while
:- Vòng
for
đầu tiên điền từleft
sangright
trên hàngtop
. Sau đó,top
được tăng lên để "cắt bỏ" hàng đã điền. - Rất quan trọng: Sau mỗi bước điền (phải, xuống, trái, lên), chúng ta kiểm tra lại điều kiện
top <= bottom && left <= right
(hoặctop > bottom || left > right
để break). Điều này xử lý các trường hợp ma trận có kích thước lẻ (như 3x3, 5x5) nơi có thể còn lại một hàng hoặc một cột duy nhất ở trung tâm sau khi hoàn thành 3 hướng di chuyển. - Vòng
for
thứ hai điền từtop
xuốngbottom
trên cộtright
. Sau đó,right
được giảm đi. - Vòng
for
thứ ba điền từright
sangleft
trên hàngbottom
. Sau đó,bottom
được giảm đi. - Vòng
for
cuối cùng điền từbottom
lêntop
trên cộtleft
. Sau đó,left
được tăng lên.
- Vòng
- Sau vòng lặp
while
, ma trận đã được điền. Chúng ta dùng hai vòng lặpfor
lồng nhau để duyệt và in ma trận ra màn hình.setw(4)
đảm bảo mỗi số chiếm ít nhất 4 ký tự, giúp các cột thẳng hàng kể cả khi số có 1 hoặc 2 chữ số.
Code Minh Họa (Ví dụ 2: Ma trận chữ nhật)
Thuật toán boundary-shrinking hoạt động tốt cho cả ma trận chữ nhật M x N. Chúng ta chỉ cần thay đổi cách khởi tạo kích thước và các biến biên ban đầu.
#include <iostream>
#include <vector>
#include <iomanip> // For setw and fixed
int main() {
int m, n;
cout << "Nhap so hang M: ";
cin >> m;
cout << "Nhap so cot N: ";
cin >> n;
// Tao ma tran M x N
vector<vector<int>> matrix(m, vector<int>(n, 0));
int count = 1; // So bat dau dien
int top = 0;
int bottom = m - 1;
int left = 0;
int right = n - 1;
// Vong lap chinh tiep tuc khi con vung de dien
while (top <= bottom && left <= right) {
// 1. Dien tu trai sang phai (Hang top)
for (int j = left; j <= right; ++j) {
matrix[top][j] = count++;
}
top++; // Tang top vi hang tren cung da xong
// Kiem tra dieu kien sau khi tang top
if (top > bottom || left > right) break;
// 2. Dien tu tren xuong duoi (Cot right)
for (int i = top; i <= bottom; ++i) {
matrix[i][right] = count++;
}
right--; // Giam right vi cot ben phai da xong
// Kiem tra dieu kien sau khi giam right
if (top > bottom || left > right) break;
// 3. Dien tu phai sang trai (Hang bottom)
for (int j = right; j >= left; --j) {
matrix[bottom][j] = count++;
}
bottom--; // Giam bottom vi hang duoi cung da xong
// Kiem tra dieu kien sau khi giam bottom
if (top > bottom || left > right) break;
// 4. Dien tu duoi len tren (Cot left)
for (int i = bottom; i >= top; --i) {
matrix[i][left] = count++;
}
left++; // Tang left vi cot ben trai da xong
// Kiem tra dieu kien sau khi tang left
if (top > bottom || left > right) break;
}
// In ma tran ra man hinh
cout << "\nMa tran xoan oc " << m << "x" << n << " la:\n";
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
cout << setw(4) << matrix[i][j];
}
cout << endl; // Xuong dong sau moi hang
}
return 0;
}
Giải Thích Code (Ví dụ 2)
Code cho ma trận chữ nhật gần như y hệt code cho ma trận vuông. Sự khác biệt chính là:
- Chúng ta đọc vào hai biến
m
vàn
cho số hàng và số cột. - Khi khởi tạo ma trận:
vector<vector<int>> matrix(m, vector<int>(n, 0));
. - Khi khởi tạo các biên:
bottom = m - 1;
vàright = n - 1;
.
Logic thuật toán điền theo 4 hướng và cập nhật biên vẫn hoàn toàn áp dụng được cho ma trận chữ nhật, làm nổi bật tính tổng quát của phương pháp này.
Lưu Ý và Mở Rộng
- Điều kiện kiểm tra
if (top > bottom || left > right) break;
sau mỗi vòngfor
là cực kỳ quan trọng, đặc biệt với ma trận có một chiều là 1 (ví dụ 1x5, 5x1) hoặc ma trận có kích thước lẻ. Nó đảm bảo rằng chúng ta không cố gắng truy cập vào các vị trí ngoài giới hạn hoặc điền đè lên các ô đã điền khi vùng còn lại chỉ là một hàng hoặc một cột đơn. - Bài toán ma trận xoắn ốc còn có một biến thể khác là duyệt ma trận xoắn ốc (cho trước một ma trận đã điền, hãy in ra các phần tử theo thứ tự xoắn ốc). Thuật toán sử dụng 4 biên và 4 hướng di chuyển tương tự, chỉ khác là thay vì điền số, chúng ta chỉ cần truy cập và in ra giá trị của phần tử.
Comments