<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 表示兩點間是否有連邊
無向圖的矩陣會是對稱的

----
## 有向圖
沒有連邊的點對通常權重設為 0

----
## 鄰接串列(Adjacency Lists)
- $n$ 個串列,第 $i$ 個串列存 $i$ 所指向的邊(點)
- 若為帶權圖,則串列可以存 pair(目標點,邊權)
- 通常以 vector 實作,也就是 vector 陣列或二維 vector
- 空間複雜度 $O(m)$
- 遍歷圖的時間複雜度 $O(m)$
- 最常使用的儲存方式
----

----

----
### 無權無向圖
在點 $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)$(若用鄰接串列)
- 由於較難寫因此較少用,常用在找不帶權最短路
----

----
```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));
}
}
```
----

---
## 樹的基本演算法
----
### 深度
求出一棵有根樹每個節點的深度
- 子節點深度為父節點+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
```
----
## 解法
考慮每一個狀態當成一個節點,
把倒水後的狀態當成連向的節點建邊會形成一張有向圖

----

(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}]"}