图论算法:最小生成树——Prim算法和Kruskal算法C++实现

前言:

        这次我记录的是另外一种很有意思的东西——最小生成树。相信学过离散数学的朋友都对这种东西不陌生,那么在代码中,我们该如何实现它呢?接下来,我将对此讲解一些自己的心得。

        开头让我插入一个Github传送门:JZX555的Github

原理:

      首先让我们看看什么是最小生成树—— 一个有n个节点的连通图的生成树是原图的极小连通子图,且包含原图中的所有n个节点,并且有保持图连通的最少的边。这是《数据结构与算法分析》中对于最小生成树的描述,用更简便的话来说,就是一个图的每个节点都被链接起来,但是如果 任意去掉一个边那么这个图就不是连通的。如果每条边上还有权重的话,那么最小生成树还需要满足所有边的权重之和最少这个条件。因此,最小生成树也是 最小权重生成树的简称。
        而要实现最小生成树我们一般采用Prim算法或者是Kruskal算法来实现,接下来,将对这两种算法进行说明。
        注意:最小生成树中的边数为节点数减一,即|V| - 1;

C++实现:

1.Prim算法:
   Prim算法的原理是在开始时选取一个节点最为其实节点,然后遍历所有该节点链接的边,并选取其中权重最小的边,同时将已经选取的节点标位已知;接下来重复之前的步骤,在已知节点的链接的边中选取权重最小的边,同时将其所链接的边标位已知,一直这样操作,直到所有的节点都已知。 (PS:在实际代码中,我们使用一个表格来储存生成树的信息)
      伪代码如下:
void Prim(Node Start) { 
    Node v, w; v = Start; 
    v is known;

    while(have node not be retrieved) { 
    // 更新边信息 
    for each w adjacent to v 
        update information v--w; 
    // 寻找最小权重的边 
    find the least weighted edge v--w && w is unknown;
    w is known; 
    }
}

看看伪代码其实还是很简单的吧。
2.Kruskal算法:
        Kruskal算法的原理其实和Prim算法有很多类似的地方,不过与Prim不同,Kruskal算法是在所有的边中选取权重最小的不构成圈的边,直到选取的边数为节点数减一(即VexNum - 1),这样我们就可以构成一个最生成树了;其中我们需要注意的是如何判断一个节点是否与另外一个节点链接,这里我们就可以使用Union/Find算法进行判断;而对于最小边的选取,我们可以使用优先队列来完成。
        注:Union/Find算法,即不相交集算法,用于判断元素是否在同一个集合中;其中Find(x)返回元素x所在的集合,Set(x, y)将x,y所在集合链接在一起。具体可以参考我的另外一篇博客哦~~
        伪代码如下: 
 
  
void Kruskal() {
    int EdgeAccepted = 0;
    PriorityQueue H;
    Disjoint_Set S;
    Node v, w;
    SetType vSet, wSet;
    Edge e;
    
    Read Edges into H;

    while(EdgeAccepted < VexNum) {
        // 获取权重最小的边
        E = DeleteMin(H);
        vSet = S.Find(v), wSet = S.Find(w);
        // 判断两个顶点是否链接
        if(vSet != wSet) {
            S.Set(v, w);
            EdgeAccepted++;
        }
    }
}

完整代码:

        最后,就是我们的完整代码了,首先是.h文件:

#ifndef ALGRAPH_H
#define ALGRAPH_H

#include 
#include 
#include 
#include "Disjoint_Set.h"
using namespace std;

// 重定义边节点,便于操作
typedef struct ArcNode *Position;

/* 边节点 
 * 储存元素:
 * AdjName:该有向边的尾结点
 * Weight:该有向边的权重
 * Next:该有向边的头结点的其他有向边
 */
struct ArcNode {
	int AdjName;
	int Weight;
	Position Next;

};

/* 边节点:(用于KrusKal算法)
 * 储存元素:
 * Vex1:该无向边的节点
 * Vex2:该无向边的另一个节点
 * Weight:该无向边的权重
 */
struct Edge {
	int Vex1;
	int Vex2;
	int Weight;

	// 重载“<”符号,用于KrusKal算法
	friend bool operator < (const Edge &a, const Edge &b) {
		return a.Weight > b.Weight;
	}
};

