文章目录



1 概念

1.1 定义

最小生成树:在一个给定的无向图G(V,E)中求一颗树T,使这棵树拥有图G中的所有顶点,且所有边都来自于图G中的边,并且满足整颗树的边权之和最小。

1.2 性质

  • 1 最小生成树是树,因此其边数等于其顶点树减一,且树内一定不会有环;
  • 2 对于给定的图G(V,E),其最小生成树可以不唯一,但其边权之和一定唯一;
  • 3 由于最小生成树是无向图上生成的,因此其根结点可以是这颗树上的任意结点。

2 求解最小生成树

2.1 prim算法(普里姆算法)

2.1.1 算法思想

  • 对于图G(V,E)设置集合S来存放已被访问的顶点,然后执行n次下面的两个步骤:
  • 1 每次从集合V-S中选择与集合S最近的一个顶点(记为u),访问u并将其加入集合S,同时把这条离集合S最近的边加入最小生成树中,
  • 2 令顶点u作为集合S和集合V-S连接的接口,优化从u能到达的未访问的顶点v与集合S的最短距离

2.1.2 模版伪代码

//图G一般设置为全局变量;数组d为顶点与集合S的最短距离
Prim(G, d[]){
初始化;
for (循环n次)
{
u = 使d[u]最小的海未被访问的顶点标号;
记u已被访问;
for (从u出发能到达的所有顶点v)
{
if(v未被访问&&以u为中介点使得v与集合S的最短距离d[v]更优){
将G[u][v]赋值给v与集合S的最短距离d[v];
}
}
}
}

2.1.3 代码

2.1.3.1 邻接矩阵
const int MAXV = 1000;
const int INF = 0x3fffffff;
int G[MAXV][MAXV];//图
int n;//顶点个数
bool vis[MAXV] = {false};//记录顶点是否被访问
int d[MAXV];//顶点与集合S的最短距离

int prim(){//默认0号为初始点,函数返回最小生成树的边权之和
fill(d, d + MAXV, INF);
d[0] = 0;//只有0号顶点到集合S的距离为0,其余为INF
int ans = 0;//存放最小生成树的边权之和
for (int i = 0; i < n; ++i)
{
int u = -1, min = INF;
for (int j = 0; j < n; ++j)
{
if(vis[j] == false && d[j] < min){
u = j;
min = d[j];
}
}
//找不到小于INF的d[u],则剩下的顶点和集合S不连通
if(u == -1) return -1;
vis[u] = true;
ans += d[u];//将与集合S距离最小的边加入最小生成树

for (int v = 0; v < n; ++v)
{
//v未访问 && u能到达v && 以u为中介点可以是v离集合S更近
if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]){
d[v] = G[u][v];
}
}
}

return ans;
}
2.1.3.2 邻接表
struct node
{
int v;
int dis;
};

const int MAXV = 1000;
const int INF = 0x3fffffff;
vector<node> G[MAXV];//图
int n;//顶点个数
bool vis[MAXV] = {false};//记录顶点是否被访问
int d[MAXV];//顶点与集合S的最短距离

int prim(){//默认0号为初始点,函数返回最小生成树的边权之和
fill(d, d + MAXV, INF);
d[0] = 0;//只有0号顶点到集合S的距离为0,其余为INF
int ans = 0;//存放最小生成树的边权之和
for (int i = 0; i < n; ++i)
{
int u = -1, min = INF;
for (int j = 0; j < n; ++j)
{
if(vis[j] == false && d[j] < min){
u = j;
min = d[j];
}
}
//找不到小于INF的d[u],则剩下的顶点和集合S不连通
if(u == -1) return -1;
vis[u] = true;
ans += d[u];//将与集合S距离最小的边加入最小生成树

for (int j = 0; j < G[u].size(); ++j)
{
int dis = G[u][j].dis;
int v = G[u][j].v;
//v未访问 && 以u为中介点可以是v离集合S更近
if(vis[v] == false && dis < d[v]){
d[v] = dis;
}
}
}

return ans;
}

2.1.4 注意点

  • 上面写法的算法时间复杂度为O(V2);
  • 邻接表的写法可以通过堆优化,将时间复杂度降到O(VlogV+E)
  • 尽量在顶点较少边较多的情况下(稠密图),使用prim算法

2.1.5 示例

输入:
6 10 //6个顶点,10条边。以下10行为10条边
0 1 4 //边0->1和1->0的边权为4,下同
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3
输出:
15 //最小生成树边权

#include 
#include
#include

using std::vector;
using std::fill;

struct node
{
int v;
int dis;
node(int _v, int _dis): v(_v), dis(_dis){}
};

const int MAXV = 1000;
const int INF = 0x3fffffff;
vector<node> G[MAXV];//图
int n;//顶点个数
bool vis[MAXV] = {false};//记录顶点是否被访问
int d[MAXV];//顶点与集合S的最短距离

int prim(){//默认0号为初始点,函数返回最小生成树的边权之和
fill(d, d + MAXV, INF);
d[0] = 0;//只有0号顶点到集合S的距离为0,其余为INF
int ans = 0;//存放最小生成树的边权之和
for (int i = 0; i < n; ++i)
{
int u = -1, min = INF;
for (int j = 0; j < n; ++j)
{
if(vis[j] == false && d[j] < min){
u = j;
min = d[j];
}
}
//找不到小于INF的d[u],则剩下的顶点和集合S不连通
if(u == -1) return -1;
vis[u] = true;
ans += d[u];//将与集合S距离最小的边加入最小生成树

for (int j = 0; j < G[u].size(); ++j)
{
int dis = G[u][j].dis;
int v = G[u][j].v;
//v未访问 && 以u为中介点可以是v离集合S更近
if(vis[v] == false && dis < d[v]){
d[v] = dis;
}
}
}
return ans;
}

