圖論
建國中學 張均豪
圖?
一張圖=一些點+一些邊
圖
1
2
4
5
3
有時點上或邊上會有權重
1
2
4
5
3
5
3
4
7
9
有向圖/無向圖
1
2
4
5
3
1
2
4
5
3
簡單圖
2
2
3
1
1
沒有自環或重邊的圖,
稱為簡單圖
自環
重邊
度數/相鄰
若兩點之間有連邊,則稱他們相鄰
一個點的度數為該點連出的邊數
路徑
1
2
4
5
3
簡單路徑:不經過重複點的路徑
環
1
2
4
5
3
起終點相同的路徑
連通/連通分量
1
2
4
5
3
6
弱/強連通/強連通分量
1
2
4
5
3
6
子圖
1
2
4
5
3
1
2
4
3
補圖
1
2
4
3
1
2
4
3
完全圖
1
2
4
3
樹/森林
1
2
4
3
5
7
6
8
9
沒有環的連通圖稱為樹,森林就是很多棵樹
二分圖
1
2
4
3
7
6
5
把一張圖分為兩部分,使兩部分內沒有邊連接
有向無環圖(DAG)
1
2
4
3
7
6
5
稀疏圖/稠密圖
- 稀疏:邊很少(跟點數差不多)
- 稠密:邊很多(點數的平方)
圖的儲存/遍歷
鄰接矩陣
=>
=>
之間有連邊
之間沒有連邊
1
2
4
5
3
=>
扣的
bool G[MAXN][MAXN];
int main(){
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++){
int a,b;
cin>>a>>b;
G[a][b]=1;
G[b][a]=1;
}
}
鄰接串列
把與每個點相鄰的點記錄起來
1
2
4
5
3
=>
扣的++
vector<int> G[MAXN];
int main(){
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++){
int a,b;
cin>>a>>b;
G[a].push_back(b);
G[b].push_back(a);
}
}
哪個比較好?
- 鄰接矩陣用於稠密圖
- 鄰接串列用於稀疏圖
- 大部分題目的圖都是稀疏圖
- 結論: 用鄰接串列
註: 還有其他存圖方式,只不過很少用
圖的遍歷-深度優先搜尋(DFS)
- 走到一個點
- 看他可以走到哪些點
- 把那些點都走完
- 離開
圖的遍歷-深度優先搜尋(DFS)
vector<int> G[MAXN];
bool visited[MAXN];
void dfs(int x){
visited[x]=true;
for(auto i:G[x]){
if(!visited[i]) dfs(i);
}
}
int main(){
for(int i=0;i<n;i++){
if(!visited[i]) dfs(i);
}
}
用遞迴實作
記得要把所有連通分量都跑一次
圖的遍歷-廣度優先搜尋(BFS)
- 先開一個queue
- 從queue前面拿出一個點
- 看他可以走到哪些點
- 把那些點丟進queue裡
- 離開
圖的遍歷-廣度優先搜尋(BFS)
vector<int> G[MAXN];
queue<int> que;
bool visited[MAXN];
void bfs(int x){
que.push(x);
visited[x]=1;
while(!que.empty()){
int now=que.front();
que.pop();
for(auto i:G[now]){
if(!visited[i]){
visited[i]=1;
que.push(i);
}
}
}
}
特性: 可以維護起點到所有點的最短距離
TIOJ 1336
給一個 的照片,- 代表空地,G 代表綠地,W 代表河流或湖泊,B 代表建築物。如果有兩格八方位相鄰的綠地,那麼兩格綠地會被計算為同一塊綠地。同樣地,八方位相鄰的空地也會被視為同一塊空地。現在需要知道城市中究竟有多少塊綠地和空地。
TIOJ 1085
給定一個立體(x * y * z)的迷宮,某人自(1,1,1)走至(x,y,z),請求出一條最短路徑,若有多組解,任一組都可。
TIOJ 1209
給定多張無向圖,對於每張圖,若該圖是二分圖,請輸出Yes,否則輸出No。
樹
名詞介紹
1
2
4
3
5
7
6
8
9
樹根
父節點
子節點
樹葉
子樹
No judge
給定一棵有根樹,求每個點
(1)到根節點的距離
(2)以該點為根的子樹大小
vector<int> G[MAXN];
int dis[MAXN], sz[MAXN];
void dfs(int x, int f){
dis[x]=dis[f]+1;
sz[x]=1;
for(auto i:G[x]){
if(i!=f){
dfs(i,x);
sz[x]+=sz[i];
}
}
}
樹直徑
一棵樹上最長的簡單路徑
做法:先從任意一點DFS到離自己最遠的點v,
再從點v DFS到離他最遠的點u,
則u,v之間的簡單路徑即為樹直徑
樹重心
樹上的一個點,如果將這點當作整棵樹的根,就可以使根節點最大的子樹最小。
可以證明該最大子樹大小不超過N/2(N為頂點數),且樹重心最多兩個,且若有兩個則他們必為鄰居。
做法:先從任意一點開始DFS,
如果找到一個子樹大小超過N/2,就往那個子樹走,
走到無法再走(所有子樹大小都<=N/2)時,
即找到樹重心。
DAG與拓樸排序
當我們想要在DAG上DP
每個點代表一個狀態
有向邊代表可能的轉移方式
拓樸排序則代表一種DP可行的計算順序
做法:紀錄每個點的入度,把所有入度為0的點拔掉,
在更新每個點的入度,重複直到點被拔光為止。
int deg[MAXN];
queue<int> que;
vector<int> G[MAXN], order;
void topological_sort(){
for(int i=1;i<=n;i++){
for(auto j:G[i]) deg[j]++;
}
for(int u=1;i<=n;i++){
if(deg[i]==0) que.push(i);
}
while(!que.empty()){
int tmp=que.front();
que.pop();
order.push_back(tmp);
for(auto i:G[tmp]){
deg[i]--;
if(deg[i]==0) que.push(i);
}
}
}
並查集
簡介
並查集是一個資料結構,它可以:
(1)查詢一元素所在的集合
(2)把兩個集合合併成一個
用於圖論中,它可以:
(1)查詢一元素位於的連通塊
(2)把兩個連通塊合併成一個
作法
對每個元素記錄自己的"上級",一開始先設為自己
查詢時,一直往上級尋找,直到停下來,
此時稱為找到"代表元素"
合併時,對兩個的元素分別找到"代表元素"後,
將其中一個的上級設為另一個
優化
路徑壓縮: 在找到代表元素後,直接將上級設為它
啟發式合併:合併時,把小集合併進大集合裡
複雜度:
成長速度極慢,可視為常數
int dsu[MAXN];
void init(int n){
for(int i=1;i<=n;i++){
dsu[i]=i;
}
}
int query(int x){
if(x==dsu[x]) return x;
int tmp=query(dsu[x]);
dsu[x]=tmp;
return tmp;
}
void union(int x, int y){
int a=query(x), b=query(y);
dsu[a]=b;
}
最短路徑
單點源最短路徑
給定一張帶權圖,求某一點到所有點的最短路徑
前提:不能有負環
鬆弛(Relaxation)
假如起點到點 的距離分別為
且
就可以把 更新為
Bellman-Ford Algorithm
枚舉所有邊進行鬆弛,鬆弛 次
時間複雜度:
vector<pii> G[MAXN];
int dis[MAXN];
void Bellman_Ford(){
dis[1]=0;
for(int i=2;i<=n;i++) dis[i]=INF;
for(int i=0;i<n-1;i++){
for(int j=0;j<m;j++){
int a = edge[j].F, b = edge[j].S;
if(dis[a]!=INF && dis[a]+c[j]<dis[b]){
dis[b]=dis[a]+c[j];
}
}
}
}
SPFA
剛剛好像有點慢(?
優化:每次只枚舉有被鬆弛到的點的邊,
期望複雜度:
最差複雜度:
還要更快?
如果沒有負邊的話...
最短路徑樹
子節點的距離一定不比父節點小
Dijkstra's Algorithm
每次選取一個不在樹上且距離最近的點,
加到樹上來鬆弛其他點,
用 priority_queue 維護!
複雜度
vector<pii> G[MAXN];
priority_queue <pii, vector<pii>, greater<pii> > pq;
void Dijkstra(){
fill(dis,dis+n+1,INF);
dis[1]=0;
pq.push({0,1});
pii cur;
for(int i=0;i<n;i++){
do{
cur=pq.top();
pq.pop();
} while(!pq.empty()&&cur.F>dis[cur.S]);
for(auto i:G[cur.S]){
if(dis[i.F] > cur.F+i.S){
dis[i.F]=cur.F+i.S;
pq.push({dis[i.F], i.F});
}
}
}
}
全點對最短路徑
一條路徑除了起終點,其他都是中繼點
Floyd-Warshall Algorithm
dp[k][i][j]=由第 i 點前往第 j 點,使用前 k 點的最短路徑
dp[k][i][j]=min(dp[k-1][i][j], dp[k-1][i][k]+dp[k-1][k][j])
複雜度:
int dis[MAXN][MAXN];
void Floyd_Warshall(){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j) dis[i][j]=0;
else dis[i][j]=INF;
}
}
//cin
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
}
OJDL7010、7042、7043
題敘略
TIOJ 1641
有 個城市、 條連接兩城市的單向道。你有一單位的貨物要從 A 運到 B,但是每經過一條道路後,你必須要多帶原先 倍的貨物,其中 是該條道路的「方便率」。求到達終點時你最少會有多少單位的貨物。
TIOJ 2049(極挑戰)
有 個城市、 條連接兩城市的雙向道。給定起終點S、T,求若拔掉一個點後,S到T的最短路徑最大會是多少。
最小生成樹
定義
對於所有連通圖,必定存在一個子圖頂點數與原圖相同,
且是一棵樹,稱為生成樹。
邊權總和最小的生成樹即稱為最小生成樹
性質
當我們要合併兩個最小生成樹
使用連接兩樹權重最小的邊一定會最好
Kruskal's Algorithm
每次找當前權重最小的邊,
看他是否連接兩個不同的最小生成樹,
是的話就代表需要用到這條邊,
並且把兩棵樹合併起來。
還記得並查集嗎?
int Kruskal(){
sort(edge, edge+m, cmp);
int ans=0;
for(int i=0;i<m;i++){
if(query(edge[i].a)!=query(edge[i].b)){
ans+=edge[i].c;
union(edge[i].a, edge[i].b);
}
}
return ans;
}
Prim's Algorithm
跟 Dijkstra 概念類似
將"與起點的距離"換成"與當前最小生成樹的距離"
TIOJ 1211
最小生成樹練習
LCA
Lowest Common Ancestor
1
2
4
3
5
7
6
8
9
LCA(5,6)=1
LCA(7,8)=4
LCA(3,9)=3
做法#1:倍增法
還記得我上禮拜講的sparse table 嗎?
\(anc[i][j]\) 代表點 \(i\) 的 \(2^j\) 輩祖
然後就可以得到
\(anc[i][j]=anc[anc[i][j-1]][j-1]\)
先對所有的\(i, j\)找到\(anc[i][j]\)
那這樣可以幹嘛呢
加快「一格一格爬上去」的過程!
那這樣可以幹嘛呢
首先,把深度比較深的那個點往上移到兩個點深度相同。(兩點LCA必只有一個深度)
那這樣可以幹嘛呢
如果此時\(a == b\)(也就是\(a\)是\(b\)的祖先,則直接回傳
那這樣可以幹嘛呢
再者,從最大的長度開始跑,如果\(anc[i][a] != anc[i][b]\),則LCA必定在\(anc[i][a]\)的祖先
那這樣可以幹嘛呢
最後,我們會走到兩個點\(a, b\),使得\(a != b, anc[0][a] == anc[0][b]\),因此根據定義,\(anc[0][a]\)就是LCA
int anc[20][MAXN], dep[MAXN];
void dfs(int x, int f, int d){
anc[0][x]=f;
dep[x]=d;
for(auto i:G[x]){
if(x!=f){
dfs(i,x,d+1);
}
}
}
void init(){
dfs(1,0,1);
for(int j=1;j<20;j++){
for(int i=1;i<=n;i++){
anc[j][i]=anc[j-1][anc[i][j-1]];
}
}
}
int find_lca(int a, int b){
if(dep[a]<dep[b]) swap(a,b);
int k=dep[a]-dep[b];
for(int c=0;k>0;c++){
if(k%2){
a=anc[c][a];
}
k/=2;
}
for(int j==__lg(n);j>0;j--){
if(anc[j][a]!=anc[j][b]){
a=anc[j][a];
b=anc[j][b];
}
}
if(a!=b) return anc[a][0];
return a;
}
圖論
By jass921026
圖論
- 2,511