/* 顶点节点
 * 储存元素:
 * Name:该顶点的名称
 * FirstArc:以该顶点为头结点的第一个有向边
 */
struct VexNode {
	int Name;
	Position FirstArc;
};

/* 表节点
 * 储存元素:
 * Known:该节点的状态
 * Dist:该节点距指定节点的距离
 * Path:指向该节点的节点
 */
struct TableNode {
	bool Known;
	int Dist;
	int Path;
};

/* ALGraph类
 * 接口:
 * Display:展示功能,展示距离表中的信息
 * MakeEmpty:重置功能,重置该图中的所有边
 * Creat:构建功能,在图中构建边
 * KrusKal:KrusKal算法,在图中寻找最小生成树
 * Prim:Prim算法,在图中寻找最小生成树
 */
class ALGraph
{
public:
	// 构造函数
	ALGraph(int = 10);
	// 析构函数
	~ALGraph();

//--------- 接口函数------------

	// 基础函数
	void Display();
	void MakeEmpty();
	void Creat();

	// 最小生成树函数
	void KrusKal();
	void Prim();

//-------------------------------

private:
	// 辅助函数
	void InitTable();

	// 数据成员
	int VexNum; // 储存顶点数
	int ArcNum; // 储存边数
	VexNode *AdjList; // 储存邻接表
	TableNode *Table; // 储存距离表
	priority_queue  H; // 储存优先队列(用于KrusKal算法)
};

#endif

接下来是.cpp文件:


#include "stdafx.h"
#include "ALGraph.h"

/* 构造函数:初始化对象
 * 返回值:无
 * 参数:vnum:图中的顶点数
 */
ALGraph::ALGraph(int vnum)
: VexNum(vnum), ArcNum(0){
	// 申请邻接表储存空间
	AdjList = new VexNode[VexNum + 1];
	// 申请距离表储存空间
	Table = new TableNode[VexNum + 1];

	// 判断是否申请成功
	if (AdjList == NULL || Table == NULL) {
		cout << "邻接表申请失败!" << endl;
		return;
	}

	// 初始化邻接表以及距离表
	for (int i = 0; i < VexNum + 1; i++) {
		AdjList[i].FirstArc = NULL;
		AdjList[i].Name = i;
		Table[i].Dist = INT_MAX;
		Table[i].Known = false;
		Table[i].Path = 0;
	}
}

/* 析构函数:对象消亡时回收储存空间
 * 返回值:无
 * 参数:无
 */
ALGraph::~ALGraph()
{
	// 重置所有的边
	MakeEmpty();

	// 删除邻接表
	delete AdjList;
	AdjList = NULL;
	
	// 删除距离表
	delete Table;
	Table = NULL;
}

/* 重置函数:重置所有的边
 * 返回值:无
 * 参数:无
 */
void ALGraph::MakeEmpty() {
	// 暂时储存中间节点
	Position P;

	// 遍历邻接表
	for (int i = 1; i < VexNum + 1; i++) {
		P = AdjList[i].FirstArc;

		// 遍历所有链接的边
		while (P != NULL) {
			AdjList[i].FirstArc = P->Next;
			delete P;
			P = AdjList[i].FirstArc;
		}
	}

	// 边数初始化
	ArcNum = 0;
}

/* 构建函数:在图中构建有向边
 * 返回值:无
 * 参数:无
 */
void ALGraph::Creat() {
	int tmp; // 储存边数
	cout << "请输入要创建的无向边数: ";
	cin >> tmp;
	ArcNum += tmp;

	// 创建所有的无向边
	for (int i = 0; i < tmp; i++) {
		// v:边的顶点
		// w:边的另一个顶点
		// weight:边的权重
		int v, w, weight;
		cout << "请输入要创建的无向边(v, w): ";
		cin >> v >> w;
		cout << "请输入该无向边的权重: ";
		cin >> weight;

		// 构建一个单向边
		Position P1 = new ArcNode();
		P1->AdjName = w;
		P1->Weight = weight;
		P1->Next = AdjList[v].FirstArc;
		AdjList[v].FirstArc = P1;

		// 构建另一个单向边
		Position P2 = new ArcNode();
		P2->AdjName = v;
		P2->Weight = weight;
		P2->Next = AdjList[w].FirstArc;
		AdjList[w].FirstArc = P2;

		// 构建一个无向边(用于KrusKal算法)
		Edge E;
		E.Vex1 = v;
		E.Vex2 = w;
		E.Weight = weight;

		// 将该无向边压入优先队列
		H.push(E);
	}
}

