Hướng dẫn giải của Thắp đèn
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 mô phỏng quá trình di chuyển và bật đèn trong một lưới N×N. Bắt đầu tại ô (1,1) đã sáng, người chơi có thể di chuyển sang các ô kề cạnh (trên, dưới, trái, phải) chỉ nếu ô đó đang sáng. Trại ô (x, y) có thể chứa các công tắc giúp bật đèn các ô khác (c, d). Mục tiêu là tìm số lượng tối đa các ô có thể thắp sáng.
Giải thích chi tiết:
- Ban đầu chỉ ô (1,1) sáng.
- Khi ở một ô sáng, ta có thể bật tất cả các công tắc tại ô đó để thắp sáng các ô đích.
- Ta có thể di chuyển sang các ô lân cận nếu ô đó đang sáng.
- Vấn đề nảy sinh khi một ô được thắp sáng nhưng chưa thể đi đến ngay lập tức (do bị ngăn cách bởi các ô tối). Ta cần tiếp tục tìm kiếm khi có các ô mới được thắp sáng và nằm trong vùng có thể đi đến.
Ví dụ: Có thể thắp sáng ô B từ ô A, nhưng ô B bị ngăn cách bởi ô tối C. Nếu sau đó ta thắp sáng ô C (từ nơi khác), ta có thể đi qua C để đến B.
Các cách tiếp cận
Cách BFS Tái thẩm định (Re-running BFS)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n, m;
vector<pair<int, int>> switches[MAXN][MAXN];
bool light[MAXN][MAXN];
bool visited[MAXN][MAXN];
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, -1, 0, 1};
bool inGrid(int x, int y) {
return x >= 1 && x <= n && y >= 1 && y <= n;
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; ++i) {
int a, b, c, d;
cin >> a >> b >> c >> d;
switches[a][b].push_back({c, d});
}
int res = 0;
// Lặp lại BFS cho đến khi không còn thay đổi
while (true) {
memset(visited, 0, sizeof(visited));
queue<pair<int, int>> q;
q.push({1, 1});
visited[1][1] = true;
// BFS để tìm vùng sáng có thể đi tới
while (!q.empty()) {
auto [x, y] = q.front(); q.pop();
for (int d = 0; d < 4; ++d) {
int nx = x + dx[d], ny = y + dy[d];
if (inGrid(nx, ny) && !visited[nx][ny] && light[nx][ny]) {
visited[nx][ny] = true;
q.push({nx, ny});
}
}
}
// Đếm và bật công tắc
int old_res = res;
res = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (visited[i][j]) {
res++;
// Nếu đang ở ô này, bật tất cả công tắc
for (auto sw : switches[i][j]) {
if (!light[sw.first][sw.second]) {
light[sw.first][sw.second] = true;
}
}
}
}
}
if (res == old_res) break;
}
cout << res << endl;
return 0;
}
- Time Complexity: O(K * N^2 + M), với K là số lần lặp (thường nhỏ)
- Space Complexity: O(N^2 + M)
Cách tiếp cận này chia bài toán thành các giai đoạn. Mỗi giai đoạn:
- Chạy BFS từ (1,1) trên lưới chỉ xét các ô đang sáng và đã được visit. Điều này xác định vùng sáng có thể đi tới hiện tại.
- Duyệt qua tất cả các ô trong vùng này, đếm số ô sáng và bật tất cả các công tắc trong các ô đó.
- Nếu có ô mới được bật sáng, ta lặp lại giai đoạn 1.
Phương pháp này đảm bảo ta luôn thắp sáng các ô mới và kiểm tra xem có thể đi đến chúng không. Nó đơn giản để hiểu nhưng có thể chậm do phải chạy lại BFS nhiều lần.
Cách BFS Đơn kết hợp Kiểm tra Đèn mới
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n, m;
vector<pair<int, int>> switches[MAXN][MAXN];
bool light[MAXN][MAXN];
bool visited[MAXN][MAXN];
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, -1, 0, 1};
bool inGrid(int x, int y) {
return x >= 1 && x <= n && y >= 1 && y <= n;
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; ++i) {
int a, b, c, d;
cin >> a >> b >> c >> d;
switches[a][b].push_back({c, d});
}
light[1][1] = true;
visited[1][1] = true;
queue<pair<int, int>> q;
q.push({1, 1});
int res = 1;
while (!q.empty()) {
auto [x, y] = q.front(); q.pop();
// 1. Bật công tắc tại ô hiện tại
for (auto [nx, ny] : switches[x][y]) {
if (!light[nx][ny]) {
light[nx][ny] = true;
res++;
// 2. Kiểm tra nếu ô mới sáng này nằm kề ô đã thăm
// Nếu có, ta có thể đi vào nó ngay lập tức
for (int d = 0; d < 4; ++d) {
int adjx = nx + dx[d];
int adjy = ny + dy[d];
if (inGrid(adjx, adjy) && visited[adjx][adjy]) {
visited[nx][ny] = true;
q.push({nx, ny});
break; // Đã vào hàng đợi, không cần check tiếp
}
}
}
}
// 3. Thêm các ô lân cận chưa thăm vào hàng đợi
for (int d = 0; d < 4; ++d) {
int nx = x + dx[d], ny = y + dy[d];
if (inGrid(nx, ny) && !visited[nx][ny] && light[nx][ny]) {
visited[nx][ny] = true;
q.push({nx, ny});
}
}
}
cout << res << endl;
return 0;
}
- Time Complexity: O(N^2 + M)
- Space Complexity: O(N^2 + M)
Đây là thuật toán tối ưu nhất. Ta chỉ cần chạy BFS một lần duy nhất.
Logic:
- Hàng đợi
qlưu trữ các ô đã đến được và đang chờ xử lý. - Khi xử lý ô
(x, y):- Bật tất cả công tắc. Nếu có ô
(nx, ny)mới sáng:- Ngay lập tức kiểm tra xem
(nx, ny)có kề với bất kỳ ô nào đã visit chưa. Nếu có, ta có thể đi vào nó ngay, đánh dấu visit và đẩy vào hàng đợi.
- Ngay lập tức kiểm tra xem
- Duyệt các ô lân cận của
(x, y). Nếu ô lân cận sáng và chưa visit, thì visit và đẩy vào hàng đợi.
- Bật tất cả công tắc. Nếu có ô
Điểm mấu chốt: Việc kiểm tra điều kiện "kề ô đã visit" khi bật công tắc giúp ta không cần phải chạy BFS lại từ đầu. Nếu một ô mới sáng mà không kề ô nào đã visit, nó sẽ được xử lý sau này, khi mà đường đi được mở thông qua các ô khác.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(K * N^2 + M), với K là số lần lặp (thường nhỏ) | O(N^2 + M) | BFS Tái thẩm định (Re-running BFS) |
| 2 | O(N^2 + M) | O(N^2 + M) | BFS Đơn kết hợp Kiểm tra Đèn mới |
Bài học kinh nghiệm
- Vấn đề có thể mô hình hóa như việc tìm vùng liên thông (Connected Component) trong đồ thị các ô, nhưng các cạnh (đường đi) chỉ tồn tại nếu ô đích đang sáng.
- Thuật toán BFS chỉ cần chạy 1 lần nếu ta xử lý việc "thêm ô mới vào hàng đợi" ngay tại thời điểm ô đó được thắp sáng và có đường vào.
- Một ô được coi là "có thể đi vào" nếu nó sáng và có ít nhất một ô lân cận đã được duyệt qua (visited).
Lỗi thường gặp
- Quên kiểm tra điều kiện
light[nx][ny]khi duyệt các ô lân cận trong BFS, dẫn đến việc đi vào ô tối. - Trong cách tiếp cận BFS 1 lần, nếu chỉ kiểm tra các ô lân cận của ô hiện tại (
x, y) khi bật công tắc, ta có thể bỏ lỡ việc vào ô mới nếu ô mới đó kề một ô đã được xử lý trước đó nhưng không kề ô hiện tại. Do đó, phải check kề tất cả các ô đã visit (hoặc check khi visit ô mới). - Lưu ý đánh dấu
visitedđúng lúc để tránh vào hàng đợi 2 lần.
Bình luận