<style> .reveal .slides { text-align: left; font-size:35px } </style> # 圖論 Introduction to Competitive Programming 2/9 ---- - 名詞介紹 - 建圖方法(矩陣, 鄰接表[vector]) - 圖上 DFS/BFS 怎麼遍歷 - 樹上 DFS/BFS 怎麼遍歷 - 樹的基本演算法 - 樹經典例題 ---- ## 名詞介紹 (以下內容也都會在資工其他領域學習到 ---- - 圖(graph):由一些點與一些邊所組成,通常以$G=(V,E)表示$ - 點(vertex):節點,通常以$V$表示 - 邊(edge):連接兩點,通常以$E$表示,$e=(u,v)$ 代表邊$e$連接$u,v$兩點,也就是$u,v$為$e$的端點 ---- ## 相關用語定義 - 權重(weight):指圖的點/邊附帶的數值 - 點數/邊數:點/邊的數量,通常記為$V/E(n/m)$ - 有向邊:邊可以分為無向邊(雙向)與有向邊(單向) - 重邊(multiple edge):指兩點之間有多條邊連接 - 自環(self loop):指兩端為同一點的邊$e=(u,u)$ - 度數(degree):一個點所連接的邊的數量,若是有向邊則又分為出度與入度 - 相鄰(adjacent):指兩個點間有無向邊相連 - 指向(consecutive):有向邊的起點"指向"終點 ---- ## 更多的定義 - 路徑(path):從起始點到目標點上所經過的所有點,路徑上的點/邊皆可重複 - 行跡(trace):邊不重複的路徑 - 迴路(circuit):邊不重複且起終點相同的路徑 - 簡單路徑(track):點不重複的路徑 - 環(cycle):點不重複且起終點相同的路徑 - 連通(connected):$u,v$連通若且唯若存在$u到v$或$v到u$的路徑,一群點連通代表這群點兩兩連通 ---- ## 各種圖的定義 - 簡單圖(simple graph):沒有重邊與自環的圖 - 無向圖(undirected graph)/有向圖(directed graph):只有無向邊/有向邊的圖 - 連通圖(connected graph):任兩點皆連通的圖 - 樹(tree):無向無環連通圖(其實也可以有向) - 森林(forest):每個連通分量(連通塊)都是樹的圖。按照定義,一棵樹也是森林 - 完全圖(complete graph):任兩點都相鄰的圖 - 有向無環圖(directed acyclic graph,DAG):... - 二分圖(bipartite graph):圖上的點可以分為兩群$U,V$使得同群點之間皆不相鄰 ---- ## 圖之間關係的定義 - 子圖(subgraph):邊/點皆為原圖的子集 - 補圖(complement graph):若兩張圖點集相同,邊集互斥且聯集為完全圖,則兩張圖互為補圖 - 同構(isomorphic):不考慮編號長的完全相同的圖 - 生成樹(spanning tree):點集相同且為樹的子圖 ---- ## 對於有根樹的定義 - 父親(parent node):對於除根以外的每個結點,定義為從該結點到根路徑上的第二個結點。 根結點沒有父結點。 - 祖先(ancestor):一個結點到根結點的路徑上,除了它本身外的結點。 根結點的祖先集合為空。 - 子結點(child node):如果 u 是 v 的父親,那麽 v 是 u 的子結點。 - 結點的深度(depth):到根結點的路徑上的邊數。 - 樹的高度(height):所有結點的深度的最大值。 - 兄弟(sibling):同一個父親的多個子結點互為兄弟。 - 後代(descendant):子結點和子結點的後代。 - 子樹(subtree):刪掉與父親相連的邊後,該結點所在的子圖。 ---- ## 進階圖這堂課則不討論 --- # 下課時間 --- ## 建圖方式 * 矩陣建圖 * 鄰接表[vector] * 邊建圖(很少用到因此不教) ---- ## 矩陣建圖(鄰接矩陣) - $n\times n$的矩陣,其中$A_{ij}$代表點$i$是否連通或是指向點$j$ - 若為帶權圖則$A_{ij}$存$i$到$j$的邊權 - 若圖有重邊也可以令$A_{ij}$存邊$(i,j)$的數量 - 空間複雜度$O(n^2)$ - 遍歷圖的時間複雜度$O(n^2)$ - 由於複雜度高因此不常用 ---- ## 鄰接表[vector] (鄰接串列) - $n$個串列,第$i$個串列存$i$所指向的邊(點) - 若為帶權圖,則串列可以存pair(目標點,邊權) - 通常以vector取代串列,也就是vector陣列或二維vector - 空間複雜度$O(m)$ - 遍歷圖的時間複雜度$O(m)$ - 最常使用的儲存方式 --- ## 深度優先搜索(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)$(若用鄰接串列) - 由於較難寫因此較少用,常用在找不帶權最短路 ---- ```cpp 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; } } } ``` --- ## 樹上DFS 由於沒有環,只要不要走回頭路就不會走到重複的點,因此多傳遞一個參數"父節點"就不需要visit陣列判斷點是否造訪過 ---- ```cpp void dfs(int x,int father){ for(int i:edge[x]){ if(i!=father) dfs(i,x); } } ``` ---- ## 樹上BFS 由於沒有環,只要不要走回頭路就不會走到重複的點,因此多傳遞一個參數"父節點"就不需要visit陣列判斷點是否造訪過(as DFS) 從樹根開始,嚴格按照層次來訪問節點。 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)); } } ``` --- ## 樹的基本演算法 ---- ### 深度 求出一棵有根樹每個節點的深度 - 子節點深度為父節點+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://cses.fi/problemset/task/1131) 求出一棵樹最遠兩點的距離 - 先任選一點作為根,將其視作有根樹 - 考慮分治法:直徑可能 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 ``` ---- ## 解法 每次遇到水就把那攤水跑過一次全部消除,消除完就可以獲得數量以及每次都更新最大最小值即可 ---- ## [走迷宮問題](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 =40%x) 假設最大容量為(6,3,1),而初始狀態為(6,0,0)
{"metaMigratedAt":"2023-06-17T19:03:23.837Z","metaMigratedFrom":"YAML","title":"圖論","breaks":true,"contributors":"[{\"id\":\"e4a3af8b-860c-46a0-96f1-558667fdcf41\",\"add\":20534,\"del\":14007},{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":97,\"del\":921},{\"id\":\"5df14ba0-4dd2-4172-b309-8b5af9ede7a1\",\"add\":13,\"del\":0}]"}
    959 views
   Owned this note