第七章 圖(最小生成樹之prime演算法和 kruskal演算法)
阿新 • • 發佈:2019-02-16
最小生成樹
所謂最小生成樹,就是在一個具有N個頂點的帶權連通圖G中,如果存在某個子圖G’,其包含了圖G中的所有頂點和一部分邊,且不形成迴路,並且子圖G’的各邊權值之和最小,則稱G’為圖G的最小生成樹。
由定義我們可得知最小生成樹的三個性質:
•最小生成樹不能有迴路。
•最小生成樹可能是一個,也可能是多個。
•最小生成樹邊的個數等於頂點的個數減一。
prime演算法
以下面的圖為例,實現prime演算法
#include<iostream>
#include<cstring>
using namespace std;
const int inf=10000;
int graph[4][4]={
{inf,6,1,inf},
{6,inf,5,3},
{1,5,inf,4},
{inf,3,4,inf}
};
bool vis[4];
int m=4;
int dis[100];
int prime(int cur)
{
int index;
int sum = 0;
memset(vis, false, sizeof(vis));
vis[cur] = true;//最小生成樹的起點標記訪問過
for(int i = 0; i < m; i ++){
dis[i] = graph[cur][i];
}
//cur點已經訪問過,剩下的結點數目是m-1
for(int i = 1; i < m; i ++){
int minmum=inf;
for(int j = 0; j < m; j ++){
if(!vis[j] && dis[j] < minmum){
minmum = dis[j];
index = j;
}
}
vis[index] = true ;
sum += minmum;//求最小生成樹的最小邊權之和
//更新dis陣列,對index點來說,與之相連的邊和對應的以前的邊
//比較,選擇較短的邊。比如開始和點2相連的所有邊是6,inf,5,3
//index更新為點4後,與4相連的所有邊是inf,3,4,inf,原來的dis
//是6,inf,5,3更新成6,3,4,3
for(int j = 0; j < m; j ++){
if(!vis[j] && dis[j] > graph[index][j]){
dis[j] = graph[index][j];//更新dis陣列
}
}
}
return sum;
}
int main(){
cout<<prime(1)<<endl;
return 0;
}
kruskal演算法
講kruskal演算法之前,先講並查集
並查集
並查集根據其名字幹兩件事:
1.把元素a所在的集合和元素b所在的集合合併為一個集合。
2.查元素a和元素b是否屬於同一個集合。
下面給出程式碼(含有解釋),該程式碼合併集合的方法是根據樹的深度,把深度小的樹(集合)合併到深度大的(集合)上,如下示例。
/*
並查集思路。
並查集演算法的思想,結合了樹的思想。
剛開始給出n個元素,這n個元素可以看成n棵樹。對於其中一個元素a,
如果另一個元素b和a屬於同一個集合,那麼我們可以把b當做a的父母,
或者把a當做b的父母(起初,每個元素的父母是他自己),如果元素c
和a,b仍然屬於同一個集合,那麼我們可以把a,b中任一個當做c的父母,
或者把c當做a或b的父母,以此類推。這樣集合用一棵樹表示了。不同
的集合表示成了不同棵樹。如果我們想把不同集合合併,我們只需要將
一個集合中任一個元素當做另一個集合的父母即可(即在兩棵樹直接添
加了一條邊,把兩棵樹連在了一起)。如果我們想查兩個元素d,e是否
屬於同一個集合,我們只需要一直遞歸向上查d,e的父母(即他們分別
所屬的樹根)(請記得起初的時候每個元素的父母是他自己,遞迴到最
後一定能查出一個值),如果他們的父母相同,則他們屬於同一個集合,
否則,不屬於同一個集合。
上面的描述中,還有一點補充,給每一個元素定個級別,剛開始每個元
素的級別是1,表明以該元素為根的樹深為1。
*/
/*
本程式例子是,包含6個結點,分別是1,2,3,4,5,6。7其中1,2,
3,7屬於一個集合,4,5屬於一個集合。6屬於一個集合
*/
#include <iostream>
#include <cstring>
#include<algorithm>
using namespace std;
const int maxn = 10000;
int par[maxn]; //結點的父母
int r[maxn]; //每個結點的等級,即以該結點為根的樹的深度
//初始化n個元素,父母是他自己,等級(樹深)初始化為1
void init(int n)
{
for (int i = 1; i <=n; i++) {
par[i] = i;
r[i] = 1;
}
}
//查詢樹根
int find_root(int x) {
if (par[x] == x) {
return x;
}
else {
return find_root(par[x]);
}
}
//合併x和y所屬集合
void united(int x, int y) {
x = find_root(x);
y = find_root(y);
if (x == y) return;//屬於同一棵樹,無需進行合併
//深度淺的樹合併到深度大的樹上
if (r[x]<r[y]) {
par[x] = y;r[y]=max(r[x]+1,r[y]);
} else {
par[y] = x;r[x]=max(r[x],r[y]+1);
}
}
//判斷x和y是否屬於同一個集合
bool same_set(int x, int y) {
return find_root(x) == find_root(y);
}
int main()
{
int n,m;
cout<<"請輸入元素的總數: ";cin>>n;init(n);
cout<<"請輸入集合的總數: ";cin>>m;
cout<<"輸入每個集合中的任意兩個元素(最多情況數為該集合元素數減1):"<<endl;
//n個元素構成m個集合,最多隻需輸入n-m對元素即可完成所有關係建立
//比如1,2,3,7為一個集合,4,5為一個集合,6為一個集合,7個元素
//形成3個集合,只需要輸入1、2,1、3,4、5這三對(<=7-3)即可
int e,p;
for(int i=1;i<=n-m;i++){
cin>>e>>p;
p=find_root(p);
par[e]=p; //路徑壓縮,e的父母直接變為p的樹根
r[p]=2; //由於建立的樹只有兩層,孩子層深度為1,父母層深度為2
//這樣建立起來的樹只有兩層,一個父母,眾多個孩子
}
cout << "輸入兩個元素,把這兩個元素所在的集合合併: ";
int e1,e2;
cin>>e1>>e2;
united(e1, e2);//把e1,e2所在的兩個集合合併
cout << "輸入兩個元素,查詢是否屬於一個集合: ";
cin >> e1 >> e2;
if(same_set(e1,e2)) cout<<"same"<<endl;
else cout<<"different"<<endl;
return 0;
}
/*
本程式樣例輸入輸出:
請輸入元素的總數: 7
請輸入集合的總數: 3
輸入每個集合中的任意兩個元素(最多情況數為該集合元素數減1):
1 2
2 3
4 5
6 6
輸入兩個元素,把這兩個元素所在的集合合併: 2 5
輸入兩個元素,查詢是否屬於一個集合: 1 4
same
*/
構造一個只含n個頂點,而邊集為空的子圖,若將該子圖中各個頂點看成是各棵樹的根節點,則它是一個含有n棵樹的森林 。之後,從網的邊集中選取一條權值最小的邊,若該邊的兩個頂點分屬不同的樹 ,則將其加入子圖,也就是這兩個頂點分別所在的 兩棵樹合成一棵樹;反之,若該邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。依次類推,直至森林只有一棵樹。kruskal演算法能夠在並查集的基礎很快的實現。
以下面的圖為例,實現kruskal演算法
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=10000;
int par[maxn];//結點的父母,用並查集
int r[maxn];//每個結點等級(以該結點為樹根的樹深),用並查集
typedef struct{
int st;//一條邊的起始結點
int en;//一條邊的結束結點
int w;//邊權重
}edge_node;
edge_node edge[maxn/2*maxn];
//並查集演算法的初始化
void init(int n){
for(int i=1;i<=n;i++){
par[i]=i;
r[i]=1;
}
}
//並查集演算法的找樹根
int find_root(int x){
if(x==par[x]) return x;
else return find_root(par[x]);
}
//並查集演算法的合併集合
void united(int x,int y){
x=find_root(x);
y=find_root(y);
if(x==y) return;
if(r[x]<r[y]) {par[x]=y;r[y]=r[x]+1;}
else{par[y]=x;r[x]=r[y]+1;}
}
bool cmp(edge_node e1,edge_node e2){
return e1.w<e2.w;
}
int kruskal(int n,edge_node edge[]){
int sum=0;
init(n);
sort(edge+1,edge+n+1,cmp);
for(int i=1;i<=n;i++){
if(find_root(edge[i].st)!=(find_root(edge[i].en))){
united(edge[i].st,edge[i].en);
sum+=edge[i].w;
}
}
return sum;
}
int main(){
int n;
cout<<"請輸入邊的條數: ";cin>>n;
int x,y,w;
for(int i=1;i<=n;i++){
cout<<"請輸入第"<<i<<"條邊的兩個頂點及權重: "<<endl;
cin>>x>>y>>w;
edge[i].st=x;edge[i].en=y;edge[i].w=w;
}
cout<<"最小生成樹的邊權之和: ";
cout<<kruskal(n,edge)<<endl;
return 0;
}
/*
輸入輸出樣例:
請輸入邊的條數: 5
請輸入第1條邊的兩個頂點及權重:
1 2 6
請輸入第2條邊的兩個頂點及權重:
1 3 1
請輸入第3條邊的兩個頂點及權重:
2 3 5
請輸入第4條邊的兩個頂點及權重:
2 4 3
請輸入第5條邊的兩個頂點及權重:
3 4 4
最小生成樹的邊權之和: 8
*/