# m372. 3. 搬家 ### 題目網址: https://zerojudge.tw/ShowProblem?problemid=m372 --- ### 解題思路 * 需要用到的函式: 1. canconnect(目前水管,目標水管,連通方向) : 用來判斷某個方向能不能接通。 2. DFS 主迴圈 : 不斷往四個方向擴張,只要「沒出界」且「水管接得通」且「沒走過」,就繼續遞迴並計數。 3. main主程式 : 掃描地圖上的每一個點 $(i, j)$如果該點 有水管 且 沒被拜訪過 $\rightarrow$ 發動一次 DFS。 每次 DFS 結束會回傳一個大小,代表跟他連結的水管數量,紀錄最大的那個就是答案。 * 需要用到的變數 : 1. grid地圖 2. visited 用來存是否走過(我喜歡叫他影子地圖 3. pipetype 定義水管開口 4. dx dy op 偏移量 --- ### 程式碼 DFS版本 ```cpp #include<bits/stdc++.h> using namespace std; //全域變數 char grid[600][600]={"0"}; //地圖 int visited[600][600]={{0}}; //是否走過 0為沒走過 int n,m; map<char,vector<bool>> pipetype={ //定義每個水管的開口:上, 下, 左, 右 // 上 下 左 右 {'I', {true, true, false, false}}, {'H', {false, false, true, true }}, {'L', {true, false, false, true }}, {'J', {true, false, true, false}}, {'7', {false, true, true, false}}, {'F', {false, true, false, true }}, {'X', {true, true, true, true }} }; //偏移量 上 下 左 右 int dx[4]={0,0,-1,1}; int dy[4]={-1,1,0,0}; int op[4]={1,0,3,2};// 這是反方向,例如我要找目前跟上方的管子k=0對到map的上,此時op[k]=1 對應到map的下 bool canconnect(char a,char b,int k){//判斷兩根水管可不可以連接 return pipetype[a][k] && pipetype[b][op[k]];//兩個都是true才會回傳true } int dfs(int cury,int curx){//目前座標 visited[cury][curx]=1;//標記成已經走過 int count=1;//這裡算一格 //嘗試往四周擴散 for(int k=0;k<4;k++){ int ny=cury+dy[k]; int nx=curx+dx[k]; //1.邊界檢查 if (ny < 0 || ny >= n || nx < 0 || nx >= m) continue; //2.接通檢查 if(grid[ny][nx]!='0' && visited[ny][nx]==0 && canconnect(grid[cury][curx],grid[ny][nx],k)){ //不是空氣 沒有走過 可以連接 count+=dfs(ny,nx); //繼續遞迴,並且把底下算出的值加上目前的count,就是這層的聯通管子數 } } return count; } int main(){ cin>>n>>m; for(int i=0;i<n;i++){ for(int j=0;j<m;j++){ cin>>grid[i][j]; } } //count int ans=0; for(int i=0;i<n;i++){ for(int j=0;j<m;j++){ if(grid[i][j]!='0' && visited[i][j]==0){ //不是空氣而且沒走過 ans=max(ans,dfs(i,j));//開始dfs,並且把每次算完的值取最大的 } } } cout<<ans<<endl; } ``` 這個程式碼最後有一個測資過不了,顯示記憶體區段錯誤,我詢問過其他高手們,他們推測可能是遞迴太深導致stack爆掉,他們還推薦我試試其他方式。 * DFS用的是 「系統堆疊 (System Stack)」,這個空間很小,容易爆掉。 * BFS (廣度優先):不會爆,因為它用的是 「記憶體堆積 (Heap)」,這個空間很大(通常是電腦的 RAM)。 * DSU (並查集):不會爆,因為它主要是操作 「陣列」,幾乎不需要遞迴,路徑非常短。 --- ### 程式碼 BFS版本 如果要改成BFS,可以延續剛剛寫的版本,只把dfs函式改成bfs函式,其他的都不用改 :::info BFS的核心是queue ::: ```cpp int bfs(int start_y, int start_x) { queue<pair<int,int>> q; //主要核心queue // 把起點放進去 q.push({start_y, start_x}); visited[start_y][start_x] = 1; int count = 0; // 紀錄數量 while(!q.empty()) { //只要queue中還有數,就繼續做 // 1. 拿出排頭 pair<int,int> curr = q.front(); q.pop(); count++; // 算一格 int cy = curr.first; int cx = curr.second; // 2. 往四周蔓延 for(int k=0; k<4; k++) { int ny = cy + dy[k]; int nx = cx + dx[k]; // 邊界檢查 if (ny < 0 || ny >= n || nx < 0 || nx >= m) continue; // 接通檢查 (沒走過 + 能接通) if(grid[ny][nx]!='0' && visited[ny][nx]==0 && canconnect(grid[cy][cx], grid[ny][nx], k)) { visited[ny][nx] = 1; // 立刻設成已經拜訪,避免重複加入 q.push({ny, nx}); //這裡不是呼叫 dfs,而是「加入隊伍」 } } } return count; } ``` --- ### 並查集 (Disjoint Set Union) 版本 這也是他們提出的一個方法,我覺得解這種問題很適合 ```cpp #include<bits/stdc++.h> using namespace std; //全域變數 char grid[600][600]={"0"}; //地圖 int visited[600][600]={{0}}; //是否走過 0為沒走過 int n,m; map<char,vector<bool>> pipetype={ //定義每個水管的開口:上, 下, 左, 右 // 上 下 左 右 {'I', {true, true, false, false}}, {'H', {false, false, true, true }}, {'L', {true, false, false, true }}, {'J', {true, false, true, false}}, {'7', {false, true, true, false}}, {'F', {false, true, false, true }}, {'X', {true, true, true, true }} }; //偏移量 上 下 左 右 int dx[4]={0,0,-1,1}; int dy[4]={-1,1,0,0}; int op[4]={1,0,3,2};// 這是反方向,例如我要找目前跟上方的管子k=0對到map的上,此時op[k]=1 對應到map的下 // DSU 結構 struct DSU { vector<int> parent;// parent[i]=i的老大是誰(會指向最上層的) vector<int> sz; // 用來紀錄每個老大底下有多少小弟(包含自己) DSU(int total_nodes) { //建構子寫法,在宣告這個struct的時候會一併執行,把初始值設定好 parent.resize(total_nodes); sz.resize(total_nodes, 1); // 一開始大家都是 1 個人(沒有小弟) for(int i=0;i<total_nodes;i++) parent[i] = i; // 一開始大家都是自己的老大 } int find(int x) { if (parent[x] == x) return x; //自己就是最上層老大,回傳自己 //下面三行是路徑壓縮(把樹壓縮成兩層) int root=find(parent[x]);//遞迴找最上層的老大 parent[x]=root;//更新我的老大(直接指向最上面的人),方便下次查 return root; } void unite(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { //x和y的老大是不同人 parent[rootX] = rootY; // 把 x接到 y 下面,y變成老大 sz[rootY] += sz[rootX]; // Y 的人數增加了 X 的人數 } } }; bool canconnect(char a, char b, int k) { return pipetype[a][k] && pipetype[b][op[k]]; } int main() { cin>>n>>m; for(int i=0;i<n;i++){ for(int j=0;j<m;j++){ cin>>grid[i][j]; } } DSU dsu(n * m);//建立 DSU,總節點數是 n * m for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (grid[i][j] == '0') continue; int current_id = i * m + j; //因為DSU是一維的工具,所以我需要把二維座標轉成一維的獨特編號,公式:i(列) * m(寬度) + j(行) for (int k = 0; k < 4; k++) {//嘗試往四周蔓延 int ny = i + dy[k]; int nx = j + dx[k]; int neighbor_id = ny * m + nx; if (ny >= 0 && ny < n && nx >= 0 && nx < m && grid[ny][nx] != '0') { //目標格子在便藉內且不是0 if (canconnect(grid[i][j], grid[ny][nx], k)) {//有連通 dsu.unite(current_id, neighbor_id); } } } } } // 最後掃描一遍,找出最大的 size int max_ans = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (grid[i][j] != '0') {//過濾掉空地 int id = i * m + j; if (dsu.parent[id] == id) {// 只看老大身上的 size max_ans = max(max_ans, dsu.sz[id]); } } } } cout << max_ans << endl; } ``` 這個版本就成功通過全部測資了,如果有其他想法歡迎一起討論~ --- [TOC]