# 演算法課程題解 - BFS # TOJ 432 ## 題目 https://toj.tfcis.org/oj/pro/432/ 有一個 $N \times M$ 的棋盤,在這個棋盤上有 $F$ 個著火的點 $(x_i, y_i)$ 以及一個門 $(u, v)$ 現在有個人在 $(x_{me}, y_{me})$ 的位置,每次可以上下左右選擇一個移動一格,請問這個人是否能在不碰到火的情況下走到門的位置 ## 想法 By Koios 我們可以嘗試從人所在的位置開始,每次走一步,每一個方向都走過,直到找到門或是把全部可行的格子走過,那麼就會知道答案了 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int n, m, f, arr[1005][1005], que[1000005][2]; int start_x, start_y, end_x, end_y, cur_x, cur_y, nx, ny; bool vis[1005][1005], ans=false; int dx[4] = {0, 1, 0, -1}; int dy[4] = {1, 0, -1, 0}; void BFS(){ que[0][0] = start_x; que[0][1] = start_y; vis[start_x][start_y] = true; for(int i=0, j=1 ; i<j ; i++){ cur_x = que[i][0]; cur_y = que[i][1]; // 找到門了 if(cur_x == end_x && cur_y == end_y){ ans=true; break; } // 四個方向都走過 for(int k=0 ; k<4 ; k++){ nx = cur_x + dx[k]; ny = cur_y + dy[k]; // 把超出棋盤的移除 if(nx > n || nx <= 0 || ny <= 0 || ny > m) continue; if(!vis[nx][ny] && arr[nx][ny] != 1){ vis[nx][ny] = true; que[j][0] = nx; que[j][1] = ny; j++; } } } } int main(){ cin>>n>>m; for(int i=1 ; i<=n ; i++){ for(int j=1 ; j<=m ; j++){ arr[i][j] = 0; vis[i][j] = false; } } cin>>start_x>>start_y>>end_x>>end_y; cin>>f; for(int i=0, x, y ; i<f ; i++){ cin>>x>>y; arr[x][y] = 1; } BFS(); if(ans){ cout<<"Cool!\n"; } else{ cout<<"Harakiri!\n"; } return 0; } ``` ### 時間複雜度分析 輸入時間複雜度為 $O(nm + f)$ BFS 時間複雜度為 狀態數 $\times$ 操作數量 $= (nm) \times 4$,計為 $O(nm)$ 總時間複雜度為 $O(2nm + f)$ # UVa 439 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?439 在一個 $8 \times 8$ 的西洋棋棋盤上有一個騎士,給定騎士當前的位置,求到達某個指定的位置所需的最少步數,保證必定有解 因為是騎士,所以當然要用騎士的走法 ## 想法 By Koios 因為要找的是最少的步數,所以可以想到用 BFS 來搜尋 每次將每種走法都走一步,直到走到詢問的點 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int que[1010][2], dis[10][10], ans; int start_x, start_y, end_x, end_y, cur_x, cur_y, nx, ny; int dx[] = {-2, -2, -1, -1, 1, 1, 2, 2}; int dy[] = {-1, 1, -2, 2, -2, 2, -1, 1}; bool vis[10][10]; char Ax, Ay, Bx, By; int BFS(){ que[0][0] = start_x; que[0][1] = start_y; vis[start_x][start_y] = true; for(int i=0, j=1 ; i<j ; i++){ cur_x = que[i][0]; cur_y = que[i][1]; if(cur_x == end_x && cur_y == end_y){ return dis[end_x][end_y]; } // 對於 8 個走法走一步 for(int k=0 ; k<8 ; k++){ nx = cur_x + dx[k]; ny = cur_y + dy[k]; // 移除超出棋盤的點 if(nx <= 0 || nx > 8 || ny <= 0 || ny > 8) continue; if(!vis[nx][ny]){ vis[nx][ny] = true; dis[nx][ny] = dis[cur_x][cur_y]+1; que[j][0] = nx; que[j][1] = ny; j++; } } } return -1; } int main(){ while(cin>>Ay>>Ax>>By>>Bx){ // 將棋盤座標轉換成平面座標 start_x = Ax-'0'; start_y = Ay-'a'+1; end_x = Bx-'0'; end_y = By-'a'+1; for(int i=0 ; i<10 ; i++){ for(int j=0 ; j<10 ; j++){ dis[i][j] = 0; vis[i][j] = false; } } ans = BFS(); cout<<"To get from "<<Ay<<Ax<<" to "<<By<<Bx<<" takes "<<ans<<" knight moves.\n"; } return 0; } ``` ### 時間複雜度分析 令 $n = 8$ 每筆測資輸入時間複雜度為 $O(1)$ 每筆測資 BFS 時間複雜度為 狀態數 $\times$ 操作數量 $= (n^2) \times 8$,計為 $O(n^2)$ 每筆測資總時間複雜度為 $O(n^2)$ # UVa 532 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?532 有一個三維的地牢,包含了 `#` `.` `S` `E` ,分別表示 障礙物、空格、起點、終點 每次移動可以選擇 前、後、左、右、上、下 六種走法 求從起點走到終點的最短步數,如果無解則輸出 `Trapped!` ## 想法 By Koios 因為要找的是最小步數,所以可以想到用 BFS 來搜尋,只是這次變成三維而已 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int L, R, C, ans; int dx[6] = {1, -1, 0, 0, 0, 0}; int dy[6] = {0, 0, 1, -1, 0, 0}; int dz[6] = {0, 0, 0, 0, 1, -1}; char arr[35][35][35]; bool vis[35][35][35]; struct dot{ int x, y, z; int dis; // 建立一個 dot 元素,並且給定 x y z dot(int X, int Y, int Z):x{X}, y{Y}, z{Z}, dis{0} {} // 建立一個空 dot dot(): x{-1}, y{-1}, z{-1}, dis{0} {} }; dot start, End, cur, nxt; int BFS(){ dot que[50000]; que[0] = start; vis[start.x][start.y][start.z] = true; for(int i=0, j=1 ; i<j ; i++){ cur = que[i]; if(cur.x == End.x && cur.y == End.y && cur.z == End.z){ return cur.dis; } for(int k=0 ; k<6 ; k++){ nxt = cur; nxt.x += dx[k]; nxt.y += dy[k]; nxt.z += dz[k]; // 超出界線 if(nxt.x < 0 || nxt.x >= L || nxt.y < 0 || nxt.y >= R || nxt.z < 0 || nxt.z >= C) continue; if(!vis[nxt.x][nxt.y][nxt.z] && arr[nxt.x][nxt.y][nxt.z] != '#'){ vis[nxt.x][nxt.y][nxt.z] = true; nxt.dis = cur.dis + 1; que[j] = nxt; j++; } } } return -1; } int main(){ while(cin>>L>>R>>C && (L!=0 && R!=0 && C!=0)){ for(int i=0 ; i<L ; i++){ for(int j=0 ; j<R ; j++){ for(int k=0 ; k<C ; k++){ cin>>arr[i][j][k]; vis[i][j][k] = false; if(arr[i][j][k] == 'S'){ start = dot(i, j, k); } else if(arr[i][j][k] == 'E'){ End = dot(i, j, k); } } } } ans = BFS(); if(ans == -1){ cout<<"Trapped!\n"; } else{ cout<<"Escaped in "<<ans<<" minute(s).\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(LRC)$ 每筆測資 BFS 時間複雜度為 狀態數 $\times$ 操作數量 $= (LRC) \times 6$,計為 $O(LRC)$ 每筆測資 總時間複雜度為 $O(LRC)$ # UVa 10102 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10102 有一個 $M \times M$ 的棋盤,棋盤上有許多 `1` `2` `3` 請問所有以 `1` 為起點 `3` 為終點的最短路徑當中的最長長度為何,並且保證必定有解 ## 想法 By Koios 因為要找最短路徑,所以可以想到用 BFS 來搜尋 不過因為要求的是全部的路徑中的最長,所以可以每次選擇其中一個 `1` 當作起點計算最短路徑,然後取出最大值 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int m, R, ans, res, que[10005][2], dis[105][105], start[10005][2]; int cur_x, cur_y, nx, ny; int dx[4] = {0, 1, 0, -1}; int dy[4] = {1, 0, -1, 0}; char arr[105][105]; bool vis[105][105]; int BFS(int n){ que[0][0] = start[n][0]; que[0][1] = start[n][1]; vis[start[n][0]][start[n][1]] = true; dis[start[n][0]][start[n][1]] = 0; for(int i=0, j=1 ; i<j ; i++){ cur_x = que[i][0]; cur_y = que[i][1]; if(arr[cur_x][cur_y] == '3'){ return dis[cur_x][cur_y]; } for(int k=0 ; k<4 ; k++){ nx = cur_x + dx[k]; ny = cur_y + dy[k]; if(nx < 0 || nx >= m || ny < 0 || ny >= m) continue; if(!vis[nx][ny]){ vis[nx][ny] = true; dis[nx][ny] = dis[cur_x][cur_y] + 1; que[j][0] = nx; que[j][1] = ny; j++; } } } return 0; } int main(){ while(cin>>m){ ans=res=0; R=0; for(int i=0 ; i<m ; i++){ for(int j=0 ; j<m ; j++){ cin>>arr[i][j]; if(arr[i][j] == '1'){ start[R][0] = i; start[R][1] = j; R++; } } } for(int i=0 ; i<R ; i++){ for(int j=0 ; j<105 ; j++){ for(int k=0 ; k<105 ; k++){ vis[j][k] = false; } } res = BFS(i); ans = max(ans, res); } cout<<ans<<"\n"; } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(m^2)$ 每筆測資 BFS 時間複雜度為 狀態數 $\times$ 操作數量 $= m^2 \times 4$,每次要做 $R$ 次操作,計為 $O(Rm^2)$ 每筆測資總時間複雜度為 $O(Rm^2)$ # UVa 10959 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10959 我們可以把 Don Giovanni 當作是所有人的起點 所有跟 Don Giovanni 跳過舞的人都距離 Don Giovanni $1$ 所有跟距離 Don Giovanni $n$ 的人跳過舞,則他的距離會變成 $n+1$ 現在告訴你有 $P$ 個人,並且有 $D$ 對跳舞的人,求每個人到 Don Giovanni 的距離 ## 想法 By Koios 可以把所有跳舞的人之間連上一條邊 然後從 Don Giovanni $(0)$ 開始,把所有邊都走過一遍 因為要的是距離,可以用 BFS ,如此一來第一次走到的距離就是 $1$,第二次距離就是 $2$,以此類推 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int t, P, D, que[1005], dis[1005], cur; bool edge[1005][1005]; void BFS(){ que[0] = 0; dis[0] = 0; for(int i=0, j=1 ; i<j ; i++){ cur = que[i]; for(int nxt=0 ; nxt<P ; nxt++){ if(edge[cur][nxt] && dis[nxt] > dis[cur] + 1){ dis[nxt] = dis[cur] + 1; que[j] = nxt; j++; } } } } int main(){ cin>>t; for(int i=0 ; i<t ; i++){ if(i) cout<<"\n"; for(int j=0 ; j<1005 ; j++){ // 先假設每個點愈離都是一個極大值 dis[j] = 1e9+10; for(int k=0 ; k<1005 ; k++){ edge[j][k] = false; } } cin>>P>>D; for(int j=0, a, b ; j<D ; j++){ cin>>a>>b; edge[a][b] = edge[b][a] = true; } BFS(); for(int i=1 ; i<P ; i++){ cout<<dis[i]<<"\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(D)$ 每筆測資 BFS 時間複雜度為 $O(P)$ 總時間複雜度為 $O(t(D + P))$ # UVa 11352 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?11352 在一個 $M \times N$ 的西洋棋棋盤上有一些騎士以及兩個王國 $A, B$ 現在有一個國王要從王國 $A$ 走到王國 $B$,求最短路徑,若無解則輸出 `King Peter, you can’t go now!` ## 想法 By Koios 因為要求的是最短路徑,所以可以想到用 BFS 來求解 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int t, m, n, ans, que[100005][2], dis[105][105]; // 騎士的走法 int H_dx[8] = {-2, -2, -1, -1, 1, 1, 2, 2}; int H_dy[8] = {-1, 1, -2, 2, -2, 2, -1, 1}; // 國王的走法 int K_dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1}; int K_dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1}; int start_x, start_y, end_x, end_y, cur_x, cur_y, nx, ny; char arr[105][105], tmp; bool vis[105][105]; int BFS(){ que[0][0] = start_x; que[0][1] = start_y; dis[start_x][start_y] = 0; vis[start_x][start_y] = true; for(int i=0, j=1 ; i<j ; i++){ cur_x = que[i][0]; cur_y = que[i][1]; if(cur_x == end_x && cur_y == end_y){ return dis[end_x][end_y]; } for(int k=0 ; k<8 ; k++){ nx = cur_x + K_dx[k]; ny = cur_y + K_dy[k]; if(nx < 0 || nx >=m || ny < 0 || ny >= n) continue; if(!vis[nx][ny] && arr[nx][ny] != '#'){ vis[nx][ny] = true; dis[nx][ny] = dis[cur_x][cur_y] + 1; que[j][0] = nx; que[j][1] = ny; j++; } } } return -1; } int main(){ cin>>t; while(t--){ cin>>m>>n; // 初始化 for(int i=0 ; i<m ; i++){ for(int j=0 ; j<n ; j++){ arr[i][j] = '.'; vis[i][j] = false; } } for(int i=0 ; i<m ; i++){ for(int j=0 ; j<n ; j++){ cin>>tmp; if(tmp == 'A'){ arr[i][j] = tmp; start_x = i; start_y = j; } else if(tmp == 'B'){ arr[i][j] = tmp; end_x = i; end_y = j; } else if(tmp == 'Z'){ arr[i][j] = '#'; for(int k=0 ; k<8 ; k++){ nx = i + H_dx[k]; ny = j + H_dy[k]; if(nx >= 0 && nx < m && ny >= 0 && ny < n && arr[nx][ny] == '.'){ arr[nx][ny] = '#'; } } } } } ans = BFS(); if(ans == -1){ cout<<"King Peter, you can't go now!\n"; } else{ cout<<"Minimal possible length of a trip is "<<ans<<"\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(nm)$ 每筆測資 BFS 時間複雜度為 狀態數 $\times$ 操作數量 $= (nm) \times 8$,計為 $O(nm)$ 總時間複雜度為 $O(t(nm))$ # UVa 11730 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?11730 給兩個數字 $S, T$ 數字 $A$ 可以透過加上 $A$ 除了本身以外的質因數轉移成 $A'$ 問最少透過多少次轉移從 $S$ 變成 $T$,若無解則輸出 $-1$ ## 想法 By Koios 因為要找的是最少的步數,所以可以想到用 BFS 來搜尋答案 不過如果每次都要重新尋找質因數會消耗太多時間 所以可以預先建立質數表,每次只需要尋找這些質數是否符合條件即可 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int s, t, px, cur, Case=1, prime[1005], que[1000000], dis[1005]; bool not_prime[1005], vis[1005]; void build(){ for(int i=0 ; i<=1000 ; i++){ not_prime[i] = false; } int i; px=1; prime[0] = 2; for(i=3 ; i*i<=1000 ; i+=2){ if(!not_prime[i]){ prime[px++] = i; for(int j=i*i ; j<=1000 ; j+=2*i){ not_prime[j] = true; } } } for( ; i<=1000 ; i+=2){ if(!not_prime[i]){ prime[px++] = i; } } } int BFS(int start, int end){ que[0] = start; vis[start] = true; for(int i=0, j=1 ; i<j ; i++){ cur = que[i]; if(cur == end){ return dis[end]; } for(int k=0 ; k<px && prime[k]<cur && cur+prime[k]<=end ; k++){ if(cur%prime[k] == 0 && !vis[cur + prime[k]]){ que[j++] = cur + prime[k]; vis[cur + prime[k]] = true; dis[cur + prime[k]] = dis[cur] + 1; } } } return -1; } int main(){ // 建立質數表 build(); while(cin>>s>>t && (s!=0 && t!=0)){ for(int i=0 ; i<=1000 ; i++) dis[i] = 0, vis[i] = false; cout<<"Case "<<Case++<<": "<<BFS(s, t)<<"\n"; } return 0; } ``` # Kattis Conquest Campaign ## 題目 https://open.kattis.com/problems/conquestcampaign 給一張 $R \times C$ 的地圖,其中有 $N$ 個可重複的點,標示我們的起點 圖上沒有任何障礙物,在這至多 $N$ 個起點每天我們可以往 上下左右 四個方向擴散,請問最少幾天可以把全部地圖佔領 ## 想法 By Koios 因為要球最少步驟,所以想到用 BFS 來實作 會影響到未來發展的只有我們佔據了哪些點,所以狀態只需要紀錄被佔領的點座標即可 接下來每次向外四個方向擴散 要注意的是,起點可能重複給,但是實際上重複是沒有意義的 並且為了方便確定當前是不是全部的點都佔領了,我們透過確認當前總共有多少座標被標記在 BFS 的序列當中,那麼重複的點就不應該被重複計算,所以要先拔除 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int R, C, N, S, que[10005][2], dis[105][105]; int dx[] = {-1, 0, 0, 1}; int dy[] = {0, -1, 1, 0}; bool vis[105][105]; void print_all(){ for(int i=1 ; i<=R ; i++){ for(int j=1 ; j<=C ; j++){ if(vis[i][j]) cout<<"*"; else cout<<'.'; } cout<<"\n"; } cout<<"\n"; } int BFS(){ for(int i=0, j=S, x, y, nx, ny ; i<j ; i++){ x = que[i][0]; y = que[i][1]; if(j == R*C){ // 檢查是否已經佔領所有點 return dis[que[j-1][0]][que[j-1][1]]; } for(int k=0 ; k<4 ; k++){ nx = x + dx[k]; ny = y + dy[k]; if(nx <= 0 || nx > R || ny <= 0 || ny > C) continue; if(!vis[nx][ny]){ vis[nx][ny] = true; dis[nx][ny] = dis[x][y] + 1; que[j][0] = nx; que[j][1] = ny; j++; } } } return -1; } int main(){ for(int i=0 ; i<105 ; i++){ for(int j=0 ; j<105 ; j++){ vis[i][j] = false; } } cin>>R>>C>>N; S=0; // 紀錄不重複的起點數量 for(int i=0, x, y ; i<N ; i++){ cin>>x>>y; if(!vis[x][y]){ que[S][0] = x; que[S][1] = y; vis[x][y] = true; dis[x][y] = 1; S++; } } cout<<BFS()<<"\n"; return 0; } ``` ### 時間複雜度分析 輸入時間複雜度為 $O(N)$ BFS 時間複雜度為 狀態數 $\times$ 操作的數量 $= (nm) \times 4$ ,計為 $O(nm)$ 總時間複雜度為 $O(N + nm)$ # UVa 571 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?571 倒水問題,給兩個容量分別為 $A, B$ 加侖的桶子,有 $6$ 種操作 - fill A - fill B - empty A - empty B - pour A B - pour B A 請問最少要花多少步驟可以組合出 $N$ 加侖的水 ## 想法 By Koios 因為要問的是最小的步驟,所以可以想到用 BFS 來搜尋解法 每次檢查六種操作是否可以執行,然後枚舉出所有的操作,直到組合出 $N$ 加侖的水 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int a, b, max_a, max_b, cur_a, cur_b, n, ans; string methods[] = {"fill A", "fill B", "empty A", "empty B", "pour A B", "pour B A"}; /* methods: 0: fill A 1: fill B 2: empty A 3: empty B 4: pour A B 5: pour B A */ /* 0: cur_a 1: cur_b 2: 從哪個點來的 3: 用的方法編號 */ int res[1000005], que[1005][4]; bool vis[1005][1005]; void BFS(){ que[0][0] = 0; que[0][1] = 0; que[0][2] = -1; que[0][3] = -1; vis[0][0] = true; for(int i=0, j=1 ; i<j ; i++){ cur_a = que[i][0]; cur_b = que[i][1]; if(cur_a == n || cur_b == n){ ans = i; return; } if(cur_a != max_a && !vis[max_a][cur_b]){ // fill A vis[max_a][cur_b] = true; que[j][0] = max_a; que[j][1] = cur_b; que[j][2] = i; que[j][3] = 0; j++; } if(cur_b != max_b && !vis[cur_a][max_b]){ // fill B vis[cur_a][max_b] = true; que[j][0] = cur_a; que[j][1] = max_b; que[j][2] = i; que[j][3] = 1; j++; } if(cur_a != 0 && !vis[0][cur_b]){ // empty A vis[0][cur_b] = true; que[j][0] = 0; que[j][1] = cur_b; que[j][2] = i; que[j][3] = 2; j++; } if(cur_b != 0 && !vis[cur_a][0]){ // empty B vis[cur_a][0] = true; que[j][0] = cur_a; que[j][1] = 0; que[j][2] = i; que[j][3] = 3; j++; } if(max_b-cur_b >= cur_a){ // pour A B if(!vis[0][cur_b+cur_a]){ vis[0][cur_b+cur_a] = true; que[j][0] = 0; que[j][1] = cur_b+cur_a; que[j][2] = i; que[j][3] = 4; j++; } } else{ if(!vis[cur_a-(max_b-cur_b)][max_b]){ vis[cur_a-(max_b-cur_b)][max_b] = true; que[j][0] = cur_a-(max_b-cur_b); que[j][1] = max_b; que[j][2] = i; que[j][3] = 4; j++; } } if(max_a-cur_a >= cur_b){ // pour B A if(!vis[cur_a+cur_b][0]){ vis[cur_a+cur_b][0] = true; que[j][0] = cur_a+cur_b; que[j][1] = 0; que[j][2] = i; que[j][3] = 5; j++; } } else{ if(!vis[max_a][cur_b-(max_a-cur_a)]){ vis[max_a][cur_b-(max_a-cur_a)] = true; que[j][0] = max_a; que[j][1] = cur_b-(max_a-cur_a); que[j][2] = i; que[j][3] = 5; j++; } } } } // 利用遞迴的方式輸出過程 void print_ans(int cur){ if(que[cur][2] == -1){ return; } print_ans(que[cur][2]); cout<<methods[que[cur][3]]<<"\n"; } int main(){ while(cin>>max_a>>max_b>>n){ ans=-1; for(int i=0 ; i<1005 ; i++){ for(int j=0 ; j<1005 ; j++){ vis[i][j] = 0; } } BFS(); print_ans(ans); cout<<"success\n"; } return 0; } ``` # UVa 10047 ## 題解 http://domen111.github.io/UVa-Easy-Viewer/?10047 有一個輪子,從圓心將輪子平分成五等分,每等分都是 $72^{\circ}$,並且都有一個顏色,如附圖 ![](https://i.imgur.com/5Q8Apcr.png) 一開始這個輪子都是以**綠色**觸碰地面 在一張地圖上有起點、終點、障礙物,現在輪子在起點朝向北方,每次你可以選擇要 **前進**、**右轉$90^{\circ}$**、**左轉$90^{\circ}$** 每次前進,輪子就會以下一個顏色觸碰地面 請問,走到終點並且以綠色觸碰地面的最少操作數是多少 ## 想法 By Koios 因為要找的是最少操作數,可以想到用 BFS 因為除了走到終點以外,還需要符合綠色觸碰地面的條件,所以我們可以以 不同的方向、不同顏色觸碰地面 去走過走過的點 所以對於每個點的狀態有 - X 座標 - Y 座標 - 方向 - 顏色 接下來分別對三個不的同操作來轉移,記錄下 操作次數、拜訪與否 即可 這題跟過去 BFS 不太一樣的是,之前的題目大概都是只拿座標當作狀態,但是在本題多了方向、顏色的因素 計算操作次數也是一樣,如果只拿座標去當作參數,那會造成不同的狀態共用一個操作次數,這樣是不合理的 我自己寫這題的時候就沒考慮到這些,然後被卡了很久w ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; // 0: N, 1: W, 2: S, 3: E // 0: Green, 1: Black, 2: Red, 3: Blue, 4: White int n, m, x, y, start_x, start_y, end_x, end_y, dir, color, nx, ny, ncolor, ndir, ans, Case=1; int dis[30][30][4][5], que[2000000][4]; int dx[] = {-1, 0, 1, 0}; int dy[] = {0, -1, 0, 1}; int co[] = {1, 0, 0}; int dd[] = {0, 1, 3}; const int MaxN = 2147483647; char arr[30][30]; bool vis[30][30][4][5]; int BFS(){ que[0][0] = start_x; que[0][1] = start_y; que[0][2] = 0; que[0][3] = 0; vis[start_x][start_y][0][0] = true; dis[start_x][start_y][0][0] = 0; for(int i=0, j=1 ; i<j ; i++){ x = que[i][0]; y = que[i][1]; dir = que[i][2]; color = que[i][3]; if(arr[x][y] == 'T' && color == 0){ return dis[x][y][dir][color]; } for(int k=0 ; k<3 ; k++){ nx = x + co[k] * dx[dir]; ny = y + co[k] * dy[dir]; ndir = (dir + dd[k])%4; ncolor = (color+co[k])%5; if(nx<0 || nx>=n || ny<0 || ny>=m || arr[nx][ny] == '#') continue; if(!vis[nx][ny][ndir][ncolor]){ vis[nx][ny][ndir][ncolor] = true; dis[nx][ny][ndir][ncolor] = dis[x][y][dir][color] + 1; que[j][0] = nx; que[j][1] = ny; que[j][2] = ndir; que[j][3] = ncolor; j++; } } } return -1; } int main(){ while(cin>>n>>m && (n!=0 && m!=0)){ if(Case>1) cout<<"\n"; for(int i=0 ; i<n ; i++){ for(int j=0 ; j<m ; j++){ for(int k=0 ; k<4 ; k++){ for(int l=0 ; l<5 ; l++){ vis[i][j][k][l] = false; dis[i][j][k][l] = MaxN; } } cin>>arr[i][j]; if(arr[i][j] == 'S'){ start_x=i; start_y=j; } else if(arr[i][j] == 'T'){ end_x = i; end_y = j; } } } ans = BFS(); if(ans != -1){ cout<<"Case #"<<Case++<<"\n"<<"minimum time = "<<ans<<" sec\n"; } else{ cout<<"Case #"<<Case++<<"\n"<<"destination not reachable\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(nm)$ 每筆測資 BFS 時間複雜度為 狀態數 $\times$ 操作的數量 $= 20nm \times 3$,大約是 $O(nm)$ 每筆測資總時間複雜度約為 $O(nm)$ # UVa 321 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?321 在一個房子裡面有 $r$ 個房間、 $d$ 個通道以及 $s$ 個電燈開關 告訴你這 $d$ 個通道分別連接哪兩個房間,以及這 $s$ 個電燈開關位在哪個房間,分別可以控制哪個房間的電燈 現在有個人在編號 $1$ 的房間當中,想要走到編號 $r$ 的房間,並且由於他怕黑,經過的房間必定要是有開燈的才可以進入 每次可以選擇要觸發在當前房間有的開關,或是要走到下一個房間 編號 $1$ 的房間燈一開始是開的狀態,請問最少需要多少的操作可以走到編號 $r$ 的房間,並且輸出其過程 ## 想法 By Koios 因為要找的是最少的操作數,所以可以想到用 BFS 來解 題目要求當前所在房間的燈要是開的,所以當前在哪個房間是重要的狀態 並且當前有哪些燈是開著的也會影響到未來的發展 所以對於這一題有兩個狀態 **所在房間編號** 以及 **所有房間燈的狀態** 至於轉移則有兩種方式 第一種是移動到有連接的房間,需要保證下個房間燈是開的 第二種是切換某個房間的電燈開關,需要確保不會關到自己的燈 有了狀態以及轉移,接下來只要確保每次轉移的狀態過去都沒有出現過即可 這一題我用數字的每個 bit 去記錄 **房間燈開關的狀態** 以及 **房間 x 可以操作哪個房間的電燈** 轉移當中的切換開關就可以使用位元運算去找到下一個可行的點(詳細看程式碼) 至於回朔的方式,這一題我是用遞迴 在 BFS 轉移的過程當中去紀錄 **從誰轉移過來** 因為我是用陣列去模擬 queue ,所以資料都會留著,就可以直接紀錄是從陣列的哪個 index 轉移過來即可 至於要判斷是哪個操作,我們可以觀察 **房間燈開關的狀態** 如果前後狀態相同就是走到下一個房間 如果有個房間燈狀態從 $0 \to 1$,那就是開燈 反之就是關燈 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int r, d, s, ans, Case=1, edge_len[15], io[15], edge[15][15]; int que_id[40000], que_light[40000], from[40000], room_id[3000]; bool vis[15][3000]; int BFS(){ // 一開始的房間是在編號 1 // 一開始房間 1 的燈是開的,所以 2^1 的位置要是 1 que_id[0] = 1; que_light[0] = 2; vis[1][2] = true; for(int head=0, tail=1, id, light ; head<tail ; head++){ id = que_id[head]; light = que_light[head]; // 找到答案要確保只有編號 r 的房間燈是亮的 if(id == r && light == (1<<r)){ return head; } // 走到下一個房間 for(int i=0 ; i<edge_len[id] ; i++){ if(!vis[edge[id][i]][light] && (light & (1<<edge[id][i])) > 0){ que_id[tail] = edge[id][i]; que_light[tail] = light; from[tail] = head; tail++; } } // 切換開關 // i&=~lowbit 會讓 i 當中最低位的 1 變成 0,其他不變 for(int i=io[id], lowbit, nxt_light ; i!=0 ; i&=~lowbit){ lowbit = (i & -i); // 取得 i 最低位的位置 nxt_light = light ^ lowbit; // 得到可以操作的開關 // 切換前要先確認不會關到自己當前的房間燈 if(!vis[id][nxt_light] && (nxt_light & (1<<id))!=0){ vis[id][nxt_light] = true; que_id[tail] = id; que_light[tail] = nxt_light; from[tail] = head; tail++; } } } return -1; } void print_ans(int step, int cur_idx){ if(from[cur_idx] == -1){ cout<<"The problem can be solved in "<<step<<" steps:\n"; return; } int pre_idx = from[cur_idx]; print_ans(step+1, pre_idx); if(que_light[pre_idx] == que_light[cur_idx]){ // 操作是走到下個房間 cout<<"- Move to room "<<que_id[cur_idx]<<".\n"; } else{ // 操作是切換開關 // 透過 xor 可以剩下被切換的房間是 1 // 透過預先建好的表對應到房間 id // 也可以改用 log (以二為底) int room = room_id[que_light[cur_idx] ^ que_light[pre_idx]]; if((que_light[cur_idx] & (1<<room)) == 0){ // off cout<<"- Switch off light in room "<<room<<".\n"; } else{ // on cout<<"- Switch on light in room "<<room<<".\n"; } } } int main(){ for(int i=0 ; i<=10 ; i++){ // 將燈的值轉換成房間 id room_id[1<<i] = i; } while(cin>>r>>d>>s && (r!=0 || d!=0 || s!=0)){ // 初始化 from[0] = -1; for(int i=0 ; i<15 ; i++){ edge_len[i] = io[i] = 0; for(int j=0 ; j<3000 ; j++){ vis[i][j] = false; } } // 將每條邊用類似 list 的方式儲存比較方便 // 每條 list 的長度紀錄在 edge_len[] for(int i=0, a, b ; i<d ; i++){ cin>>a>>b; edge[a][edge_len[a]++] = b; edge[b][edge_len[b]++] = a; } // 利用每個 bit 去記錄房間 a 的開關控制的房間 for(int i=0, a, b ; i<s ; i++){ cin>>a>>b; io[a] |= (1<<b); } ans = BFS(); cout<<"Villa #"<<Case++<<"\n"; if(ans == -1){ cout<<"The problem cannot be solved.\n"; } else{ print_ans(0, ans); } cout<<"\n"; } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(d + s)$ 每筆測資 BFS 時間複雜度為 狀態數 $\times$ 操作的數量 $(d \times 2^s) \times (s + d)$,大約是 $O(2^s)$ 每筆測資遞迴求解時間複雜度為 $O(r)$ 每筆測資總時間複雜度為 $O(2^s)$ # TOJ 449 ## 題目 https://toj.tfcis.org/oj/pro/449/ 給一張迷宮的地圖,其中包含障礙物、門、鑰匙、起點、終點 每個門都需要有相對應的鑰匙才可以打開,而一把鑰匙可以無限次使用 地圖可能有多終點 每次可以選擇上下左右一個方向前進,請問最少需要花多少的步驟走到終點 ## 想法 By Koios 因為要找的是最少步驟,因此可以想到用 BFS 來解 現在所擁有的鑰匙會影響到未來發展,因此每次需要考慮到當前有哪些鑰匙 因此我們的狀態就包含了 **當前座標** 以及 **擁有的鑰匙** 我們依舊可以使用 bit 的方式去記錄每個鑰匙是否擁有 每次走到門就檢查該 bit 是否是 1 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int n, m, ans, start_x, start_y; int dx[] = {-1, 0, 0, 1}; int dy[] = {0, -1, 1, 0}; int key_num[26], dis[105][105][50]; int que[10010][3]; string keys="RGBY"; char arr[105][105]; bool vis[105][105][50]; int BFS(){ que[0][0] = start_x; que[0][1] = start_y; que[0][2] = 0; dis[start_x][start_y][0] = 0; vis[start_x][start_y][0] = true; for(int head=0, tail=1, x, y, key, nx, ny, nkey ; head<tail ; head++){ x = que[head][0]; y = que[head][1]; key = que[head][2]; if(arr[x][y] == 'X'){ return dis[x][y][key]; } for(int i=0 ; i<4 ; i++){ nx = x + dx[i]; ny = y + dy[i]; nkey = key; if(nx<0 || nx>=n || ny<0 || ny>=m || arr[nx][ny] == '#') continue; if(arr[nx][ny] == 'R' || arr[nx][ny] == 'G' || arr[nx][ny] == 'B' || arr[nx][ny] == 'Y'){ // 下一個點是門,檢查該 bit 是否為 1 if((key & (1<<key_num[arr[nx][ny]-'A'])) == 0){ continue; } } else if(arr[nx][ny] == 'r' || arr[nx][ny] == 'g' || arr[nx][ny] == 'b' || arr[nx][ny] == 'y'){ // 下個點是鑰匙,更新擁有的鑰匙 nkey = (key | (1<<key_num[arr[nx][ny]-'a'])); } if(!vis[nx][ny][nkey]){ vis[nx][ny][nkey] = true; que[tail][0] = nx; que[tail][1] = ny; que[tail][2] = nkey; dis[nx][ny][nkey] = dis[x][y][key] + 1; tail++; } } } return -1; } int main(){ // 把鑰匙對應一個數字 for(int i=0 ; i<keys.size() ; i++){ key_num[keys[i]-'A'] = i; } while(cin>>n>>m){ for(int i=0 ; i<n ; i++){ for(int j=0 ; j<m ; j++){ for(int k=0 ; k<50 ; k++){ vis[i][j][k] = false; } cin>>arr[i][j]; if(arr[i][j] == '*'){ start_x = i; start_y = j; } } } ans = BFS(); if(ans == -1){ cout<<"The poor student is trapped!\n"; } else{ cout<<"Escape possible in "<<ans<<" steps.\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(nm)$ 每筆測資 BFS 時間複雜度為 狀態數 $\times$ 操作的數量 $= (2^4nm) \times(4)$ 大約為 $O(nm)$ 每筆測資總時間複雜度為 $O(nm)$ # Kattis fontan ## 題目 https://open.kattis.com/problems/fontan 給一個 $N \times M$ 的圖,上面有 `.`, `#`, `V` 三種元素,分別表示 空氣、石頭、水 水可以每次從上往下流動一個單位,但是當往下會遇到石頭的時候水會往兩側移動 現在告訴你一開始哪裡有水,請輸出水不再流動時圖的情況 ## 想法 水的擴散情形就跟地圖上 BFS 一樣,每次都是全部擴散出去一格 每次檢查是否可以往下走,如果可以就往下,如果不行就檢查兩側,直到沒有水可以再移動就輸出答案 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int n, m, S=0, que[2550][2]; int dx[] = {0, 0}; int dy[] = {-1, 1}; char arr[55][55]; bool vis[55][55]; void BFS(){ for(int i=0, j=S, x, y, nx, ny ; i<j ; i++){ x = que[i][0]; y = que[i][1]; // 先檢查往下 nx = x + 1; ny = y; if(nx < 0 || nx >= n || ny < 0 || ny >= m) continue; if(!vis[nx][ny] && arr[nx][ny] == '.'){ vis[nx][ny] = true; que[j][0] = nx; que[j][1] = ny; arr[nx][ny] = 'V'; j++; } // 是石頭就再檢查左右 else if(!vis[nx][ny] && arr[nx][ny] == '#'){ for(int k=0 ; k<2 ; k++){ nx = x + dx[k]; ny = y + dy[k]; if(nx < 0 || nx >= n || ny < 0 || ny >= m) continue; if(!vis[nx][ny] && arr[nx][ny] == '.'){ vis[nx][ny] = true; que[j][0] = nx; que[j][1] = ny; arr[nx][ny] = 'V'; j++; } } } } } int main(){ for(int i=0 ; i<55 ; i++){ for(int j=0 ; j<55 ; j++){ vis[i][j] = false; } } cin>>n>>m; for(int i=0 ; i<n ; i++){ for(int j=0 ; j<m ; j++){ cin>>arr[i][j]; if(arr[i][j] == 'V'){ que[S][0] = i; que[S][1] = j; vis[i][j] = true; S++; } } } BFS(); for(int i=0 ; i<n ; i++){ for(int j=0 ; j<m ; j++){ cout<<arr[i][j]; } cout<<"\n"; } return 0; } ``` ### 時間複雜度分析 輸入時間複雜度為 $O(nm)$ BFS 時間複雜度為 狀態數 $\times$ 操作的數量 $= (nm) \times 3$ 計為 $O(nm)$ 總時間複雜度為 $O(nm)$ # Kattis pebblesolitaire ## 題目 https://open.kattis.com/problems/pebblesolitaire 題目包含多筆測資,每筆測資有一個字串,字串當中只會有 `-` 和 `o` 兩種字元 對於一個字串,如果有連續出現 `-oo` 或是 `oo-` ,我們可以選擇要不要把第一個以及最後一個交換,然後把中間換成 `-` 也就是 `-oo` -> `o--` ; `oo-` -> `--o` 請問經過多次這樣的操作之後,字串中最少可以只剩下多少 `o` ## 想法 By Koios 我們每次操作都必定只會讓 `o` 的數量減少,但是要在甚麼時機選擇進行操作是不明確的 這時候可以考慮枚舉,注意到每次 `o` 減少的數量都是固定的,那麼我們就可以用 BFS 來枚舉了 每次檢查字串中每個出現 `-oo` 以及 `o--` 的位置,只要操作後的字串過去沒有出現過就可以繼續枚舉 那麼要怎麼檢查字串有沒有用過? 可以用後面教到的位元運算來解決,我們用一個 int 的每個 bit 來記錄字串中某個位置是不是 `o` 例如 `--o` 就紀錄成 `001`,用十進位表示就是 `2` 字串長度固定會是 $12$,那麼我們只需要 $2^{12} = 4096$ 就可以紀錄全部的情況了 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int n, stat; string s, str, tmp, que[5000]; bool vis[5000]; int str_to_stat(string s){ // 把字串轉換成數字的狀態 int res = 0; for(int i=0 ; i<s.size() ; i++){ if(s[i] == 'o'){ res |= (1<<i); } } return res; } int BFS(){ que[0] = s; vis[str_to_stat(s)] = true; int i, j=1; for(i=0, j=1 ; i<j ; i++){ str = que[i]; for(int k=0 ; k<str.size()-2 ; k++){ if((str[k] == 'o' && str[k+1] == 'o' && str[k+2] == '-') || (str[k] == '-' && str[k+1] == 'o' && str[k+2] == 'o')){ tmp = str; swap(tmp[k], tmp[k+2]); tmp[k+1] = '-'; stat = str_to_stat(tmp); if(!vis[stat]){ vis[stat] = true; que[j] = tmp; j++; } } } } return j-1; } int main(){ cin>>n; while(n--){ for(int i=0 ; i<5000 ; i++){ vis[i] = false; } cin>>s; int last = BFS(); int ans = 0; for(int i=0 ; i<que[last].size() ; i++){ if(que[last][i] == 'o') ans++; } cout<<ans<<"\n"; } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(1)$ BFS 時間複雜度為 狀態數 $\times$ 操作數量,最多為 $4096 \times 10$ 假設字串長度是 $s$,那麼 BFS 時間複雜度估計為 $O(10 \times 2^s)$ 總時間複雜度為 $O(2^s \times n)$ ###### tags: `SCIST 演算法 題解`