int main(int argc, char const *argv[])
{
int m, u, v, w;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++i)
{
scanf("%d%d%d", &u, &v, &w);
G[u].push_back(node(v,w));
G[v].push_back(node(u,w));
}

int ans = prim();
printf("%d\n", ans);
return 0;
}
/*
6 10
0 1 4
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3
*/

2.2 kruskal算法(克鲁斯卡尔算法)

2.2.1基本思想

  • 在初始状态时,隐去图中所有边,这样图中每个顶点都自成一个连通块,。之后执行下面的步骤:
  • 1 对所有边按边权按从小到大排序;
  • 2 按边权从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则,将边舍弃;
  • 3 执行步骤2, 直到最小生成树中的边数等于顶点树减1或是测试完所有边时结束。而当结束时,如果最小生成树的边数小于总顶点树减1,说明该图不连通。
  • 简单说:每次选择图中最小边权的边,如果边两端的顶点在不同连通块中,则把这条边加入最小生成树中,

2.2.2 模版伪代码

  • 1 边的定义
    因为需要判断边的两个断点是否在不同的连通块中,因此边的两个端点编号一定需要;算法涉及边权,因此边权也需要。
struct edge
{
int u, v;//边的两个端点编号
int cost;//边权
};
  • 2 排序函数
    让数组E按边权从小到大排序
bool cmp(edge a, edge b){
return a.cost < b.cost;
}
  • 3 伪代码
int krukal(){
令最小生成树的边权之和为ans、最小生成树的当前边数为Num_Edge;
将所有边按边权大小排序;
for(从小到大枚举所有的边){
if(当测试的两个端点在不同连通块中){
将测试边加入最小生成树中;
ans += 测试边的边权;
最小生成树的当前边权Num_Edge加1;
当边数Num_Edge等于顶点数减1时结束循环;
}
}
return ans;
}

2.2.3 实现代码

const int MAXV = 120;//最多顶点数
const int MAXE = 1000;//最多边数
int father[MAXV];//并查集数组

struct edge
{
int u, v;//边的两个端点编号
int cost;//边权
}E[MAXE];



int findFather(int x){
int a = x;
while(x != father[x]){
x = father[x];
}

while(a != father[a]){
int z = a;
a = father[a];
father[z] = x;
}

return x;
}


bool cmp(edge a, edge b){
return a.cost < b.cost;
}

int krukal(int n, int m){
int ans = 0, Num_Edge = 0;//ans:所求边权之和;Num_Edge:当前生成树的边数
for (int i = 1; i <= n; ++i)//假设顶点范围为1~n
{
father[i] = i;//并查集初始化
}
sort(E, E + m, cmp);//所有边权从小到大排序

for (int i = 0; i < m; ++i)//枚举所有边
{
int faU = findFather(E[i].u); //查询测试边的两个端点所在集合的根结点
int faV = findFather(E[i].v);
if(faU != faV){//如果不在一个集合中
father[faU] = faV;//合并集合(把测试边加入最小生成树)
ans += E[i].cost;//边权之和加入测试边的边权
Num_Edge++;//当前生成树边数加1
if(Num_Edge == n - 1){//边数等于顶点树-1时,结束算法
break;
}
}
}
if(Num_Edge != n - 1){//无法连接时,返回-1
return -1;
}else{//返回最小生成树的边权之和
return ans;
}
}

2.2.4 注意

*时间复杂度主要在于对边进行排序,时间复杂度为O(ElogE)

  • 适合顶点较多,边数较少的情况(稀疏图)

2.2.5 示例

输入:
6 10 //6个顶点、10条边,下面跟着10条无向边
0 1 4 //0号顶点与1号顶点的无向边的边权为4
0 4 1
0 5 2
1 2 1
1 5 3
2 3 6
2 5 5
3 4 5
3 5 4
4 5 3
输出:
11 //最小生成树的边权之和

#include 
#include

using std::sort;

const int MAXV = 110;
const int MAXE = 10000;
int father[MAXV];

struct edge
{
int u, v;
int cost;
}E[MAXE];

bool cmp(edge a, edge b){
return a.cost < b.cost;
}

int findFather(int x){
int a = x;
while(x != father[x]){
x = father[x];
}

while(a != father[a]){
int z = a;
a = father[a];
father[z] = x;
}

return x;
}


int kruskal(int n, int m){
int ans = 0, Num_Edge = 0;
for (int i = 0; i < n; ++i)
{
father[i] = i;
}

sort(E, E + m, cmp);
for (int i = 0; i < m; ++i)
{
int faU = findFather(E[i].u);
int faV = findFather(E[i].v);
if(faU != faV){
father[faU] = faV;
ans += E[i].cost;
Num_Edge++;
if(Num_Edge == n - 1){
break;
}
}
}
if(Num_Edge != n - 1){
return -1;
}else {
return ans;
}
}

int main(int argc, char const *argv[])
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++i)
{
scanf("%d%d%d", &E[i].u, &E[i].v, &E[i].cost);
}
int ans = kruskal(n, m);
printf("%d\n", ans);
return 0;
}

/*
6 10
0 1 4
0 4 1
0 5 2
1 2 1
1 5 3
2 3 6
2 5 5
3 4 5
3 5 4
4 5 3
*/