洛谷P3366 最小生成樹板子題
這篇博客介紹兩個算法:Prim算法和Kruskal算法,兩個算法各有優劣
一般來說當圖比較稀疏的時候,Kruskal算法比較快
而當圖很密集,Prim算法就大顯身手了
下面是這兩種算法的介紹
Prim算法
百度百科定義:傳送門
好吧,其實當我第一眼看到這個東西的時候感覺和Dijkstra好像,但是學了之后發現其實區別還是很明顯(並且好記)的
Dijkstra是維護從到源點的最短長度,而Prim則是維護到最小生成樹的最短長度(其實就是到最小生成樹上所有點的最短長度)
那么Prim到底是什么呢?
Prim的思想是將任意節點作為根,再找出與之相鄰的所有邊(用一遍循環即可),再將新節點更新並以此節點作為根繼續搜,維護一個數組:dis,作用為已用點到未用點的最短距離。
Prim算法之所以是正確的,主要基於一個判斷:對於任意一個頂點v,連接到該頂點的所有邊中的一條最短邊(v, vj)必然屬於最小生成樹(即任意一個屬於最小生成樹的連通子圖,從外部連接到該連通子圖的所有邊中的一條最短邊必然屬於最小生成樹)
舉個栗子:
STEP 1
此為原始的加權連通圖。每條邊一側的數字代表其權值。
STEP 2
頂點D被任意選為起始點。頂點A、B、E和F通過單條邊與D相連。A是距離D最近的頂點,因此將A及對應邊AD以高亮表示。
STEP 3
下一個頂點為距離D或A最近的頂點。B距D為9,距A為7,E為15,F為6。因此,F距D或A最近,因此將頂點F與相應邊DF以高亮表示。
STEP 4
算法繼續重復上面的步驟。距離A為7的頂點B被高亮表示。
STEP 5
在當前情況下,可以在C、E與G間進行選擇。C距B為8,E距B為7,G距F為11。點E最近,因此將頂點E與相應邊BE高亮表示。
STEP 6
這里,可供選擇的頂點只有C和G。C距E為5,G距E為9,故選取C,並與邊EC一同高亮表示。
STEP 7
頂點G是唯一剩下的頂點,它距F為11,距E為9,E最近,故高亮表示G及相應邊EG。
STEP 8
現在,所有頂點均已被選取,圖中綠色部分即為連通圖的最小生成樹。在此例中,最小生成樹的權值之和為39。
復雜度:
這里記頂點數v,邊數e
鄰接矩陣:O(v) 鄰接表:O(elog2v)
下面是代碼及注釋:
#include<cstdio> #include<iostream> #include<cstdlib> #include<iomanip> #include<cmath> #include<cstring> #include<string> #include<algorithm> #include<time.h> #include<queue> using namespace std; typedef long long ll; typedef long double ld; typedef pair<int,int> pr; const double pi=acos(-1); #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define Rep(i,u) for(int i=head[u];i;i=Next[i]) #define clr(a) memset(a,0,sizeof a) #define pb push_back #define mp make_pair #define fi first #define sc second ld eps=1e-9; ll pp=1000000007; ll inf=2147483647; #define maxn 5005 #define maxm 200005 ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;} ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;} ll read(){ ll ans=0; char last=' ',ch=getchar(); while(ch<'0' || ch>'9')last=ch,ch=getchar(); while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar(); if(last=='-')ans=-ans; return ans; } //head struct edge { int to,_dis,next;////出邊的終點、出邊的長度、下一條出邊 }edge[maxm<<1];//因為是無向圖,所以開雙倍數組,雙倍快樂 int head[maxn],dis[maxn],cnt,n,m,tot,now=1,ans; //dis數組表示當前點到最小生成樹的最短路徑 bool vis[maxn]; inline void add_edge(int from,int to,int value) { edge[++cnt].to=to; edge[cnt]._dis=value; edge[cnt].next=head[from]; head[from]=cnt; }//添加邊 inline int prim() { rep(i,2,n) dis[i]=inf;//初始化 for(int i=head[1];i;i=edge[i].next)//遍歷當前節點的每一條出邊 dis[edge[i].to]=min(dis[edge[i].to],edge[i]._dis);//重邊の處理 while(++tot<n)//就是tot<=n-1,因為最小生成樹的邊數一定等於節點數-1 { int minn=inf;//初始化min vis[now]=1;//已經到達 rep(i,1,n) if(!vis[i]&&minn>dis[i])//尋找到最小生成樹距離最短的節點 minn=dis[i],now=i;//更新 ans+=minn;//更新最小生成樹 for(int i=head[now];i;i=edge[i].next)//遍歷每一條出邊 { int to=edge[i].to; if(dis[to]>edge[i]._dis&&!vis[to]) dis[to]=edge[i]._dis;//更新dis數組 } } return ans; } int main() { n=read(),m=read(); rep(i,1,m) { int from=read(),to=read(),value=read(); add_edge(from,to,value);//因為是無向圖 add_edge(to,from,value);//雙倍存儲,雙倍快樂 } cout<<prim(); }
Kruskal算法
Kruskal算法的思想比Prim好理解一些。先把邊按照權值進行排序,用貪心的思想優先選取權值較小的邊,並依次連接,若出現環則跳過此邊(用並查集來判斷是否存在環)繼續搜,直到已經使用的邊的數量比總點數少一即可。
證明:剛剛有提到:如果某個連通圖屬於最小生成樹,那么所有從外部連接到該連通圖的邊中的一條最短的邊必然屬於最小生成樹。所以不難發現,當最小生成樹被拆分成彼此獨立的若干個連通分量的時候,所有能夠連接任意兩個連通分量的邊中的一條最短邊必然屬於最小生成樹
上面提到:這個東西要用到“並查集”
不了解的:傳送門
代碼及注釋:
#include<cstdio> #include<iostream> #include<cstdlib> #include<iomanip> #include<cmath> #include<cstring> #include<string> #include<algorithm> #include<time.h> #include<queue> using namespace std; typedef long long ll; typedef long double ld; typedef pair<int,int> pr; const double pi=acos(-1); #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define Rep(i,u) for(int i=head[u];i;i=Next[i]) #define clr(a) memset(a,0,sizeof a) #define pb push_back #define mp make_pair #define fi first #define sc second ld eps=1e-9; ll pp=1000000007; ll inf=2147483647; #define maxn 5005 #define maxm 200005 ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;} ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;} ll read(){ ll ans=0; char last=' ',ch=getchar(); while(ch<'0' || ch>'9')last=ch,ch=getchar(); while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar(); if(last=='-')ans=-ans; return ans; } //head struct Edge { int from,to,_dis; }edge[maxm]; int fa[maxn],n,m,ans,eu,ev,cnt; //father數組用來存儲父親節點 bool cmp(Edge a,Edge b) { return a._dis<b._dis;//比較函數 } inline int find_die(int x) { while(x!=fa[x]) x=fa[x]=fa[fa[x]]; return x;//找爹 } inline int kruskal() { sort(edge+1,edge+1+m,cmp);//先將所有的邊按權值排序 rep(i,1,m) { eu=find_die(edge[i].from); ev=find_die(edge[i].to);//分別找始點和終點的祖宗節點 if(eu==ev)//如果是一個祖宗,就說明他們在一個聯通圖中 continue; ans+=edge[i]._dis;//更新最小生成樹長度 fa[ev]=eu;//順便標記父親 if(++cnt==n-1)//知道生成最小生成樹 break; } return ans; } int main() { n=read(),m=read(); rep(i,1,n) fa[i]=i;//初始化自己是自己的父親 rep(i,1,m) edge[i].from=read(),edge[i].to=read(),edge[i]._dis=read(); cout<<kruskal(); }
加上判斷無解的情況:
#include<cstdio> #include<iostream> #include<cstdlib> #include<iomanip> #include<cmath> #include<cstring> #include<string> #include<algorithm> #include<time.h> #include<queue> using namespace std; typedef long long ll; typedef long double ld; typedef pair<int,int> pr; const double pi=acos(-1); #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define Rep(i,u) for(int i=head[u];i;i=Next[i]) #define clr(a) memset(a,0,sizeof a) #define pb push_back #define mp make_pair #define fi first #define sc second ld eps=1e-9; ll pp=1000000007; ll inf=2147483647; #define maxn 500001 #define maxm 1000001 ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;} ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;} ll read(){ ll ans=0; char last=' ',ch=getchar(); while(ch<'0' || ch>'9')last=ch,ch=getchar(); while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar(); if(last=='-')ans=-ans; return ans; } //head struct Edge { int from,to,_dis; }edge[maxm]; int fa[maxn],n,m,ans,eu,ev,cnt; //father數組用來存儲父親節點 bool cmp(Edge a,Edge b) { return a._dis<b._dis;//比較函數 } inline int find_die(int x) { while(x!=fa[x]) x=fa[x]=fa[fa[x]]; return x;//找爹 } int main() { n=read(),m=read(); rep(i,1,n) fa[i]=i;//初始化自己是自己的父親 rep(i,1,m) edge[i].from=read(),edge[i].to=read(),edge[i]._dis=read(); sort(edge+1,edge+1+m,cmp);//先將所有的邊按權值排序 rep(i,1,m&&cnt<=n-1) { eu=find_die(edge[i].from); ev=find_die(edge[i].to);//分別找始點和終點的祖宗節點 if(eu==ev)//如果是一個祖宗,就說明他們在一個聯通圖中 continue; ans+=edge[i]._dis;//更新最小生成樹長度 fa[ev]=eu;//順便標記父親 cnt++; } if(cnt!=n-1) cout<<"orz"; else cout<<ans; }
碼字不易,如果看的滿意請點個推薦哦~