C++:图的最小生成树

一、简介

        连通图的生成树是包含图中全部顶点的一个极小连通子图。极小的含义是包含全部顶点的连通子图中,边数最小的子图。一颗具有n个顶点的生成树,有且仅有n-1条边。显然,生成树可能不唯一。

        无向连通网的生成树上各边的权值之和称为该生成树的代价,在图的所有生成树中,代价最小的生成树称为最小生成树

        最小生成树的概念可以应用到许多实际问题中,例如,在n个城市之间建造通信网络,至少需要假设n-1条通信线路,每两个城市之间架设通信线路的造假是不一样的,如何设计才能使得总造价最小?如果用图的顶点表示城市,用边上的权值表示建造城市之间的通信线路所需的费用,则最小生成树给出了建造通信网络的最优方案。 

         本文将介绍两种求最小生成树的算法——Prim算法Kruskal算法

二、Prim算法 

         Prim算法是用来求解无向图的最小生成树的一种贪心算法

        它的基本思想是:从一个任意节点出发,逐步选择边来连接未被访问的节点,使得当前生成树的边的权值之和最小,直到所有节点都被连接为止。

        Prim算法适用于求稠密网的最小生成树,其步骤大致如下:

  1. 选择一个起始点:选择一个任意的节点作为最小生成树的初始点,可以取第一个节点(0)。
  2. 扩展树的边:从已选节点开始,选择最小的边,并将其加入生成树。
  3. 更新权值:在加入新节点后,更新现有生成树到其他未加入节点的最小边的权值。
  4. 重复以上步骤,直到所有节点都被加入生成树。
#include <iostream>
using namespace std;
#include <vector>           //使用vector要用


//图的邻接矩阵(0表示无边,非0值表示边的权重/距离)
vector<vector<int>> edge={{ 0,2,0,6,0 },
						  { 2,0,3,8,5 },
						  { 0,3,0,0,7 },
					      { 6,8,0,0,9 },
					      { 0,5,7,9,0 }};
//图的顶点个数
int vertex_num=5;

//功能:找出带最小权值的边的未访问顶点
//参数:1.权值数组;2.顶点访问状态数组
int Get_min_dist(const vector<int>& dist, const vector<bool>& visited)
{
	int min = INT_MAX, min_index(-1);
	for (int i = 0; i < vertex_num; i++)   //遍历所有顶点
	{
		if (!visited[i] && dist[i] < min)  //若该顶点未被访问且权值较小
		{
			min = dist[i];  //更新最小值
			min_index = i;  //更新最小值对应的下标
		}
	}
	return min_index;       //返回最小值对应的下标
}

//功能:求最小生成树
//参数:图的邻接矩阵
void primMST(vector<vector<int>> edge)
{
	vector<int> dist(vertex_num, INT_MAX);
	//存储每个节点到最小生成树的直接距离的最小值,初始值为INT_MAX,表示还未连接到树中

	vector<int> parent(vertex_num, -1);
	//存储每个节点的父节点,每个节点及其父节点构成最小生成树的一条边,初始值为-1

	vector<bool> visited(vertex_num, false);
	//用来标记一个节点是否已经包含在最小生成树中

	dist[0] = 0;
	//从节点0开始构建最小生成树

	//构建最小生成树
	for (int i = 0; i < vertex_num - 1; i++)     //执行n-1次循环,之后便可确定n-1条边
	{
		int u = Get_min_dist(dist, visited);
        //获得离已有生成树最近的节点的下标u
		visited[u] = true;
        //标记该节点为已访问(加入到生成树中)
		for (int j = 0; j < vertex_num; j++)     //遍历每个节点,n次循环
		{
			if (edge[u][j] && !visited[j] && edge[u][j] < dist[j])
            //若该节点和u之间有边、未被访问、距离较小
			{
				dist[j] = edge[u][j];  //更新最小距离
				parent[j] = u;         //更新最小距离对应的父节点
			}
		}
	}

	//输出结果
	int sum(0);
	cout << "edge\tweight\n";
	for (int i = 1; i < vertex_num; i++)
	{
		cout << i << "-" << parent[i] << "\t" << dist[i] << endl;
		sum += dist[i];          //将最小生成树的边累加起来
	}
	cout << "sum_dist:" << sum;  //总的最小距离,一般需要的是这个。
}

