🔔五道習題手把手拿捏普利姆演算法以及克魯斯卡爾演算法
- 💓最小生成樹演算法大綱
- 🌟最小生成樹的基本概念
- 🌻自由樹和生成樹
- 🌻最小生成樹
- 💓普利姆演算法(prim)——模板題
- 🌟通俗演示
- 🌟例題描述
- 🌟參考代碼(C++版本)
- 🌟演算法模板
- 🌟疑難雜癥剖析
- 💓克魯斯卡爾演算法(Kruskal)——模板題
- 🌟通俗演示
- 🌟例題描述
- 🌟參考代碼(C++版本)
- 🌟演算法模板
- 🌻疑難雜癥剖析
- 💓例題一
- 🌟解題報告
- 🌻解題思路
- 🌻參考代碼(C++版本)
- 💓例題二
- 🌟解題報告
- 🌻解題思路
- 🌻參考代碼(C++版本)
- 🌻疑難雜癥剖析
- 💓例題三
- 🌟解題報告
- 🌻解題思路
- 🌻參考代碼(C++版本)
- 🌻疑難雜癥剖析
- 💓總結
- 謝謝友友們耐心觀看啦~,若有偏頗,歡迎及時私信指出喔💖💖💖
- 基礎演算法持續更新中ing~
| 咱們先看看藍橋杯對最小生成樹的考察范圍如下 |

| 圈定的范圍是考Prim演算法和Kruskal演算法,那接下來咱們就開始逐步了解最小生成樹以及可愛的Prim演算法和Kruskal演算法吧 |
💓最小生成樹演算法大綱

🌟最小生成樹的基本概念
🌻自由樹和生成樹
自由樹(樹):
1、自由樹就是一個無回路的連通圖(沒有確定根)
2、n個頂點就一定有n-1條邊
生成樹:
1、包含全部頂點
2、n-1條邊全部在圖中
圖的生成樹不惟一,從不同的頂點出發進行遍歷,可以得到不同的生成樹,

🌻最小生成樹
| 如果圖G是一個連通圖,G上的一棵各邊權值之和最小的帶權生成樹,稱為G的最小生成樹, |
💓普利姆演算法(prim)——模板題
🌟通俗演示

🌟例題描述

🎇🎇🎇傳送門🎇🎇🎇
| 因為這是用來做分析的模板題,題目要求里就很直接明了的指出,要咱們求最小生成樹的樹邊權重之和,現在就可以直接去觀察資料范圍,可以看出是稠密圖,那么我們可愛的Prim演算法就可以掏出來啦~ |
🌟參考代碼(C++版本)
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 510 , INF = 0x3f3f3f3f;
int n,m;
int g[N][N];
int dist[N];
bool st[N];
int prim()
{
//初始化距離陣列
memset(dist,0x3f,sizeof dist);
//res中存放最小生成樹的樹邊權重之和
int res = 0;
for(int i = 0; i < n;i++)
{
int t = -1;
for(int j = 1; j <= n;j++)
if(!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
//如果不是第一個點以及距離最小的點距離是正無窮,說明當前距離最近的點,到集合的距離都是正無窮,即當前圖不連通
if(i && dist[t] == INF) return INF;
if(i) res += dist[t];
//用t去更新其他點
for(int j = 1; j <= n;j++)
dist[j] = min(dist[j],g[t][j]);//注意這里是g[t][j],Dijkstra中是dist[t]+g[t][j]
st[t] = true;
}
return res;
}
int main()
{
//輸入
scanf("%d%d",&n,&m);
//初始化鄰接矩陣
memset(g,0x3f,sizeof g);
//建圖
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
//因為是無向圖,所以得建a 到 b 和 b 到 a的
g[a][b] = g[b][a] = min(g[a][b],c);
}
int t = prim();
if(t == INF ) puts("impossible");
else printf("%d\n",t);
return 0;
}
🌟演算法模板
Prim演算法實作的流程圖如下:

Prim演算法實作的代碼描述:
int n; // n表示點數
int g[N][N]; // 鄰接矩陣,存盤所有邊
int dist[N]; // 存盤其他點到當前最小生成樹的距離
bool st[N]; // 存盤每個點是否已經在生成樹中
// 如果圖不連通,則回傳INF(值是0x3f3f3f3f), 否則回傳最小生成樹的樹邊權重之和
int prim()
{
memset(dist, 0x3f, sizeof dist);
int res = 0;
for (int i = 0; i < n; i ++ )
{
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
if (i && dist[t] == INF) return INF;
if (i) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
🌟疑難雜癥剖析
一、建圖
g[a][b] = g[b][a] = min(g[a][b],c);
| 因為是無向圖,所以需要建立從a 到 b的邊 以及 從b 到 a的邊 |
二、計算最小權值之和
if(i && dist[t] == INF) return INF;
if(i) res += dist[t];
| 計算最小的權值之和需要在保證當前這個點能和已經存在的最小生成樹集合之間存在最小距離,倘若是距離是正無窮,說明無法與已經存在的最小生成樹連通 |
三、更新
for(int j = 1; j <= n;j++)
dist[j] = min(dist[j],g[t][j]);
| 將演算法模板的代碼實作看完的小伙伴可能發現,Prim演算法和樸素版Dijkstra演算法好像呀 |

相似,又不完全相似
Dijkstra演算法中,dist陣列維護的是1號點到當前點的距離
Prim演算法中,dist陣列維護的是當前點到已經存在的最小生成樹集合的距離
因此
Dijkstra演算法的更新是將t作為中介,將從1號點到j的距離dist[j] 和 1號點到t,再從t到j的距離dist[t]+g[t][j]作比較,找到最小的權值
Prim演算法的更新則是,t是作為已經確實的最小生成樹集合的代表

💓克魯斯卡爾演算法(Kruskal)——模板題
🌟通俗演示

Kruskal演算法在理解上是比Prim更舒服的
🌟例題描述

🎇🎇🎇傳送門🎇🎇🎇
🌟參考代碼(C++版本)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;
int n,m;
int p[N];//并查集
int cnt,res;//cnt存當前加入多少條邊 ; res存放的是最小生成樹中所有樹邊的權重之和
//kruskal演算法可以不用鄰接表存,只要把點和點到點的邊存下來就好
//就不用使用復雜的資料結構,直接結構體搞了,只是要注意多載小于符號,讓sort的時候可以根據權重來比較
struct Edge
{
int a,b,w;
bool operator < (const Edge &W)const
{
return w < W.w;
}
}edges[N];
//并查集的find函式
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void kruskal()
{
//對存放的邊按照權重排序
sort(edges,edges+m);
//初始化并查集
for(int i = 1; i <= n;i++) p[i] = i;
//從小到大列舉所以邊
for(int i = 0; i < m; i++)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
//找到a的祖宗結點
a = find(a),b = find(b);
//如果a 和 b 不在一個連通塊中
if(a != b)
{
//將a連通到b上
p[a] = b;
res += w;
cnt ++;
}
}
}
int main()
{
//輸入
scanf("%d%d",&n,&m);
//建圖
for(int i = 0; i < m;i++)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i] = {a,b,w};
}
kruskal();
//輸出 :輸出一個整數,表示最小生成樹的樹邊權重之和
//n個點,因為成最小生成樹只能有n-1邊
if(cnt < n-1) puts("impossible");
else printf("%d\n",res);
return 0;
}
🌟演算法模板
Kruskal演算法的執行流程圖如下;

Kruskal演算法代碼實作:
int n, m; // n是點數,m是邊數
int p[N]; // 并查集的父節點陣列
struct Edge
{
int a,b,w;
}edges[N];
//自定義比較的方式,待會放置到sort函式中進行比較
bool cmp(Edge a,Edge b)
{
return a.w < b.w;
}
int find(int x) // 并查集核心操作
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal()
{
sort(edges, edges + m);
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ )
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) // 如果兩個連通塊不連通,則將這兩個連通塊合并
{
p[a] = b;
res += w;
cnt ++ ;
}
}
if (cnt < n - 1) return INF;
return res;
}
🌻疑難雜癥剖析
一、存盤邊
| Kruskal演算法和Bellman-Ford演算法挺相似的,都是隨便大方的乖演算法,只要能夠獲取到存盤的資訊就好,因此演算法模板中可以使用最簡單的結構體存盤資料, |

| 唯一需要注意的是,要重新制定比較的邏輯,讓比較的邏輯是根據權重w來比較的, |
二、并查集的使用
2.1、初始化并查集——讓每個結點做自己的父結點
2.2、并查集的find函式的撰寫
嗯哼?對并查集不了解呀,嗦嘎

| 貼心的筆者以前寫過關于并查集的博客喔,兩篇都是上了熱榜的,值得信賴,對并查集不太熟悉的友友就可以看看這篇文章呀,筆芯, |
演算法基礎系列第二章——出發,淺酌并查集
看到這里的小伙伴看到這里可能會覺得,呀,就這,就這呀?!學會了學會了

💓例題一

🎇🎇🎇傳送門🎇🎇🎇
那咱們先來一道簡單的活動活動筋骨
🌟解題報告
🌻解題思路
| 把題目閱覽完之后,小伙伴們心中大抵知道這是最小生成樹的題,因為題目資訊給的很直接呀,hh,題目中讓咱們求最小生成樹的各邊的長度之和,然后看給的資料范圍,可以看出是稀疏圖,那么Kruskal演算法就可以拿出來了, 感覺起來,和咱們上面演示的例題是不是感徑訓湯不換藥呀,那就開始操作啦~ |

