# BFS(廣度優先搜尋) ## 複習:實作queue&pair ```cpp= #include <iostream> #include <queue> using namespace std; #define F first //定義F為first S為second #define S second //define可以使程式簡短 int main(){ queue<pair<int, int>> q; for(int i = 1; i <= 5; i+=2){ q.emplace(i, i+1); //相當於 q.push({i, i+1}); } while(!q.empty()){ auto tmp = q.front(); //自動設定資料型態 (此為pair<int, int>) cout << tmp.F << ' ' << tmp.S << '\n'; //輸出first跟second /* 也可寫成: auto [a, b] = q.front(); //將a = q.front().first, b = q.front().second cout << a << ' ' << b << '\n'; */ q.pop(); } return 0; } ``` :::spoiler 輸出 ``` 1 2 3 4 5 6 ``` ::: ## BFS概念說明 BFS是一種演算法(解題的方法),精隨為藉由慢慢擴展的方式,來找到起點對於每個點的最短路徑 *也有一種說法叫淹水,慢慢地淹過每一格* 實際運作過程如下圖所例 ||(source: trt4497)|| ![image](https://hackmd.io/_uploads/H1PGptXxWx.jpg) ## BFS實作 實作範例: #### 輸入 第一行輸入一正整數 $N \leq 20$表示$N * N$的地圖 接下來有$N$行,每一行有$N$個數字$w_1, w_2, ... , w_n \in (0, 1)$ $w_i = 0$ 代表該格不能通過 #### 輸出 輸出一個正整數 $s$ 代表從左上$(0, 0)$走到右下$(n-1, n-1)$的最短步數 ### 思路 我們可以明顯地觀察到需要使用BFS來計算最短路徑 而BFS的實作順序為 新增起點 $\rightarrow$ 遍歷地圖```mp```$*_{(1)}$ $\rightarrow$ 輸出右下角```cnt```數值即為最短路徑 --- *$_{(1)}$: 1. 取得現在位置座標 $(y,\ x)$ 並從```queue```中移除 2. 往四個方向擴展 $\rightarrow$ 另存新座標 $(ny,\ nx)$ 3. 判斷是否超出地圖範圍 ($0 \leq x,\ y < 20$)、```used```是否走過、```mp[ny][nx]```是否不能通過 4. ```used```陣列標記走過```= 1```、路徑長```cnt```陣列```+ 1```、合法座標放入```queue```中 --- ### 版本1 (四個if判斷) :::spoiler Code ```cpp= #include <iostream> #include <queue> using namespace std; int mp[50][50]; int used[50][50] = {0}, cnt[50][50] = {0}; //all = 0 int main(){ int n; cin >> n; for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ cin >> mp[i][j]; } } queue<pair<int, int>> q; q.emplace(0, 0); //start while(!q.empty()){ auto [y, x] = q.front(); q.pop(); for(int i = 0; i < 4; i++){ //4 dir if(i == 0){//up int ny = y - 1, nx = x; if(ny < 0 || ny >= n || nx < 0 || nx >= n) continue; //out if(used[ny][nx]) continue; //used before if(mp[ny][nx] == 0) continue; //cant pass used[ny][nx] = 1; //used cnt[ny][nx] = cnt[y][x] + 1; q.emplace(ny, nx); }else if(i == 1){//down int ny = y + 1, nx = x; if(ny < 0 || ny >= n || nx < 0 || nx >= n) continue; //out if(used[ny][nx]) continue; //used before if(mp[ny][nx] == 0) continue; //cant pass used[ny][nx] = 1; //used cnt[ny][nx] = cnt[y][x] + 1; q.emplace(ny, nx); }else if(i == 2){//left int ny = y, nx = x - 1; if(ny < 0 || ny >= n || nx < 0 || nx >= n) continue; //out if(used[ny][nx]) continue; //used before if(mp[ny][nx] == 0) continue; //cant pass used[ny][nx] = 1; //used cnt[ny][nx] = cnt[y][x] + 1; q.emplace(ny, nx); }else if(i == 3){//right int ny = y, nx = x + 1; if(ny < 0 || ny >= n || nx < 0 || nx >= n) continue; //out if(used[ny][nx]) continue; //used before if(mp[ny][nx] == 0) continue; //cant pass used[ny][nx] = 1; //used cnt[ny][nx] = cnt[y][x] + 1; q.emplace(ny, nx); } } if(cnt[n-1][n-1] != 0){ cout << cnt[n-1][n-1]; break; } } } ``` ::: ### 版本2 ($dx, dy$ 陣列) 根據上個版本,我們可以發現一個問題:~~程式碼又臭又長~~,且易讀性差 所以我們可以針對四個方向的遍歷去改良 新增一個 $\Delta x$ 陣列與一個 $\Delta y$ 陣列,分別儲存改變方向時對於座標的加數(可能為負) 經過此操作後,我們只需要每次呼叫 $\Delta x$、$\Delta y$ 中的每一項資料,與 $x,\ y$ 做加法運算即可 :::spoiler Code ```cpp= #include <iostream> #include <queue> using namespace std; int mp[50][50]; int used[50][50] = {0}, cnt[50][50] = {0}; //all = 0 int dy[4] = {-1, 1, 0, 0}; int dx[4] = {0, 0, -1, 1}; //up down left right int main(){ int n; cin >> n; for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ cin >> mp[i][j]; } } queue<pair<int, int>> q; q.emplace(0, 0); while(!q.empty()){ auto [y, x] = q.front(); q.pop(); for(int i = 0; i < 4; i++){ int ny = y + dy[i], nx = x + dx[i]; //change dir if(ny < 0 || ny >= n || nx < 0 || nx >= n) continue; if(used[ny][nx]) continue; if(mp[ny][nx] == 0) continue; used[ny][nx] = 1; cnt[ny][nx] = cnt[y][x] + 1; q.emplace(ny, nx); } if(cnt[n-1][n-1] != 0){ cout << cnt[n-1][n-1]; break; } } } ``` ::: ## 練習題 ### [Zerojudge- a982 迷宮問題#1](https://zerojudge.tw/ShowProblem?problemid=a982) :::spoiler 起點? 終點? 注意題目的座標表示方式 ::: :::spoiler 到不了終點 圖跑完程式會接到哪? 輸出就放在那 前面如果到的了要直接終止程式 ::: :::spoiler AC Code ```cpp= #include <iostream> #include <queue> using namespace std; #define pii pair<int, int> int dx[4] = {0, 0, 1, -1}; int dy[4] = {1, -1, 0, 0}; int main(){ int n; cin >> n; char mp[105][105]; int used[105][105] = {0}, cnt[105][105] = {0}; for(int i = 1; i <= n; i++){ for(int j = 1; j <= n; j++){ cin >> mp[i][j]; } } queue<pii> q; q.emplace(2, 2); cnt[2][2] = 1; used[2][2] = 1; while(!q.empty()){ auto [y, x] = q.front(); q.pop(); if(y == n-1 && x == n-1){ cout << cnt[y][x]; return 0; } for(int i = 0; i < 4; i++){ for(int j = 0; j < 4; j++){ int ny = y + dy[i], nx = x + dx[i]; if(ny < 1 || ny > n || nx < 1 || nx > n) continue; if(used[ny][nx]) continue; if(mp[ny][nx] == '#') continue; used[ny][nx] = 1; cnt[ny][nx] = cnt[y][x] + 1; q.emplace(ny, nx); } } } cout << "No solution!"; return 0; } ``` ::: ### [Zerojudge- d406 倒水時間](https://zerojudge.tw/ShowProblem?problemid=d406) :::spoiler 往上流 注意處理題目輸入,處理方向的時候排除```上``` ::: :::spoiler 起點位置 保證到水位置只會在 $y = 0$ 的時候 且只有一個$1$ ::: :::spoiler 若干筆測資 ```cpp while(cin >> s) ``` 注意輸出格式 ::: :::spoiler AC Code ```cpp= #include <iostream> #include <queue> using namespace std; #define pii pair<int, int> int dy[4] = {-1, 1, 0, 0}; int dx[4] = {0, 0, -1, 1}; int main(){ int s, Case = 0; while(cin >> s){ Case++; int n, m; cin >> n >> m; int mp[105][105]; int used[105][105] = {0}, cnt[105][105] = {0}; queue<pii> q; bool flag = false; for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ cin >> mp[i][j]; if(!flag && mp[i][j] == 1){ q.emplace(i, j); cnt[i][j] = 1; used[i][j] = 1; flag = true; } } } while(!q.empty()){ auto [y, x] = q.front(); q.pop(); for(int i = 0; i < 4; i++){ if(i == 0 && s == 2) continue; //skip up(s = 2) int ny = y + dy[i], nx = x + dx[i]; if(ny < 0 || ny >= n || nx < 0 || nx >= m) continue; if(used[ny][nx]) continue; if(mp[ny][nx] == 0) continue; used[ny][nx] = 1; cnt[ny][nx] = cnt[y][x] + 1; q.emplace(ny, nx); } } cout << "Case " << Case << ":\n"; for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ cout << cnt[i][j] << ' '; } cout << "\n"; } } } ``` ::: ### [Zerojudge- d453 最短距離](https://zerojudge.tw/ShowProblem?problemid=d453) :::spoiler 輸入方式 用字串或字元存取(無空格) ::: :::spoiler 起點終點 不一定固定 需要調整 直接判斷是否到終點並輸出```cnt``` ::: :::spoiler AC Code ```cpp= #include <iostream> #include <queue> using namespace std; #define pii pair<int, int> #define F first #define S second int dx[4] = {-1, 1, 0, 0}; int dy[4] = {0, 0, 1, -1}; int main(){ int N; cin >> N; while(N--){ int n, m, y1, x1, y2, x2; cin >> n >> m >> y1 >> x1 >> y2 >> x2; pii bg = make_pair(y1-1, x1-1), ed = make_pair(y2-1, x2-1); queue<pii> q; q.push(bg); char mp[105][105]; int used[105][105] = {0}, cnt[105][105] = {0}; used[bg.F][bg.S] = 1; cnt[bg.F][bg.S] = 1; for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ cin >> mp[i][j]; } } int flag = 0; while(!q.empty()){ auto [y, x] = q.front(); q.pop(); if(y == ed.F && x == ed.S){ cout << cnt[y][x] << "\n"; flag = 1; break; } for(int i = 0; i < 4; i++){ int ny = y + dy[i], nx = x + dx[i]; if(ny < 0 || ny >= n || nx < 0 || nx >= m) continue; if(used[ny][nx]) continue; if(mp[ny][nx] == '1') continue; used[ny][nx] = 1; cnt[ny][nx] = cnt[y][x] + 1; q.emplace(ny, nx); } } if(!flag) cout << "0\n"; } return 0; } ``` ::: ### [CSES- Counting Rooms](https://cses.fi/problemset/task/1192) :::spoiler 題譯 有一個$N * M$的建築地圖,你需要計算其中有幾個房間,你可以往上下左右行動 *#輸入* 第一行有兩個正整數$N \ M(1 \leq N, M \leq 1000)$ 表示圖的長度和寬度 接下來有$N$行,每行$M$個字元$c_1, c_2, ...,c_m \in (.,\#)$,$.$ 為地板,$\#$ 為牆壁 *#輸出* 輸出一個正整數表示房間的數量 ::: :::spoiler AC Code ```cpp= #include <iostream> #include <queue> using namespace std; #define F first #define S second char mp[1005][1005]; bool used[1005][1005] = {0}; int n, m, ans = 0; int dx[4] = {0, 0, 1, -1}; int dy[4] = {1, -1, 0, 0}; int main(){ cin.tie(0) -> sync_with_stdio(0); cin >> n >> m; vector<pair<int, int>> v; for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ cin >> mp[i][j]; if(mp[i][j] == '.'){ v.emplace_back(i, j); } } } for(auto i : v){ if(!used[i.F][i.S]){ pair<int, int> tmp = i; ans++; queue<pair<int, int>> q; q.emplace(tmp); while(!q.empty()){ auto [nx, ny] = q.front(); q.pop(); for(int i = 0; i < 4; i++){ int x = nx + dx[i], y = ny + dy[i]; if(x < 0 || x >= n || y < 0 || y >= m) continue; if(used[x][y]) continue; used[x][y] = true; if(mp[x][y] == '.') q.emplace(x, y); } } } } cout << ans; return 0; } ``` ::: ### 變體題:[CSES- Labyrinth](https://cses.fi/problemset/task/1193) ~~不想弄題譯~~ :::spoiler 起點終點 處理輸入時同時判斷 ::: :::spoiler 回溯? 找到一條最佳路徑後 要怎麼儲存行走的方向? 答案是再創一個陣列```parents```儲存來時的座標 ::: :::spoiler AC code ```cpp= #include <bits/stdc++.h> using namespace std; int dx[4] = {-1, 0, 1, 0}; int dy[4] = {0, -1, 0, 1}; pair<int, int> parents[1005][1005]; int main(){ cin.tie(0) -> sync_with_stdio(0); char mp[1005][1005]; bool used[1005][1005]; memset(used, 0, sizeof(used)); memset(parents, 0, sizeof(parents)); queue<pair<int, int>> q; int n, m; cin >> n >> m; for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ cin >> mp[i][j]; if(mp[i][j] == 'A') {q.emplace(i, j); used[i][j] = 1; parents[i][j] = {-1, -1};} } } while(!q.empty()){ auto [x, y] = q.front(); q.pop(); for(int i = 0; i < 4; i++){ int nx = x + dx[i], ny = y + dy[i]; if(nx < 0 || nx >= n || ny < 0 || ny >= m) continue; if(used[nx][ny] || mp[nx][ny] == '#') continue; used[nx][ny] = 1; parents[nx][ny] = {x, y}; //cout << nx << " " << ny << "\n"; q.emplace(nx, ny); if(mp[nx][ny] == 'B'){ int cnt = 0; cout << "YES\n"; string ans = ""; while(true){ auto [tmpx, tmpy] = parents[nx][ny]; if(tmpx == -1) break; cnt++; if(tmpx < nx) ans += "D"; else if(tmpx > nx) ans += "U"; else if(tmpy < ny) ans += "R"; else ans += "L"; nx = tmpx, ny = tmpy; } reverse(ans.begin(), ans.end()); cout << cnt << "\n" << ans; return 0; } } } cout << "NO"; } ``` :::