/* 初始化函数:初始化距离表
 * 返回值:无
 * 参数:无
 */
void ALGraph::InitTable() {
	// 遍历所有的距离表
	for (int i = 0; i < VexNum + 1; i++) {
		// 初始化参数
		Table[i].Dist = INT_MAX;
		Table[i].Known = false;
		Table[i].Path = 0;
	}
}

/* 展示函数:展示距离表中的信息
 * 返回值:无
 * 参数:无
 */
void ALGraph::Display() {
	// 遍历所有的距离表
	for (int i = 1; i < VexNum + 1; i++) {
		cout << "节点: " << i << " ,   距离: " << Table[i].Dist << " ,   路径: " << Table[i].Path << endl;
	}
}

/* KrusKal算法:寻找最小生成树
 * 返回值:无
 * 参数:无
 */
void ALGraph::KrusKal() {
	// S:不相交集,用于判断两个顶点是否链接
	// v:一条无向边的顶点
	// w:一条无向边的另一个顶点
	// EdgeAccepted:已经生成的边数
	// V:顶点v所在的集合
	// W:顶点w所在的集合
	// E:无向边节点
	Disjoint_Set S(VexNum);
	int v, w, EdgeAccepted = 0;
	SetType V, W;
	Edge E;

	// 初始化距离表
	InitTable();

	// 一直寻找直到生成的边数为:VexNum - 1
	while (EdgeAccepted < VexNum - 1) {
		// 获取最小的无向边
		E = H.top();
		H.pop();

		// 获取无向边的顶点
		v = E.Vex1;
		w = E.Vex2;

		// 获取顶点所在的集合
		V = S.Find(v);
		W = S.Find(w);

		// 判断两个顶点是否在同一个集合
		if (V != W) {
			// 生成边数增加
			EdgeAccepted++;
			// 链接量顶点
			S.SetUnion(V, W);
			// 输出信息
			cout << "顶点1: " << E.Vex1 << " ,   顶点2: " << E.Vex2 << " ,   距离: " << E.Weight << endl;;
		}
	}
}

/* Prim算法:寻找最小生成树
 * 返回值:无
 * 参数:无
 */
void ALGraph::Prim() {
	// v:一条无向边的顶点
	// w:一条无向边的另一个顶点
	// counter:计数器,统计边数
	// P:储存边节点
	int v, w, counter;
	Position P;

	// 初始化距离表,并选取起始节点
	InitTable();
	v = 1;
	counter = 1;
	Table[v].Dist = 0;
	Table[v].Known = true;

	// 一直寻找,直到边数为:VexNum - 1
	while (counter != VexNum) {
		// 获取边节点
		P = AdjList[v].FirstArc;

		// 遍历完该节点所有无向边
		while (P != NULL) {
			w = P->AdjName;
			// 判断是否需要更新距离表信息
			if (Table[w].Known == false && Table[w].Dist > P->Weight) {
				Table[w].Dist = P->Weight;
				Table[w].Path = v;
			}
			// 指向下一个无向边
			P = P->Next;
		}

		// 寻找不构成圈的权重最小的无向边
		int Min = INT_MAX;
		for (int i = 1; i < VexNum + 1; i++) {
			if (Table[i].Known == false && Min > Table[i].Dist) {
				v = i;
				Min = Table[i].Dist;
			}
		}

		// 链接该无向边,计数器增加
		Table[v].Known = true;
		counter++;
	}
}

        最后,我是用的邻接表的方法来实现的图的链接,所以可能与有些朋友的有所不同;如果大家还对最小生成树有什么疑问,可以留言问我,我也会第一时间回复大家的!
         PS:代码中的Union/Find算法是我自己写的,所以可能会和标准库中的有所不同,可以根据自己的需求更改。

参考文献:《数据结构与算法分析——C语言描述》

你可能感兴趣的:(数据结构)