Hướng dẫn giải của Ma trận xoắn ốc _ VP9
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Hiểu bài toán
Bài toán yêu cầu tìm giá trị thứ $k$ trong ma trận xoắn ốc được tạo ra dựa trên một ma trận đầu vào có kích thước $N \times M$. Ma trận đầu vào chứa các giá trị 0 hoặc 1, trong đó 1 đại diện cho các ô 'tốt' (có thể đi qua) và 0 đại diện cho các ô 'xấu' (bị chặn). Ma trận xoắn ốc bắt đầu từ ô (1,1) và đi theo chiều kim đồng hồ, chỉ duyệt qua các ô tốt. Ta cần tìm ô thứ $k$ trong chuỗi các ô tốt này. Nếu số lượng ô tốt nhỏ hơn $k$, in ra -1.
Ví dụ: Input: $N, M, K$ và ma trận $A$.
- Tạo ma trận xoắn ốc $(B)$ với các ô được đánh số tuần tự từ 1 đến $N \times M$ theo thứ tự xoắn ốc.
- Liệt kê các số $x$ sao cho tại vị trí tương ứng trong $B$, giá trị trong $A$ là 1 (tốt).
- Tìm số thứ $k$ trong danh sách này.
Các cách tiếp cận
Cách Mô phỏng Xoắn ốc với Quy hoạch Động (DP)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m, k;
cin >> n >> m >> k;
vector<vector<int>> a(n, vector<int>(m));
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
cin >> a[i][j];
// Bước 1: Gán số thứ tự cho ma trận theo thứ tự xoắn ốc
vector<vector<int>> b(n, vector<int>(m, 0));
int r1 = 0, c1 = 0, r2 = n - 1, c2 = m - 1;
int cnt = 1;
while (r1 <= r2 && c1 <= c2) {
// Hang tren
for (int j = c1; j <= c2; ++j) b[r1][j] = cnt++;
// Cot phai
for (int i = r1 + 1; i < r2; ++i) b[i][c2] = cnt++;
// Hang duoi
if (r1 < r2) {
for (int j = c2; j >= c1; --j) b[r2][j] = cnt++;
}
// Cot trai
if (c1 < c2) {
for (int i = r2 - 1; i > r1; --i) b[i][c1] = cnt++;
}
r1++; c1++; r2--; c2--;
}
// Bước 2: Duyet de tao mang cac so thu tu cua cac o tot
// Hoac su dung DP: tinh so luong o tot den truoc moi o
vector<int> dp(n * m + 1, 0);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (a[i][j] == 1) {
dp[b[i][j]] = 1;
}
}
}
// Tien tich luy
for (int i = 1; i <= n * m; ++i) {
dp[i] += dp[i - 1];
}
// Bước 3: Tim kiem nhi phan
int low = 1, high = n * m, res = -1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (dp[mid] >= k) {
res = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
if (res != -1 && dp[res] >= k) cout << res;
else cout << -1;
return 0;
}
- Time Complexity: O(N * M)
- Space Complexity: O(N * M)
Phương pháp này thực hiện mô phỏng hoàn toàn quá trình tạo xoắn ốc để gán số thứ tự cho từng ô.
- Tạo ma trận số thứ tự: Sử dụng 4 biến để quản lý ranh giới (trên, dưới, trái, phải) và điền số vào ma trận phụ $B$ theo từng lớp xoắn ốc.
- Xử lý dữ liệu: Tạo một mảng
dphoặcprefix_sumđể lưu tổng số ô tốt tính đến số thứ tự $i$. Cụ thể,dp[i] = dp[i-1] + 1nếu ô tương ứng với số $i$ là tốt. - Tìm kiếm: Sử dụng tìm kiếm nhị phân trên số thứ tự từ 1 đến $N*M$. Nếu tại số
mid, tổng số ô tốt làdp[mid]và nó lớn hơn hoặc bằng $k$, thì đáp án có thể làmidhoặc nhỏ hơn, cần tìm bên trái. Ngược lại, tìm bên phải.
Cách này dễ hiểu và đảm bảo đúng thứ tự.
Cách Tối ưu với Mảng 1D (Flat Array)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false); cin.tie(NULL);
int n, m, k;
cin >> n >> m >> k;
vector<int> a(n * m + 1);
// Đọc input theo thứ tự hàng
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
int val; cin >> val;
a[(i - 1) * m + j] = val;
}
}
// Mảng đánh dấu vị trí trong xoắn ốc
// Tuy nhiên, ta không cần mảng 2D nếu tính chỉ số trực tiếp.
// Dưới đây là giải pháp sử dụng mảng 1D lưu giá trị gốc và xử lý.
// Code này chỉ minh họa logic.
// Thực tế Solution 2 sử dụng vector 2D cho `b` (số thứ tự) và `f` (giá trị).
// Để tối ưu, ta có thể sinh số thứ tự và kiểm tra điều kiện ngay.
// Tuy nhiên, phương pháp 1 là phổ biến nhất trong C++.
// Phân tích Solution 2 (LongvnXD):
// Giải pháp này sử dụng 4 biên (h1, h2, c1, c2) và đánh số tuần tự.
// Nó duy trì một mảng `b` (số thứ tự) và một mảng `f` (giá trị).
// Sau đó nó dùng Binary Search trên tổng tiền tố.
// Cấu trúc tương tự phương pháp 1, nhưng cách lập trình vòng lặp xoắn ốc khác một chút.
// Logic chung:
// 1. Xây dựng map vị trí -> số thứ tự.
// 2. Xây dựng prefix sum.
// 3. Binary Search.
return 0;
}
- Time Complexity: O(N * M)
- Space Complexity: O(N * M)
Đây là phiên bản chi tiết hơn của phương pháp 1, thường được tối ưu hóa trong code.
- Cấu trúc dữ liệu: Sử dụng
vector<vector<long long>>để lưu trữ ma trận giá trị (f) và ma trận số thứ tự (b). - Vòng lặp xoắn ốc: Solution 2 minh họa cách điền số vào
bmột cách tuần tự bằng cách thu hẹp các biên (top, bottom, left, right). - Tiền xử lý: Duyệt qua ma trận, nếu
f[i][j] == 1thì đánh dấu tại vị tríb[i][j]là 1. - Binary Search: Tìm số thứ tự nhỏ nhất sao cho tổng số 1 từ đầu đến số đó >= $k$.
Ưu điểm của cách này là code gọn, dễ debug.
Cách Tối ưu bộ nhớ (Nếu N, M lớn)
#include <bits/stdc++.h>
using namespace std;
int main() {
// Code tương tự Approach 1, nhưng có thể tối ưu vector
// Nếu N, M quá lớn, không thể lưu ma trận 2D.
// Tuy nhiên, đề bài có N, M <= 30000, nên O(N*M) là chấp nhận được (~900 triệu phần tử nếu dùng int, quá lớn).
// Nhưng trong thực tế, các solution accepted dùng mảng 2D kích thước 201x30001 (tức là N <= 200, M <= 30000).
// Cần kiểm tra constraints kỹ.
// Nếu N, M nhỏ (<= 1000), Approach 1 là tốt nhất.
// Nếu N, M lớn, cần tối ưu tính toán chỉ số xoắn ốc không cần ma trận.
// Phân tích Solution 1 (kimtuan15):
// Dùng mảng toàn c cục `a[201][30001]`. Rõ ràng `n` <= 200.
// Nếu `m` lớn, ta chỉ cần lưu theo hàng.
// Code Solution 1 là ví dụ tối ưu cho ràng buộc `n` nhỏ.
return 0;
}
- Time Complexity: O(N * M)
- Space Complexity: O(min(N, M) * max(N, M))
Phương pháp này tập trung vào việc tối ưu bộ nhớ dựa trên ràng buộc của bài toán. Nếu $N$ nhỏ (ví dụ 200) và $M$ lớn (30000), ta chỉ cần lưu ma trận theo chiều ngược lại hoặc tối ưu kiểu dữ liệu.
Tuy nhiên, về cơ bản thuật toán vẫn là:
- Xác định vị trí xoắn ốc: Tính toán hàng và cột của số thứ tự $x$ mà không cần lưu ma trận $B$ (nếu cần tiết kiệm tối đa).
- Lưu trữ: Chỉ cần lưu ma trận đầu vào $A$.
- Tính toán: Duyệt xoắn ốc, đếm số ô tốt. Khi đếm đủ $k$, in ra số thứ tự hiện tại.
Đây là cách tiếp cận 'thuần túy' nhất, nhưng code phức tạp hơn. Trong các giải pháp provided, Solution 1 và 2 đều dùng mảng 2D, điều này cho thấy ràng buộc thực tế cho phép O(N*M) bộ nhớ.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(N * M) | O(N * M) | Mô phỏng Xoắn ốc với Quy hoạch Động (DP) |
| 2 | O(N * M) | O(N * M) | Tối ưu với Mảng 1D (Flat Array) |
| 3 | O(N * M) | O(min(N, M) * max(N, M)) | Tối ưu bộ nhớ (Nếu N, M lớn) |
Bài học kinh nghiệm
- Bài toán có thể chia làm 2 giai đoạn: (1) Xác định thứ tự xoắn ốc, (2) Truy vấn số thứ $k$.
- Việc sử dụng Binary Search trên mảng prefix sum của các ô tốt giúp giảm độ phức tạp thời gian đáng kể so với việc mô phỏng từng bước đi.
- Kỹ thuật xoắn ốc (spiral traversal) sử dụng 4 biến ranh giới (top, bottom, left, right) là phương pháp cài đặt hiệu quả và dễ nhớ nhất.
Lỗi thường gặp
- Lỗi off-by-one: Sai lệch chỉ số mảng (0-based vs 1-based) khi chuyển đổi giữa ma trận đầu vào và số thứ tự xoắn ốc.
- Xử lý sai trường hợp ma trận không vuông: Cần kiểm tra điều kiện
if (r1 < r2)vàif (c1 < c2)khi điền các cạnh cuối cùng để tránh ghi đè. - Quên các ô bị chặn (giá trị 0): Nếu chỉ tính số thứ tự mà không kiểm tra giá trị đầu vào, đáp án sẽ sai.
Bình luận