<style> .reveal .slides { text-align: left; font-size:35px } </style> ## 圖上遍歷 & 樹論 ---- - 建圖方法 - 圖上 DFS/BFS 怎麼遍歷 - 樹上 DFS/BFS 怎麼遍歷 - 樹的基本演算法 - 樹經典例題 ---- ## 建圖方式 * 鄰接矩陣 * 鄰接串列 * 邊陣列 ---- ## 鄰接矩陣(Adjacency Matrix) - $n\times n$ 的矩陣,其中 $A_{ij}$ 代表點 $i$ 是否連通或是指向點 $j$ - 若為帶權圖則 $A_{ij}$ 存 $i$ 到 $j$ 的邊權 - 若圖有重邊也可以令 $A_{ij}$ 存邊 $(i,j)$ 的數量 - 空間複雜度 $O(n^2)$ - 遍歷圖的時間複雜度 $O(n^2)$ - 由於複雜度高因此不常用 ---- ## 無向圖 無權圖用 0/1 表示兩點間是否有連邊 無向圖的矩陣會是對稱的 ![image](https://hackmd.io/_uploads/rypetsTHa.png) ---- ## 有向圖 沒有連邊的點對通常權重設為 0 ![image](https://hackmd.io/_uploads/r18xwiaH6.png) ---- ## 鄰接串列(Adjacency Lists) - $n$ 個串列,第 $i$ 個串列存 $i$ 所指向的邊(點) - 若為帶權圖,則串列可以存 pair(目標點,邊權) - 通常以 vector 實作,也就是 vector 陣列或二維 vector - 空間複雜度 $O(m)$ - 遍歷圖的時間複雜度 $O(m)$ - 最常使用的儲存方式 ---- ![image](https://hackmd.io/_uploads/r1Psco6H6.png) ---- ![image](https://hackmd.io/_uploads/S1_bijTBp.png) ---- ### 無權無向圖 在點 $u$ 和點 $v$ 之間連邊 ```cpp= vector<int> edge[100005]; for(int i = 0, u, v; i < m; i++){ cin >> u >> v; edge[u].push_back(v); edge[v].push_back(u); } ``` ---- ### 帶權有向圖 新增一條點 $u$ 指向點 $v$ 權重為 $w$ 的邊 ```cpp= vector<pair<int, int>> edge[100005]; for(int i = 0, u, v, w; i < m; i++){ cin >> u >> v >> w; edge[u].push_back(make_pair(v, w)); } ``` ---- ### 初始化 ```cpp= void init(){ for(int i = 0 ; i <= n; i++){ edge[i].clear(); } } ``` ---- ## 邊陣列(Edge List) - 開一個 pair 陣列把所有邊儲存起來 - 用於最小生成樹 - 不適合拿來遍歷 ---- #### 無權邊 ```cpp= pair<int, int> edges[100005]; for(int i = 0; i < m; i++){ cin >> edges[i].first >> edges[i].second; } ``` #### 帶權邊 ```cpp= struct Edge{ int u, v, w; }edge[100005]; for(int i = 0; i < m; i++){ cin >> edge[i].u >> edge[i].v >> edge[i].w; } ``` ---- ## 深度優先搜索(Depth-First-Search,DFS) - 優先往深的地方走 - 通常以遞迴實作,直觀好寫(也可以使用stack取代遞迴) - 時間複雜度$O(E)$(若用鄰接串列) - 由於好寫,以及遞迴方便維護,有很多好的性質,因此是最常用的搜索方法 - 然而遞迴過深有可能導致stack overflow(RE),遇到時只得改成用迴圈+stack實作(或一些毒瘤方法) ---- ## 實作範例 ```cpp void dfs(int x){ vis[x]=1; for(int i:edge[x]){ if(!vis[i]) dfs(i); } } ``` ---- ## 廣度優先搜索(Breadth-First-Search,BFS) - 從起點開始往外散開 - 以迴圈+queue實作,寫起來較麻煩 - 對於無權圖,BFS可找出到起點的最短路徑 - 時間複雜度$O(E)$(若用鄰接串列) - 由於較難寫因此較少用,常用在找不帶權最短路 ---- ![](https://i.gifer.com/Hw24.gif) ---- ```cpp bool vis[MXN]; void bfs(int s){ queue<int> q; q.push(s); vis[s]=1; while(!q.empty()){ int x=q.front();q.pop(); for(int i:edge[x]){ if(!vis[i]) q.push(i),vis[i]=1; } } } ``` ---- ## 無向圖最短路 由於 BFS 是一層一層的, 從起點開始往外遍歷,同一層都遍歷過才往下一層遍歷。 因此走到當前節點時會是從起點到自己最短的 ---- 多開一個 dis 記錄從起點到自己最短距離 ```cpp= bool vis[MXN]; int dis[MXN]; void bfs(int s){ queue<int> q; q.push(s); vis[s]=1; while(!q.empty()){ int x=q.front();q.pop(); for(int i:edge[x]){ if(!vis[i]){ q.push(i)vis[i]=1; dis[i] = dis[x]+1; } } } } ``` --- ## 樹上DFS 由於沒有環,只要不要走回頭路就不會走到重複的點,因此多傳遞一個參數"父節點"就不需要visit陣列判斷點是否造訪過 ---- ```cpp void dfs(int x,int father){ for(int i:edge[x]){ if(i!=father) dfs(i,x); } } ``` ---- ## 樹上BFS 由於沒有環,只要不要走回頭路就不會走到重複的點,因此多傳遞一個參數"父節點"就不需要visit陣列紀錄點是否造訪過 從樹根開始,嚴格按照層次來訪問節點。 BFS 過程中也可以順便求出各個節點的深度和父親節點。 ---- ```cpp void bfs(int s){ queue<pair<int,int>> q; q.push(make_pair(s,0)); while(!q.empty()){ auto [x,y]=q.front();q.pop(); for(int i:vec[x]) if(i!=y) q.push(make_pair(i,x)); } } ``` ---- ![](https://miro.medium.com/v2/resize:fit:1280/1*GT9oSo0agIeIj6nTg3jFEA.gif) --- ## 樹的基本演算法 ---- ### 深度 求出一棵有根樹每個節點的深度 - 子節點深度為父節點+1(或邊權) - 根節點的深度為0(或1,視題目要求) ```cpp void dfs(int x,int father) { for(int i:edge[x]) { if(i!=father) d[i]=d[x]+1,dfs(i,x); } } ``` ---- ### 高度 求出一棵有根樹每個節點的高度 - 一個點的高度為所有子節點中,高度最高者+1 - 葉節點的高度為0(或1,視題目要求) ```cpp void dfs(int x,int father) { h[x]=0; for(int i:edge[x]) { if(i!=father) dfs(i,x),h[x]=max(h[x],h[i]+1); } } ``` --- ## 經典例題 樹例題 - 樹直徑 圖例題 - 遍歷網格圖(水窪問題) - 網格圖最短路(走迷宮) - 倒水問題 ---- ## [樹直徑](https://vjudge.net/contest/597907#problem/F) 求出一棵樹最遠兩點的距離 - 先任選一點作為根,將其視作有根樹 - 考慮分治法:直徑可能 1. 經過根節點,也就是前兩高度 2. 不經過根節點,也就是在某棵子樹上,因此遞迴計算子樹的答案再取max - 兩者再取max即為答案 ---- ```cpp int ans; int dfs(int x,int father){ int mh1=0,mh2=0,cur; for(int i:edge[x]){ if(i!=father){ cur=dfs(i,x); if(cur>mh1) mh2=mh1,mh1=cur; else if(cur>mh2) mh2=cur; } } ans=max(ans,mh1+mh2); return mh1+1; } ``` ---- #### 另解 1. 任選一點開始做 DFS,找出最遠(深)的點,記為 mx 2. 從 mx 開始做 DFS,其與最遠點的距離即為直徑(也就是第二次DFS的高度+1) ---- ```cpp int md,mx; void dfs(int x,int father,int d=0){ if(d>md) md=d,mx=x; for(int i:edge[x]){ if(i!=father) dfs(i,x,d+1); } } int find_diameter(){ md=-1; dfs(0,0); md=-1; dfs(mx,mx); return md; } ``` ---- ## [水窪問題](https://zerojudge.tw/ShowProblem?problemid=f493) 給定 $n\times m$ 大小的矩陣,0 代表沒有水,1 代表有水, 問有多少個水窪。 ex ```cpp 3 4 0 0 0 0 1 0 1 1 1 0 1 0 ans : 2 ``` ---- ## 解法 遍歷整個矩陣,每次遇到水就把那攤水使用 BFS/DFS 跑過一次全部消除, 消除完就可以獲得數量。 ---- ## 網格圖遍歷 ```cpp= bool vis[1005][1005]; // 紀錄每個位置是否有走過 int arr[1005][1005]; // 紀錄水窪 void dfs(int x, int y){ if(vis[x][y] || arr[x][y] == 0) return; vis[x][y] = 1; arr[x][y] = 0; if(x+1 < n) dfs(x+1, y); if(x-1 >= 0) dfs(x-1, y); if(y+1 < m) dfs(x, y+1); if(y-1 >= 0) dfs(x, y-1); } ``` ---- 改用迴圈遍歷周圍格子點 ```cpp= bool vis[1005][1005]; // 紀錄每個位置是否有走過 int arr[1005][1005]; // 紀錄水窪 int dx[4] = {1,-1,0,0}; int dy[4] = {0,0,1,-1}; bool safe(int x, int y){ return x >= 0 && x < n && y >= 0 && y < m; } void dfs(int x, int y){ if(vis[x][y] || arr[x][y] == 0) return; vis[x][y] = 1; arr[x][y] = 0; for(int i = 0; i < 4; i++){ if(safe(x+dx[i], y+dy[i])){ dfs(x+dx[i], y+dy[i]); } } } ``` ---- ## [走迷宮問題](https://zerojudge.tw/ShowProblem?problemid=a982) 給定一個 $n\times n$ 的二維整數數組,用來表示一個迷宮,數組中只包含 $0$ 或 $1$,其中 $0$ 表示可以走的路,$1$ 表示不可通過的牆壁。 最初,有一個人位於左上角 $(1,1)$ 處,已知該人每次可以向上、下、左、右任意一個方向移動一個位置。 請問,該人從左上角移動至右下角 $(n,n)$ 處,至少需要移動多少次。 ---- ## 解法 由於每次轉移都是從上下左右移動過來的, 所以只需要從起點開始將可到達的地方全部塞進去, 另外開一個陣列紀錄起點到他的最短距離。 ---- ## [倒水問題](https://vjudge.net/problem/UVA-10603) 給容量分別為 $a,b,c$ 的三杯水,倒法是一杯水到另一杯水倒到自己空或是別人滿為止,一開始 $a,b$是空的 $c$ 是滿的問是否能得到d的水如果不能則輸出 $d'$ (小於 $d$ 最大的容量) sample : ```cpp input 2 1 3 6 4 1 12 15 7 output 3 4 14 7 ``` ---- ## 解法 考慮每一個狀態當成一個節點, 把倒水後的狀態當成連向的節點建邊會形成一張有向圖 ![](https://i.imgur.com/hkAGfxm.png =450x) ---- ![](https://i.imgur.com/hkAGfxm.png =450x) (2,3,1) 把 c 的水倒到 a 中 (2,3,1) $\to$ (3,3,0) ---- ## 儲存節點狀態 可以宣告一個結構把每種容量的狀態紀錄起來 並用 map 紀錄每種狀態是否已經走過 ```cpp struct item{ int a, b, c; }; map<item, int> dis; int bfs(item start, item target){ queue<item> que; que.push(start); while(!que.empty()){ item it = que.front(); que.pop(); if(it == target){ return dis[it]; } // 窮舉每種倒水的情況 if(!dis.count(item(it.a + it.c, it.b, 0))){ //c->a dis[item(it.a + it.c, it.b, 0)] = dis[it]+1; que.push(item(it.a + it.c, it.b, 0)); } //c->b, a->b, a->c, b->c, b->a } } ``` ---- ## 畫圖工具 - [graph editor](https://csacademy.com/app/graph_editor/) - [graph online](https://graphonline.ru/en/) ---- [練習題單](https://vjudge.net/contest/597907)
{"title":"Tree & graph traval","description":"建圖方法","contributors":"[{\"id\":\"08326fa4-ca9b-4ca9-873e-239ebe76662c\",\"add\":7473,\"del\":0}]"}
    630 views
   owned this note