Tác giả:
Reviewer:
Lỡ một ngày bạn ngồi trong phòng thi quốc gia, bạn gặp một đề bài như sau:
Cho một đồ thị đỉnh cạnh (), cạnh thứ có trọng số là (). Tìm đường đi ngắn nhất từ đến ().
Bạn hì hục cài Dijkstra, tuy nhiên code lại chạy quá chậm để qua được giới hạn thời gian. Lúc ấy bạn mới nhớ lại blog về 0-1 BFS trên VNOI wiki, thứ mà bạn sắp đọc bây giờ.
0-1 BFS có thể thực hiện tìm kiếm đường đi ngắn nhất từ một nguồn (Single source shortest path) trong độ phức tạp tuyến tính, với điều kiện là cạnh có trọng số bằng 0 hoặc .
0-1 BFS có rất nhiều điểm tương đồng với BFS và Dijkstra. Thay vì sử dụng hàng đợi (queue) để chứa các đỉnh sẽ được duyệt, ta sử dụng hàng đợi hai đầu (double-ended queue, thường gọi tắt là deque) để chứa các đỉnh sẽ được duyệt.
Bước 1: Khởi tạo
Bước 2: Thực hiện đến khi deque rỗng
Tính đúng đắn của 0-1 BFS có thể chứng minh như thuật toán Dijkstra. Tuy nhiên Dijkstra sử dụng cấu trúc dữ liệu hàng đợi ưu tiên để tìm đỉnh có nhỏ nhất trong các đỉnh với độ phức tạp mỗi thao tác, còn 0-1 BFS tìm đỉnh này trong .
Trong hàng đợi hai đầu, gọi là đỉnh nằm ở đầu hàng đợi, các đỉnh trong hàng đợi có tăng dần từ đầu đến cuối hàng đợi và chỉ có thể bằng hoặc :
Ta sẽ chứng minh điều này bằng phương pháp quy nạp:
Từ đây ta cũng chứng minh được một đỉnh
Cấu trúc dữ liệu:
MAXN
: Độ lớn của mảngD[]
: Tương tự trênvis[]
: Mảng đánh dấu đỉnh đã được duyệtg[]
: Danh sách kề của các đỉnhdq
: Chứa các đỉnh sẽ được duyệtconst int INF = 1e9;
const int MAXN = 2e6+5;
int n; // Số đỉnh của đồ thị
int D[MAXN], vis[MAXN];
vector<pair<int, int>> g[MAXN]; // first là đầu mút của cạnh, second là trọng số của cạnh
deque<int> dq;
void BFS_01(int s){
for(int i = 1; i <= n; i++){
D[i] = INF; // Khởi tạo
vis[i] = 0;
}
D[s] = 0;
dq.push_front(s);
while(!dq.empty()){
int u = dq.front();
dq.pop_front();
if(vis[u])continue;
vis[u] = 1; // Đánh dấu
for(auto v: g[u]){
if(D[v.first] > D[u] + v.second){ //
D[v.first] = D[u] + v.second; // Duyệt và sử lý các cạnh của u
if(v.second == 1)dq.push_back(v.first); //
else dq.push_front(v.first);
}
}
}
}
Có rất ít bài toán yêu cầu bắt buộc phải sử dụng 0-1 BFS. Rất khó để ép thời gian sao cho Dijkstra chạy quá thời gian mà 0-1 BFS lại AC. Vì vậy 0-1 BFS thường được sử dụng trong các bài khó nhưng đưa được về đồ thị có rất nhiều đỉnh hoặc cạnh.
Ta có thể giải được bài toán tìm đường đi ngắn nhất từ một nguồn trong
Link nộp: Codeforces - Labyrinth
Đề bài
Bạn đang chơi một trò chơi điện tử. Một level nào đó đặt bạn vào một ô nào đó trong một mê cung
Do bàn phím bạn sắp hỏng nên bạn chỉ được di chuyển sang trái không quá
Biết rằng bạn đang bắt đầu ở ô
Giới hạn:
Lời giải
Đây là một bài cơ bản để tập cài 0-1 BFS.
Giả sử ta muốn kiểm tra xem có đến được một ô
Gọi số lần đi sang trái là
Ta coi mê cung là một đồ thị, các ô vuông chung cạnh sẽ được nối với nhau trên đồ thị, cạnh nối có trọng số là
Độ phức tạp thời gian:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e3+5;
const int INF = 1e9+5;
int n, m, r, c, x, y;
char board[MAXN][MAXN];
int d[MAXN][MAXN];
int vis[MAXN][MAXN];
const pair<int, int> mv[4] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
bool in_bound(int a, int b){
return 1 <= a && a <= n && 1 <= b && b <= m && board[a][b] == '.';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> r >> c >> x >> y;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
cin >> board[i][j];
d[i][j] = INF;
}
}
d[r][c] = 0;
deque<pair<int, int>> dq;
dq.push_back({r, c});
while(!dq.empty()){
pair<int, int> u = dq.front();
dq.pop_front();
if(vis[u.first][u.second])continue;
vis[u.first][u.second] = 1;
for(pair<int, int> v: mv){
int nwx = v.first + u.first, nwy = v.second + u.second;
int w;
if(v == make_pair(0, -1))w = 1;
else w = 0;
if(in_bound(nwx, nwy) && d[nwx][nwy] > d[u.first][u.second] + w){
d[nwx][nwy] = d[u.first][u.second] + w;
if(w == 0)dq.push_front({nwx, nwy});
else dq.push_back({nwx, nwy});
}
}
}
int ans = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
int l = d[i][j], r = l + j - c;
if(l <= x && r <= y)ans++;
}
}
cout << ans;
}
Link nộp: Codeforces - Chamber of Secrets
Đề bài
Cho căn phòng có thể chia thành bảng lưới gồm
Có một tia sáng chiếu từ cạnh bên trái của ô
Giới hạn:
Lời giải
Để có tia sáng tới được cánh cửa, tương đương với việc phải tồn tại một đường đi bắt đầu bằng một chiếc gương ở hàng
Ta có nhận xét: Nếu tồn tại
Đến đây, ta có lời giải như sau:
Đáp án của bài toán là đường đi ngắn nhất từ ô
Độ phức tạp thời gian:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e3+5;
const int INF = 1e9+5;
vector<pair<int, int>> g[MAXN*MAXN*2];
int d[MAXN*MAXN*2], vis[MAXN*MAXN*2];
int encode(int type, int i, int j) { // mã hóa ô (i, j) của đồ thị type thành 1 số
return (type-1)*MAXN*MAXN + i*MAXN + j;
}
void add_edge(int a, int b, int w) {
g[a].push_back({b, w});
g[b].push_back({a, w});
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
char x;
cin >> x;
d[encode(1, i, j)] = INF;
d[encode(2, i, j)] = INF;
if(x == '#')add_edge(encode(1, i, j), encode(2, i, j), 1);
if(j > 1)add_edge(encode(1, i, j-1), encode(1, i, j), 0);
if(i > 1)add_edge(encode(2, i-1, j), encode(2, i, j), 0);
}
}
deque<int> dq;
d[encode(1, 1, 1)] = 0;
dq.push_front(encode(1, 1, 1));
while(!dq.empty()){
auto u = dq.front();
if(u == encode(1, n, m)){
cout << d[u];
return 0;
}
dq.pop_front();
if(vis[u])continue;;
vis[u] = 1;
for(pair<int, int> v: g[u]){
if(d[v.first] > d[u] + v.second){
d[v.first] = d[u] + v.second;
if(!v.second)dq.push_front(v.first);
else dq.push_back(v.first);
}
}
}
cout << "-1";
}