🌻參考代碼(C++版本)
🎇🎇🎇點擊這里查看參考代碼喔~🎇🎇🎇
💓例題二
| 例題二是藍橋練習系統上的習題,但是我感覺測驗資料好像有問題吧~,跑出來只有50分,我把好幾位博主的Ac代碼復制過來跑也是50分 |


🎇🎇🎇傳送門🎇🎇🎇
🌟解題報告
🌻解題思路
題目要的是一棵方差最小的生成樹
一、數學知識的回憶——方差

二、解決需求——計算方差
這道題要咱們求的方差最小,那么就是要求每條邊的(權重 - 平均值)2的和最小,
因為隨著每次輸入是動態的,所以直接求平均值是比較困難的,這個時候,我們就可以拿出我們可愛的列舉,

我們可以列舉全部邊權和的可能,對于每一個可能,都去執行Kruskal演算法,執行的時候,把(權重 - 平均值)2作為真正要用的權重w,
同時,當我們先前列舉的邊權和等于執行完Kruskal演算法之后的生成樹權值的和,此時就可以更新答案了,
🌻參考代碼(C++版本)
🎇🎇🎇點擊這里查看參考代碼喔~🎇🎇🎇
🌻疑難雜癥剖析
一、執行思路
| 執行的大框架仍然是可以套用kruskal演算法的模板, |
kruskal演算法執行流程
二、圈定列舉的區間
//minv就是可能出現的權值和的最小值,maxv則是可能出現的權值之和的最大值
for(int i = 0; i < n - 1; i++)
minv += tmp[i];
for(int i = m - 1; i > m - n; i--)
maxv += tmp[i];
for(int i = minv; i <= maxv; i++)
Kruskal(i);
因為最小生成樹有n-1條邊,
| 下界minv可以從存放在tmp陣列(注:tmp陣列已從小到大排序)中的權值依次獲取n-1個最小值, |
| 上界maxv則可以從存放在tmp陣列的權值倒著獲取n-1個最大值, |
三、謹記需求
| 方差才是我們最后要獲取的,所以在kruskal演算法中進行排序前,把(權重 - 平均值)^2^作為真正要用的權重w, |
for(int i = 0; i < m; i++)
e[i].w = (e[i].val - ave) * (e[i].val - ave);
//重新排序
sort(e, e + m, cmp);
💓例題三
例題描述:

🎇🎇🎇傳送門🎇🎇🎇
🌟解題報告
🌻解題思路
一、第一感受
| 在閱覽完例題之后,從"為了使花費最少,他希望用于連接所有的農場的光纖總長度盡可能短"這句話可以get到,這道題其實想讓我們求這個圖的最小生成樹, |
二、看資料范圍定演算法
| 重新看看最小生成樹問題中,在什么圖中應該使用什么型別的演算法模板 |
最小生成樹演算法大綱
對于這道題而言,從資料范圍可以得知是稠密圖,那么我們就可以回憶一下prim演算法的實作流程然后去逐步落實,
🌻參考代碼(C++版本)
🎇🎇🎇點擊這里查看參考代碼喔~🎇🎇🎇
🌻疑難雜癥剖析
| 留意這道例題的輸入輸出,和我們上面用來演示的模板題又不太相似的喔 本題是一個輸入,根據輸入建立一個鄰接矩陣,因此在代碼落實到時候就更清爽了,初始化鄰接矩陣和建圖環節可以放在一起啦~ |
//輸入
scanf("%d",&n);
for(int i = 1;i <= n;i++)
for(int j = 1;j<=n;j++) cin >>g[i][j];
💓總結
| 一、腦海中對稠密圖和稀疏圖應該使用的prim演算法和kruskal演算法的模板實作流程有個大致的印象,倘若忘了,可以回來看看實作流程和模板題喔 |
| 二、留意輸入輸出,從而對初始化和建圖更清晰 |
| 三、最小生成樹假如只是用來解決考試的話,只要明白通俗演示,然后可以畫出各自的生成樹就好,對于競賽的同學就建議先熟悉模板,然后寫題鞏固,去親自感受將圖論問題中的圖抽象出來的程序 |
| 四、圖論問題難點主要是將問題中這個圖抽象出來,然后才可以定,要用dfs解決?還是拓撲排序了?演算法模板是可以很快的套上去的 |
謝謝友友們耐心觀看啦~,若有偏頗,歡迎及時私信指出喔💖💖💖
基礎演算法持續更新中ing~

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/413426.html
標籤:其他
下一篇:紅黑樹(C++實作)