int main()
{
	primMST(edge);
    //调用求最小生成树的函数
	return 0;
}

        结果如下图所示:

 三、Kruskal算法

         Kruskal算法是求解的另一种经典贪心算法

        该算法的基本思想是:通过边的权重进行排序,然后逐步将权重最小的边加入生成树。对于每条边,算法检查是否会形成环,如果不形成环,则将这条边加入生成树。这个过程持续到生成树包含所有节点为止。

        Kruskal适用于稀疏网,其大致步骤如下:

  1. 初始化

    • 将图中所有的边按权重从小到大排序。
    • 为每个节点初始化一个独立的集合,通常使用并查集(Union-Find)来高效管理这些集合。
  2. 选择边

    • 从权重最小的边开始,检查该边的两个端点是否属于同一个集合。
    • 如果不在同一集合中,说明加入这条边不会形成环,将这条边加入生成树,并合并这两个集合。
  3. 终止条件

    • 重复以上步骤,直到生成树中包含 n-1条边(其中n是图中节点的数量)。
#include <iostream>
using namespace std;
#include <algorithm>      //使用sort()排序函数要用
#include <vector>

//边的结构体,用于表示边,包含起点、终点、权重
struct edge                       
{
	int src;
	int dest;
	int weight;
};

//并查集,用于维护每个节点的父节点和秩(rank),以便高效地进行合并操作
struct subset                     
{
	int parent;
	int rank;  //秩,后续初始为0,可以理解为等级,等级越高表示树的层数越高
			   //在树的合并时,将层数少的作为子树,并到层数高的树下
};

//自定义排序规则,按权重升序排列
bool compareEdge(edge a, edge b)  
{
	return a.weight < b.weight;
}

//用于查找某个节点的根节点,同时压缩路径以提高效率
int find(subset subsets[], int i)
{
	if (subsets[i].parent != i)   //若该节点的父节点不是它自己,即它不是根节点
	{
		subsets[i].parent = find(subsets, subsets[i].parent); //继续查找其根节点并压缩路径
	}
	return subsets[i].parent;                      //返回该节点的根节点
}

//用于合并两个节点的集合,采用按秩合并的方法
void union_set(subset subsets[], int x, int y)
{
	int xroot = find(subsets, x);                  //找到x的根节点
	int yroot = find(subsets, y);                  //找到y的根节点

	if (subsets[xroot].rank < subsets[yroot].rank) //取秩较大者的集合的根节点作为合并后的根节点
	{
		subsets[xroot].parent = yroot;
	}
	else if (subsets[xroot].rank > subsets[yroot].rank)
	{
		subsets[yroot].parent = xroot;
	}
	else                                          //若秩相同,则任取一方的根节点为合并后的根节点
	{
		subsets[yroot].parent = xroot;
		subsets[xroot].rank++;
	}
}

//Kruskal算法实现
void Kruskal(vector<edge>& edges, int vertex_num)
{
	vector<edge> result;
	//存放结果
	
	int edge_count = 0;
	//记录最小生成树已有的边,达到vertex_num-1时结束

	sort(edges.begin(), edges.end(), compareEdge);
	//将所有边按权值升序排列

	subset* subsets = new subset[vertex_num];
	for (int i = 0; i < vertex_num; i++)  //为每个节点创建并查集
	{
		subsets[i].parent = i;//每个节点初始时其根节点是它自己,因为每个节点最开始都是单独一个
		subsets[i].rank = 0;              //同时秩(可以理解为层数)也为最低
	}

	for (edge e : edges)                  //遍历每条边
	{
		int x = find(subsets, e.src);     //找到该边起点的根节点
		int y = find(subsets, e.dest);    //找到该边终点的根节点
		if (x != y)//若俩根节点不一致,即起点和终点没有都加入生成树,加入该边不形成环,可合并
		{
			result.push_back(e);          //把该边加入生成树
			union_set(subsets, x, y);     //合并
			edge_count++;                 //边数+1
		}

		if (edge_count == vertex_num - 1) //若边数已达n-1条,退出循环,结束构建
		{
			break;
		}
	}

	//输出结果
	int sum(0);
	cout << "edge\tweight\n";
	for (edge edge : result)
	{
		cout << edge.src << "-" << edge.dest << "\t" << edge.weight << endl;
		sum += edge.weight;
	}
	cout <<"sum_weight:"<< sum;           //一般是求这个,总的最小权重

	delete[] subsets;                     //释放动态内存,避免内存泄漏
}

int main()
{
    //图的边信息
	vector<edge> edges = { {0, 1, 10},
						   {0, 2, 6 },
						   {0, 3, 5 },
						   {1, 3, 15},
						   {2, 3, 4 } };

	int vertex_num = 4;                    //顶点数 

	Kruskal(edges, vertex_num);            //调用Kruskal算法求最小生成树

	return 0;
}

        结果如图所示:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值