【C++从0到王者】第四十七站:最小生成树

文章目录

  • 一、最小生成树的概念
    • 1.概念
    • 2.最小生成树的构造方法
  • 二、Kruskal算法
    • 1.算法思想
    • 2.代码实现
  • 三、Prim算法
    • 1.算法思想
    • 2.代码实现
    • 3.试试所有节点为起始点

一、最小生成树的概念

1.概念

  • 连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
  • 生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。即最少边连通起来
  • 最小生成树:构成生成树这些边加起来权值是最小的。最小的成本让这N个顶点连通

2.最小生成树的构造方法

  • 连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
  • 若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:
  1. 只能使用图中的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路
  • 构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略
  • 贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解

二、Kruskal算法

1.算法思想

任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量,其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中如此重复,直到所有顶点在同一个连通分量上为止。
核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

image-20240219201709539

2.代码实现

如下是我们的代码实现

namespace matrix
{//V代表顶点, W是weight代表权值,MAX_W代表权值的最大值,Direction代表是有向图还是无向图,flase表示无向template<class V, class W, W Max_W = INT_MAX, bool Direction = false>class Graph{typedef Graph<V, W, Max_W, Direction> Self;public:Graph() = default;//图的创建//1. IO输入 不方便测试//2. 图结构关系写到文件,读取文件//3. 手动添加边Graph(const V* a, size_t n){_vertexs.reserve(n);for (size_t i = 0; i < n; i++){_vertexs.push_back(a[i]);_indexMap[a[i]] = i;}_matrix.resize(n);for (size_t i = 0; i < _matrix.size(); i++){_matrix[i].resize(n, Max_W);}}size_t GetVertexIndex(const V& v){//return _indexMap[v];auto it = _indexMap.find(v);if (it != _indexMap.end()){return it->second;}else{//assert(false)throw invalid_argument("顶点不存在");return -1;}}void _AddEdge(size_t srci, size_t dsti, const W& w){_matrix[srci][dsti] = w;if (Direction == false){_matrix[dsti][srci] = w;}}void AddEdge(const V& src, const V& dst, const W& w){size_t srci = GetVertexIndex(src);size_t dsti = GetVertexIndex(dst);_AddEdge(srci, dsti, w);}void Print(){for (size_t i = 0; i < _vertexs.size(); i++){cout << "[" << i << "]" << "->" << _vertexs[i] << endl;}cout << endl;cout << "   ";for (int i = 0; i < _vertexs.size(); i++){//cout << _vertexs[i] << " ";printf("%-3d ", i);}cout << endl;for (size_t i = 0; i < _matrix.size(); i++){//cout << _vertexs[i] << " ";printf("%d ", i);for (size_t j = 0; j < _matrix[i].size(); j++){if (_matrix[i][j] == INT_MAX){cout << " *  ";}else{printf(" %d  ", _matrix[i][j]);//cout << _matrix[i][j] << " ";}}cout << endl;}}void BFS(const V& src){int srci = GetVertexIndex(src);queue<int> q; //广度遍历的队列vector<bool> visited(_vertexs.size(), false); //标记数组q.push(srci); //起点入队visited[srci] = true; //已经被遍历过了while (!q.empty()){int front = q.front();q.pop();cout << front << ":" << _vertexs[front] << endl;//把front顶点的邻接顶点入队列for (size_t i = 0; i < _matrix[front].size(); i++){if (_matrix[front][i] != Max_W){if (visited[i] == false){q.push(i);visited[i] = true;}}}}} void BFSLevel(const V& src){int srci = GetVertexIndex(src);queue<int> q; //广度遍历的队列vector<bool> visited(_vertexs.size(), false); //标记数组q.push(srci); //起点入队visited[srci] = true; //已经被遍历过了int levelSize = 1;while (!q.empty()){for (int i = 0; i < levelSize; i++){int front = q.front();q.pop();cout << front << ":" << _vertexs[front] << " ";//把front顶点的邻接顶点入队列for (size_t i = 0; i < _matrix[front].size(); i++){if (_matrix[front][i] != Max_W){if (visited[i] == false){q.push(i);visited[i] = true;}}}}cout << endl;levelSize = q.size();}}void _DFS(size_t srci, vector<bool>& visited){cout << srci << ":" << _vertexs[srci] << endl;visited[srci] = true;for (int i = 0; i < _matrix[srci].size(); i++){if (_matrix[srci][i] != Max_W && visited[i] == false){_DFS(i, visited);}}}void DFS(const V& src){int srci = GetVertexIndex(src);vector<bool> visited(_vertexs.size(), false);_DFS(srci, visited);}struct Edge{int _srci;int _dsti;W _w;Edge(int srci, int dsti, W w):_srci(srci),_dsti(dsti),_w(w){}bool operator>(const Edge& e) const{return this->_w > e._w;}};//传入的是一个只有结点的,没有边的图W Kruskal(Self& minTree){//先将所有的边,按照小堆的方式进行组织起来priority_queue<Edge, vector<Edge>, greater<Edge>> minque;size_t n = _vertexs.size();for (int i = 0; i < n; i++){for (int j = 0; j < n; j++){//由于这里是无向图,他是一个对称矩阵,但是我们的边只考虑一半就已经可以了。剩下的就重复了。if (i < j && _matrix[i][j] != Max_W){//已经按照自身的,带有边的图,将所有的边的信息全部组织好了minque.push(Edge(i, j, _matrix[i][j]));}}}//因为最小生成树一定是n-1条边,所以我们现在要选出n-1条边,size是计数器int size = 0;//用于计算权值W totalW = W();//最关键的问题是判环,这里我们可以用并查集去检测是否这两个顶点在一个集合里面,如果在集合里面,说明一定是连通的,在加上就成环了UnionFindSet ufs(n);//开始选边,我们要考虑到所有的边while (!minque.empty()){//取出一个最小的边,然后就可以将他踢出优先级队列了,如果被选中不需要它了,如果没有被选中,那只能是因为出现环了才不要它了。Edge min = minque.top();minque.pop();//看看是否在一个集合里面,如果在一个集合里面,那么他们已经是连通了,没必要在连通,还想要连通那么一定是环!if (!ufs.InSet(min._srci, min._dsti)){//我们可以看看我们选出来的边cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;//该边是符合的,我们直接为这个图加上边minTree._AddEdge(min._srci, min._dsti, min._w);//加上之后,就连通了,我们让他们形成集合ufs.Union(min._srci, min._dsti);//我们一定只有n-1条边,我们需要计数++size;//将总的权值要加起来totalW += min._w;}//成环的情况,我们只是看看这是哪条边else{cout << "构成环啦!:";cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;}}//上面的循环中,如果图是连通的,那么最终一定选出来的是n-1条边。除非图是不连通的。if (size == n - 1){return totalW;}//图不连通,直接返回0else{return W();}}private:vector<V> _vertexs; //顶点集合map<V, int> _indexMap; //顶点对应的下标关系vector<vector<W>> _matrix; //临界矩阵};void TestGraph(){Graph<char, int, INT_MAX, true> g("0123", 4);g.AddEdge('0', '1', 1);g.AddEdge('0', '3', 4);g.AddEdge('1', '3', 2);g.AddEdge('1', '2', 9);g.AddEdge('2', '3', 8);g.AddEdge('2', '1', 5);g.AddEdge('2', '0', 3);g.AddEdge('3', '2', 6);g.Print();}void TestGraphBDFS(){string a[] = { "张三", "李四", "王五", "赵六", "周七" };Graph<string, int> g1(a, sizeof(a) / sizeof(string));g1.AddEdge("张三", "李四", 100);g1.AddEdge("张三", "王五", 200);g1.AddEdge("王五", "赵六", 30);g1.AddEdge("王五", "周七", 30);g1.Print();g1.BFS("张三");cout << endl;g1.BFSLevel("张三");cout << endl;g1.DFS("张三");}void TestGraphMinTree(){const char* str = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);//g.AddEdge('a', 'h', 9);g.AddEdge('b', 'c', 8);g.AddEdge('b', 'h', 11); g.AddEdge('c', 'i', 2);g.AddEdge('c', 'f', 4);g.AddEdge('c', 'd', 7);g.AddEdge('d', 'f', 14);g.AddEdge('d', 'e', 9);g.AddEdge('e', 'f', 10);g.AddEdge('f', 'g', 2);g.AddEdge('g', 'h', 1);g.AddEdge('g', 'i', 6);g.AddEdge('h', 'i', 7);Graph<char, int> kminTree(str, strlen(str));cout << "Kruskal:" << g.Kruskal(kminTree) << endl;kminTree.Print();}
}

上面的测试用例与前面的图是一样的。

Kruskal实现的具体步骤为:

  • 首先对于参数,我们需要传入一个只有孤立的结点的图,这个我们可以在外部就创建好。

  • 对于Kruskal算法的核心步骤就是要每次选出最小的边然后加上去。我们这里可以分别来实现,先将所有的边按照权值从小到大进行排列。由于我们只关心最小的一个,不关心后面的权值,所以我们这里采用priority_queue是非常适合这个场景的。所以我们的第一个目标就是将所有的边,全部放入一个优先级队列中,对于这个边,我们完全可以用一个内部类,将边的起点和终点以及权值给形成一个类,然后让优先级队列放的就是这个边的类。然后就让这个优先级队列按照这个边的权值进行排列。注意我们这里还需要写一个仿函数,或者使用greater仿函数,因为优先级队列默认是大堆,而我们要用小堆。不过要用到greater还需要注意的是,要进行运算符重载。

至此,我们就完成了对边的排序

  • 有了对边的排序以后,我们就需要选边了。从哪里选呢?就从我们的优先级队列里面一个一个的去选!。只要优先级队列不为空就一直选下去。每次都选堆顶的边,选好之后,有可能因为选了它以后构成环了,所以我们就需要进行判断了。这里我们可以用并查集去判环,并查集里的规则是如果是连通的就让他们处于一个集合。不连通的就不处于一个集合。这样一来,如果我们新添加的边,发现已经是在一个集合里面了,也就是已经连通了,就不能在把这条边加上去了,因为再加就有两条通路了,那么就是环了。

  • 如果这条边我们发现加上去以后不构成环,那么就将这条边给加上去即可,由于我们之前写的加边的函数是用顶点的类型V的,而不是下标,这里我们最好把这个加边的函数给修改一下,搞一个子函数,可以直接用下标去添加边。然后我们就加上边了以后,那么这两个顶点就一定连通了,而且可能也会导致其他的顶点连通了,不过不要慌,因为有并查集在,我们直接让连通的顶点处于一个并查集就好了,所以接下来的操作就是让顶点处于一个并查集。

  • 加好了边以后,我们知道,最小生成树一定只有n-1条边,所以我们可以在加边的时候设置一个计数器,记录好边的数量。并且我们还可以去顺便在加边的时候将所有的权值给加起来。

  • 经过上面的操作,如果原来的图是连通的,那么最终一定是n-1条边。这时候,我们直接返回总权值即可。但是如果原来的图不连通,那么就一定不是n-1条边了。这时候就无法生成最小生成树,我们直接返回0即可

  • 注意,上面的循环中最终选出来的边数一定是小于等于n-1的,因为大于的情况,一定是环,一个图不构成环的最大边数就是顶点的个数减一。而环的情况,早已被并查集给处理掉了。至于小于n-1的情况,那是因为原来的图中一定是不连通的,这就导致了,最终形成的最小生成树一定会缺失大于等于1条边的数量使之无法连通。而最小生成树一定是连通的。

上面代码的运行结果为

image-20240219204951805

三、Prim算法

1.算法思想

image-20240219211757580

2.代码实现

namespace matrix
{//V代表顶点, W是weight代表权值,MAX_W代表权值的最大值,Direction代表是有向图还是无向图,flase表示无向template<class V, class W, W Max_W = INT_MAX, bool Direction = false>class Graph{typedef Graph<V, W, Max_W, Direction> Self;public:Graph() = default;//图的创建//1. IO输入 不方便测试//2. 图结构关系写到文件,读取文件//3. 手动添加边Graph(const V* a, size_t n){_vertexs.reserve(n);for (size_t i = 0; i < n; i++){_vertexs.push_back(a[i]);_indexMap[a[i]] = i;}_matrix.resize(n);for (size_t i = 0; i < _matrix.size(); i++){_matrix[i].resize(n, Max_W);}}size_t GetVertexIndex(const V& v){//return _indexMap[v];auto it = _indexMap.find(v);if (it != _indexMap.end()){return it->second;}else{//assert(false)throw invalid_argument("顶点不存在");return -1;}}void _AddEdge(size_t srci, size_t dsti, const W& w){_matrix[srci][dsti] = w;if (Direction == false){_matrix[dsti][srci] = w;}}void AddEdge(const V& src, const V& dst, const W& w){size_t srci = GetVertexIndex(src);size_t dsti = GetVertexIndex(dst);_AddEdge(srci, dsti, w);}void Print(){for (size_t i = 0; i < _vertexs.size(); i++){cout << "[" << i << "]" << "->" << _vertexs[i] << endl;}cout << endl;cout << "   ";for (int i = 0; i < _vertexs.size(); i++){//cout << _vertexs[i] << " ";printf("%-3d ", i);}cout << endl;for (size_t i = 0; i < _matrix.size(); i++){//cout << _vertexs[i] << " ";printf("%d ", i);for (size_t j = 0; j < _matrix[i].size(); j++){if (_matrix[i][j] == INT_MAX){cout << " *  ";}else{printf(" %d  ", _matrix[i][j]);//cout << _matrix[i][j] << " ";}}cout << endl;}for (size_t i = 0; i < _matrix.size(); i++){for (size_t j = 0; j < _matrix[i].size(); j++){if (i < j && _matrix[i][j] != Max_W){cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl;}}}}void BFS(const V& src){int srci = GetVertexIndex(src);queue<int> q; //广度遍历的队列vector<bool> visited(_vertexs.size(), false); //标记数组q.push(srci); //起点入队visited[srci] = true; //已经被遍历过了while (!q.empty()){int front = q.front();q.pop();cout << front << ":" << _vertexs[front] << endl;//把front顶点的邻接顶点入队列for (size_t i = 0; i < _matrix[front].size(); i++){if (_matrix[front][i] != Max_W){if (visited[i] == false){q.push(i);visited[i] = true;}}}}} void BFSLevel(const V& src){int srci = GetVertexIndex(src);queue<int> q; //广度遍历的队列vector<bool> visited(_vertexs.size(), false); //标记数组q.push(srci); //起点入队visited[srci] = true; //已经被遍历过了int levelSize = 1;while (!q.empty()){for (int i = 0; i < levelSize; i++){int front = q.front();q.pop();cout << front << ":" << _vertexs[front] << " ";//把front顶点的邻接顶点入队列for (size_t i = 0; i < _matrix[front].size(); i++){if (_matrix[front][i] != Max_W){if (visited[i] == false){q.push(i);visited[i] = true;}}}}cout << endl;levelSize = q.size();}}void _DFS(size_t srci, vector<bool>& visited){cout << srci << ":" << _vertexs[srci] << endl;visited[srci] = true;for (int i = 0; i < _matrix[srci].size(); i++){if (_matrix[srci][i] != Max_W && visited[i] == false){_DFS(i, visited);}}}void DFS(const V& src){int srci = GetVertexIndex(src);vector<bool> visited(_vertexs.size(), false);_DFS(srci, visited);}struct Edge{int _srci;int _dsti;W _w;Edge(int srci, int dsti, W w):_srci(srci),_dsti(dsti),_w(w){}bool operator>(const Edge& e) const{return this->_w > e._w;}};//传入的是一个只有结点的,没有边的图W Kruskal(Self& minTree){//先将所有的边,按照小堆的方式进行组织起来priority_queue<Edge, vector<Edge>, greater<Edge>> minque;size_t n = _vertexs.size();for (int i = 0; i < n; i++){for (int j = 0; j < n; j++){//由于这里是无向图,他是一个对称矩阵,但是我们的边只考虑一半就已经可以了。剩下的就重复了。if (i < j && _matrix[i][j] != Max_W){//已经按照自身的,带有边的图,将所有的边的信息全部组织好了minque.push(Edge(i, j, _matrix[i][j]));}}}//因为最小生成树一定是n-1条边,所以我们现在要选出n-1条边,size是计数器int size = 0;//用于计算权值W totalW = W();//最关键的问题是判环,这里我们可以用并查集去检测是否这两个顶点在一个集合里面,如果在集合里面,说明一定是连通的,在加上就成环了UnionFindSet ufs(n);//开始选边,我们要考虑到所有的边while (!minque.empty()){//取出一个最小的边,然后就可以将他踢出优先级队列了,如果被选中不需要它了,如果没有被选中,那只能是因为出现环了才不要它了。Edge min = minque.top();minque.pop();//看看是否在一个集合里面,如果在一个集合里面,那么他们已经是连通了,没必要在连通,还想要连通那么一定是环!if (!ufs.InSet(min._srci, min._dsti)){//我们可以看看我们选出来的边cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;//该边是符合的,我们直接为这个图加上边minTree._AddEdge(min._srci, min._dsti, min._w);//加上之后,就连通了,我们让他们形成集合ufs.Union(min._srci, min._dsti);//我们一定只有n-1条边,我们需要计数++size;//将总的权值要加起来totalW += min._w;}//成环的情况,我们只是看看这是哪条边else{cout << "构成环啦!:";cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;}}//上面的循环中,如果图是连通的,那么最终一定选出来的是n-1条边。除非图是不连通的。if (size == n - 1){return totalW;}//图不连通,直接返回0else{return W();}}W Prim(Self& minTree, const V& src){size_t srci = GetVertexIndex(src);int n = _vertexs.size();//使用集合的方式//set<int> X;//set<int> Y;//X.insert(srci);//for (int i = 0; i < n; i++)//{//	if (i != srci)//	{//		Y.insert(i);//	}//}//利用vector的方式,去记录两个集合。vector<bool> X(n, false);vector<bool> Y(n, true);X[srci] = true;Y[srci] = false;//从X->Y集合中连接的边去选最小的边priority_queue<Edge, vector<Edge>, greater<Edge>> minq;//把目前为止X集合(仅仅只有起点)的相关的边,全部放入优先级队列中for (int i = 0; i < n; i++){if (_matrix[srci][i] != Max_W){minq.push(Edge(srci, i, _matrix[srci][i]));}}//size用来判断是否达到最小生成树的个数n-1,totalW用来计算权值之和size_t size = 0;W totalW = W();//我们开始在优先级队列中去寻找while (!minq.empty()){//在优先级队列中找到一个最小的元素,由于优先级队列中的一定是我们X集合可以延申的边。所以是满足Prim的选边条件的Edge min = minq.top();//如果边使用了,那么就不用了,如果不使用,那肯定是因为环才导致的,那也不要了minq.pop();//这里比较巧妙,因为根据我们的算法思想,我们选边的时候一定是从X集合的某一个顶点开始的,然后去找一个不在X集合里面的顶点//所以这里我们可以直接判断目的点是否在X集合里面,如果在,那么一定是环。如果不是,才可以把这条边给加上去if (X[min._dsti]){cout << "构成环:";cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;continue;}//把边给加上去minTree._AddEdge(min._srci, min._dsti, min._w);cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;//X.insert(min._dsti);//Y.erase(min._dsti);//处理一下目的点的边X[min._dsti] = true;Y[min._dsti] = false;++size;totalW += min._w;//这里相当于一次优化,因为该循环一定可以保证选出来的n-1条边是最小生成树,//后面的优先级队列中的任何一条边一定会导致出现环,会在前面的检测目的点是否在X集合中被处理掉。//这里则是直接不用继续入其他的边进入队列了。可以提高一些效率,减少无用的操作if (size == n - 1){break;}//当一条边添加完成后,它就属于X集合了,我们可以将该点所延申出的边给加入到优先级队列中//只有该边存在,且目的地没有被加入过的时候,才会入队列。值得耐人寻味的是,这里虽然已经处理过一次可能出现环的情况了//但是可能由于在添加边的时候,导致某些优先级队列中的边会导致构成环了,所以就有了前面的再次根据目的地时候在X集合中去判环for (int i = 0; i < n; i++){if (_matrix[min._dsti][i] != Max_W && X[i] == false){minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));}}}if (size == n - 1){return totalW;}else{return W();}}private:vector<V> _vertexs; //顶点集合map<V, int> _indexMap; //顶点对应的下标关系vector<vector<W>> _matrix; //临界矩阵};void TestGraph(){Graph<char, int, INT_MAX, true> g("0123", 4);g.AddEdge('0', '1', 1);g.AddEdge('0', '3', 4);g.AddEdge('1', '3', 2);g.AddEdge('1', '2', 9);g.AddEdge('2', '3', 8);g.AddEdge('2', '1', 5);g.AddEdge('2', '0', 3);g.AddEdge('3', '2', 6);g.Print();}void TestGraphBDFS(){string a[] = { "张三", "李四", "王五", "赵六", "周七" };Graph<string, int> g1(a, sizeof(a) / sizeof(string));g1.AddEdge("张三", "李四", 100);g1.AddEdge("张三", "王五", 200);g1.AddEdge("王五", "赵六", 30);g1.AddEdge("王五", "周七", 30);g1.Print();g1.BFS("张三");cout << endl;g1.BFSLevel("张三");cout << endl;g1.DFS("张三");}void TestGraphMinTree(){const char* str = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);//g.AddEdge('a', 'h', 9);g.AddEdge('b', 'c', 8);g.AddEdge('b', 'h', 11); g.AddEdge('c', 'i', 2);g.AddEdge('c', 'f', 4);g.AddEdge('c', 'd', 7);g.AddEdge('d', 'f', 14);g.AddEdge('d', 'e', 9);g.AddEdge('e', 'f', 10);g.AddEdge('f', 'g', 2);g.AddEdge('g', 'h', 1);g.AddEdge('g', 'i', 6);g.AddEdge('h', 'i', 7);//Graph<char, int> kminTree;//Graph<char, int> kminTree(str, strlen(str));//cout << "Kruskal:" << g.Kruskal(kminTree) << endl;//kminTree.Print();Graph<char, int> pminTree(str, strlen(str));cout << "Prim:" << g.Prim(pminTree, 'a') << endl;pminTree.Print();}
}

如上代码中就有了Prim算法的具体实现。

在这里我们需要关注的细节如下所示

  • 与Kruskal算法类似, 但不同的是这里是从一个起点开始出发去寻找边的,而且只去寻找X集合中的可延伸的边
  • 我们仍然可以利用优先级队列。利用优先级队列先找到起始的边
  • 然后找到最优的一条边以后,就去将刚刚加入X集合的顶点的可延伸的边都给加入到优先级队列中,如此循环往复
  • 这里最麻烦的地方就是判环,而且要两处都要进行判环,一处是在我们将新加入X集合的一个元素的周围的可延伸的边进行加入优先级队列的时候,可能会有一些目的地已经在X集合了,就不能加入这条边,否则导致环;一处是在,我们取出优先级队列的最小的边的时候,可能由于前面添加边的过程,导致该边也会导致出现环,所以每次添加边之前都要判断一次目的地是否在X集合中,如果在X集合当中,那么一定出现环。

3.试试所有节点为起始点

如下代码所示,我们修改了一下测试用例

namespace matrix
{//V代表顶点, W是weight代表权值,MAX_W代表权值的最大值,Direction代表是有向图还是无向图,flase表示无向template<class V, class W, W Max_W = INT_MAX, bool Direction = false>class Graph{typedef Graph<V, W, Max_W, Direction> Self;public:Graph() = default;//图的创建//1. IO输入 不方便测试//2. 图结构关系写到文件,读取文件//3. 手动添加边Graph(const V* a, size_t n){_vertexs.reserve(n);for (size_t i = 0; i < n; i++){_vertexs.push_back(a[i]);_indexMap[a[i]] = i;}_matrix.resize(n);for (size_t i = 0; i < _matrix.size(); i++){_matrix[i].resize(n, Max_W);}}size_t GetVertexIndex(const V& v){//return _indexMap[v];auto it = _indexMap.find(v);if (it != _indexMap.end()){return it->second;}else{//assert(false)throw invalid_argument("顶点不存在");return -1;}}void _AddEdge(size_t srci, size_t dsti, const W& w){_matrix[srci][dsti] = w;if (Direction == false){_matrix[dsti][srci] = w;}}void AddEdge(const V& src, const V& dst, const W& w){size_t srci = GetVertexIndex(src);size_t dsti = GetVertexIndex(dst);_AddEdge(srci, dsti, w);}void Print(){for (size_t i = 0; i < _vertexs.size(); i++){cout << "[" << i << "]" << "->" << _vertexs[i] << endl;}cout << endl;cout << "   ";for (int i = 0; i < _vertexs.size(); i++){//cout << _vertexs[i] << " ";printf("%-3d ", i);}cout << endl;for (size_t i = 0; i < _matrix.size(); i++){//cout << _vertexs[i] << " ";printf("%d ", i);for (size_t j = 0; j < _matrix[i].size(); j++){if (_matrix[i][j] == INT_MAX){cout << " *  ";}else{printf(" %d  ", _matrix[i][j]);//cout << _matrix[i][j] << " ";}}cout << endl;}for (size_t i = 0; i < _matrix.size(); i++){for (size_t j = 0; j < _matrix[i].size(); j++){if (i < j && _matrix[i][j] != Max_W){cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl;}}}}void BFS(const V& src){int srci = GetVertexIndex(src);queue<int> q; //广度遍历的队列vector<bool> visited(_vertexs.size(), false); //标记数组q.push(srci); //起点入队visited[srci] = true; //已经被遍历过了while (!q.empty()){int front = q.front();q.pop();cout << front << ":" << _vertexs[front] << endl;//把front顶点的邻接顶点入队列for (size_t i = 0; i < _matrix[front].size(); i++){if (_matrix[front][i] != Max_W){if (visited[i] == false){q.push(i);visited[i] = true;}}}}} void BFSLevel(const V& src){int srci = GetVertexIndex(src);queue<int> q; //广度遍历的队列vector<bool> visited(_vertexs.size(), false); //标记数组q.push(srci); //起点入队visited[srci] = true; //已经被遍历过了int levelSize = 1;while (!q.empty()){for (int i = 0; i < levelSize; i++){int front = q.front();q.pop();cout << front << ":" << _vertexs[front] << " ";//把front顶点的邻接顶点入队列for (size_t i = 0; i < _matrix[front].size(); i++){if (_matrix[front][i] != Max_W){if (visited[i] == false){q.push(i);visited[i] = true;}}}}cout << endl;levelSize = q.size();}}void _DFS(size_t srci, vector<bool>& visited){cout << srci << ":" << _vertexs[srci] << endl;visited[srci] = true;for (int i = 0; i < _matrix[srci].size(); i++){if (_matrix[srci][i] != Max_W && visited[i] == false){_DFS(i, visited);}}}void DFS(const V& src){int srci = GetVertexIndex(src);vector<bool> visited(_vertexs.size(), false);_DFS(srci, visited);}struct Edge{int _srci;int _dsti;W _w;Edge(int srci, int dsti, W w):_srci(srci),_dsti(dsti),_w(w){}bool operator>(const Edge& e) const{return this->_w > e._w;}};//传入的是一个只有结点的,没有边的图W Kruskal(Self& minTree){//先将所有的边,按照小堆的方式进行组织起来priority_queue<Edge, vector<Edge>, greater<Edge>> minque;size_t n = _vertexs.size();for (int i = 0; i < n; i++){for (int j = 0; j < n; j++){//由于这里是无向图,他是一个对称矩阵,但是我们的边只考虑一半就已经可以了。剩下的就重复了。if (i < j && _matrix[i][j] != Max_W){//已经按照自身的,带有边的图,将所有的边的信息全部组织好了minque.push(Edge(i, j, _matrix[i][j]));}}}//因为最小生成树一定是n-1条边,所以我们现在要选出n-1条边,size是计数器int size = 0;//用于计算权值W totalW = W();//最关键的问题是判环,这里我们可以用并查集去检测是否这两个顶点在一个集合里面,如果在集合里面,说明一定是连通的,在加上就成环了UnionFindSet ufs(n);//开始选边,我们要考虑到所有的边while (!minque.empty()){//取出一个最小的边,然后就可以将他踢出优先级队列了,如果被选中不需要它了,如果没有被选中,那只能是因为出现环了才不要它了。Edge min = minque.top();minque.pop();//看看是否在一个集合里面,如果在一个集合里面,那么他们已经是连通了,没必要在连通,还想要连通那么一定是环!if (!ufs.InSet(min._srci, min._dsti)){//我们可以看看我们选出来的边cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;//该边是符合的,我们直接为这个图加上边minTree._AddEdge(min._srci, min._dsti, min._w);//加上之后,就连通了,我们让他们形成集合ufs.Union(min._srci, min._dsti);//我们一定只有n-1条边,我们需要计数++size;//将总的权值要加起来totalW += min._w;}//成环的情况,我们只是看看这是哪条边else{cout << "构成环啦!:";cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;}}//上面的循环中,如果图是连通的,那么最终一定选出来的是n-1条边。除非图是不连通的。if (size == n - 1){return totalW;}//图不连通,直接返回0else{return W();}}W Prim(Self& minTree, const V& src){size_t srci = GetVertexIndex(src);int n = _vertexs.size();//使用集合的方式//set<int> X;//set<int> Y;//X.insert(srci);//for (int i = 0; i < n; i++)//{//	if (i != srci)//	{//		Y.insert(i);//	}//}//利用vector的方式,去记录两个集合。vector<bool> X(n, false);vector<bool> Y(n, true);X[srci] = true;Y[srci] = false;//从X->Y集合中连接的边去选最小的边priority_queue<Edge, vector<Edge>, greater<Edge>> minq;//把目前为止X集合(仅仅只有起点)的相关的边,全部放入优先级队列中for (int i = 0; i < n; i++){if (_matrix[srci][i] != Max_W){minq.push(Edge(srci, i, _matrix[srci][i]));}}//size用来判断是否达到最小生成树的个数n-1,totalW用来计算权值之和size_t size = 0;W totalW = W();//我们开始在优先级队列中去寻找while (!minq.empty()){//在优先级队列中找到一个最小的元素,由于优先级队列中的一定是我们X集合可以延申的边。所以是满足Prim的选边条件的Edge min = minq.top();//如果边使用了,那么就不用了,如果不使用,那肯定是因为环才导致的,那也不要了minq.pop();//这里比较巧妙,因为根据我们的算法思想,我们选边的时候一定是从X集合的某一个顶点开始的,然后去找一个不在X集合里面的顶点//所以这里我们可以直接判断目的点是否在X集合里面,如果在,那么一定是环。如果不是,才可以把这条边给加上去if (X[min._dsti]){//cout << "构成环:";//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;continue;}//把边给加上去minTree._AddEdge(min._srci, min._dsti, min._w);//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;//X.insert(min._dsti);//Y.erase(min._dsti);//处理一下目的点的边X[min._dsti] = true;Y[min._dsti] = false;++size;totalW += min._w;//这里相当于一次优化,因为该循环一定可以保证选出来的n-1条边是最小生成树,//后面的优先级队列中的任何一条边一定会导致出现环,会在前面的检测目的点是否在X集合中被处理掉。//这里则是直接不用继续入其他的边进入队列了。可以提高一些效率,减少无用的操作if (size == n - 1){break;}//当一条边添加完成后,它就属于X集合了,我们可以将该点所延申出的边给加入到优先级队列中//只有该边存在,且目的地没有被加入过的时候,才会入队列。值得耐人寻味的是,这里虽然已经处理过一次可能出现环的情况了//但是可能由于在添加边的时候,导致某些优先级队列中的边会导致构成环了,所以就有了前面的再次根据目的地时候在X集合中去判环for (int i = 0; i < n; i++){if (_matrix[min._dsti][i] != Max_W && X[i] == false){minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));}}}if (size == n - 1){return totalW;}else{return W();}}private:vector<V> _vertexs; //顶点集合map<V, int> _indexMap; //顶点对应的下标关系vector<vector<W>> _matrix; //临界矩阵};void TestGraph(){Graph<char, int, INT_MAX, true> g("0123", 4);g.AddEdge('0', '1', 1);g.AddEdge('0', '3', 4);g.AddEdge('1', '3', 2);g.AddEdge('1', '2', 9);g.AddEdge('2', '3', 8);g.AddEdge('2', '1', 5);g.AddEdge('2', '0', 3);g.AddEdge('3', '2', 6);g.Print();}void TestGraphBDFS(){string a[] = { "张三", "李四", "王五", "赵六", "周七" };Graph<string, int> g1(a, sizeof(a) / sizeof(string));g1.AddEdge("张三", "李四", 100);g1.AddEdge("张三", "王五", 200);g1.AddEdge("王五", "赵六", 30);g1.AddEdge("王五", "周七", 30);g1.Print();g1.BFS("张三");cout << endl;g1.BFSLevel("张三");cout << endl;g1.DFS("张三");}void TestGraphMinTree(){const char* str = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);//g.AddEdge('a', 'h', 9);g.AddEdge('b', 'c', 8);g.AddEdge('b', 'h', 11); g.AddEdge('c', 'i', 2);g.AddEdge('c', 'f', 4);g.AddEdge('c', 'd', 7);g.AddEdge('d', 'f', 14);g.AddEdge('d', 'e', 9);g.AddEdge('e', 'f', 10);g.AddEdge('f', 'g', 2);g.AddEdge('g', 'h', 1);g.AddEdge('g', 'i', 6);g.AddEdge('h', 'i', 7);//Graph<char, int> kminTree;//Graph<char, int> kminTree(str, strlen(str));//cout << "Kruskal:" << g.Kruskal(kminTree) << endl;//kminTree.Print();//Graph<char, int> pminTree(str, strlen(str));//cout << "Prim:" << g.Prim(pminTree, 'a') << endl;//pminTree.Print();for (int i = 0; i < strlen(str); i++){Graph<char, int> pminTree(str, strlen(str));cout << "Prim:" << str[i] << ":" << g.Prim(pminTree, str[i]) << endl;}}
}

运行结果为

image-20240220024800101

可以看到,Prim算法以任何一个顶点的最小生成树都是37,而前面的Kruskal的结果也是37

需要注意的是:

  • 对于一个特定的图,Prim算法以任何一个顶点为起始点,最终得到的最小生成树的权值是确定的。这是因为Prim算法是一种确定性算法,它按照一定的贪心策略逐步构建最小生成树。

  • Prim算法的贪心策略是每次选择连接已经在生成树中的顶点与不在生成树中的顶点的最短边,并将该顶点加入生成树中。由于算法每次都选择最短的边,所以最终得到的最小生成树是唯一的,因此权值和也是确定的。

  • 所以,Prim算法以任何一个顶点为起始点,最终得到的最小生成树的权值是确定的。

  • 对于一个特定的图,Kruskal算法和Prim算法求出来的最小生成树的权值和未必相同。这是因为它们的工作原理和选择边的方式不同。
  • Kruskal算法按照边的权值从小到大的顺序选择边,并确保加入的边不会形成环路。这样做直到生成树中包含了图中的所有顶点为止。
  • Prim算法是从一个初始顶点开始,逐步扩展形成最小生成树。它每次会选择连接当前已加入生成树的顶点和未加入生成树的顶点中权值最小的边,并将其对应的顶点加入生成树。
  • 因此,尽管这两种算法都可以得到最小生成树,但由于它们的执行方式和选择边的方式不同,所以得到的最小生成树的权值和可能不相同。只有在某些特殊情况下,它们才会得到相同的最小生成树权值和。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/721258.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计模式:工厂模式 ⑤

一、思想 工厂模式&#xff1a;一个中介作用&#xff0c;在创建对象的时候。 主要作用&#xff1a;屏蔽对象创建过程&#xff0c;减少上层关注度&#xff0c;解耦并且内部方法可做更多扩展增强的处理。(比如使用映射消除if代码&#xff0c;存在多个同类对象需要抽象策略处理的时…

蓝桥杯练习系统(算法训练)ALGO-988 逗志芃的危机

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 逗志芃又一次面临了危机。逗志芃的妹子是个聪明绝顶的人&#xff0c;相比之下逗志芃就很菜了。现在她妹子要和他玩一个游戏…

express+mysql+vue,从零搭建一个商城管理系统8--文件上传,大文件分片上传

提示&#xff1a;学习express&#xff0c;搭建管理系统 文章目录 前言一、安装multer&#xff0c;fs-extra二、新建config/upload.js三、新建routes/upload.js四、修改routes下的index.js五、修改index.js六、新建上传文件test.html七、开启jwt验证token&#xff0c;通过login接…

java核心面试题汇总

文章目录 1. Java1.1. TCP三次握手/四次挥手1.2 HashMap底层原理1.3 Java常见IO模型1.4 线程与线程池工作原理1.5 讲一讲ThreadLocal、Synchronized、volatile底层原理1.6 了解AQS底层原理吗 2. MySQL2.1 MySQL索引为何不采用红黑树&#xff0c;而选择B树2.2 MySQL索引为何不采…

JVM(类加载机制)

类加载就是 .class 文件, 从文件(硬盘) 被加载到内存(元数据区)中的过程 类加载的过程 加载: 找 .class 文件的过程, 打开文件, 读文件, 把文件读到内存中 验证: 检查 .class 文件的格式是否正确 .class 是一个二进制文件, 其格式有严格的说明 准备: 给类对象分配内存空间 (先在…

【C++干货基地】面向对象核心概念 | 访问限定符 | 类域 | 实例化 | 类对象模型

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 哈喽各位铁汁们好啊&#xff0c;我是博主鸽芷咕《C干货基地》是由我的襄阳家乡零食基地有感而发&#xff0c;不知道各位的…

一分钟了解深度学习算法

自从20世纪40年代起人工智能&#xff08;AI&#xff09;问世以来&#xff0c;学者们不懈探索着如何使机器具备模拟人类学习能力的能力。随着计算性能的不断提升和算法的演进&#xff0c;深度学习算法已成为AI领域的核心技术。本文将简述深度学习算法的概念、构成要素、应用范围…

【亲测】注册Claude3教程,解决Claude3被封号无法发送手机验证码

【亲测】注册Claude3教程&#xff1a;解决无法发送手机验证码的问题 Anthropic 今日宣布推出其最新大型语言模型&#xff08;LLM&#xff09;系列——Claude 3&#xff0c;这一系列模型在各种认知任务上树立了新的性能标准。Claude 3 系列包括三个子模型&#xff1a;Claude 3 …

金三银四,程序员如何备战面试季

金三银四&#xff0c;程序员如何备战面试季 一个人简介二前言三面试技巧分享3.1 自我介绍 四技术问题回答4.1 团队协作经验展示 五职业规划建议5.1 短期目标5.2 中长期目标 六后记 一个人简介 &#x1f3d8;️&#x1f3d8;️个人主页&#xff1a;以山河作礼。 &#x1f396;️…

【单调栈】Leetcode 739.每日温度

【单调栈】Leetcode 739.每日温度 解法&#xff1a;维护单调栈栈中存的是数组的索引 解法&#xff1a;维护单调栈栈中存的是数组的索引 栈中存的是数组的索引 当新的值比当前栈顶的大&#xff0c;那么就执行出栈-更新result数组-判断当新的值比当前栈顶的大&#xff1f;的循环…

白银期货开户交割规则有哪些?

白银期货交割是指期货合约到期时&#xff0c;交易双方通过该期货合约所载商品所有权的转移&#xff0c;了结到期未平仓合约的过程。小编在此为大家详细介绍白银期货的交割规则有哪些。白银期货的交割规则有哪些&#xff1f;白银期货的交割规则主要有&#xff1a; 一、交割商品…

(3)(3.2) MAVLink2数据包签名(安全)

文章目录 前言 1 配置 2 使用 3 MAVLink协议说明 前言 ArduPilot 和任务计划器能够通过使用加密密钥添加数据包签名&#xff0c;为空中 MAVLink 传输增加安全性。这并不加密数据&#xff0c;只是控制自动驾驶仪是否响应 MAVLink 命令。 当自动驾驶仪处于激活状态时&#x…

【Python】进阶学习:pandas--info()用法详解

【Python】进阶学习&#xff1a;pandas–info()用法详解 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得到您的订…

新零售SaaS架构:订单履约系统的概念模型设计

订单履约系统的概念模型 订单&#xff1a;客户提交购物请求后&#xff0c;生成的买卖合同&#xff0c;通常包含客户信息、下单日期、所购买的商品或服务明细、价格、数量、收货地址以及支付方式等详细信息。 子订单&#xff1a;为了更高效地进行履约&#xff0c;大订单可能会被…

科技创新赋能森歌制造-浅谈森歌高品质发展之路

随着时代的变迁&#xff0c;科技创新已成为推动制造业高质量发展的关键力量。森歌&#xff0c;作为厨电行业的佼佼者&#xff0c;始终坚守着对优质品质的承诺&#xff0c;并在品牌的科技化升级之路上不断迈进。 在制造业科技化的背景下&#xff0c;新型工业化、数字经济、制造…

TQTT X310 软件无线电设备的FLASH固件更新方法

TQTT X310 除了PCIE口全部兼容USRP 官方的X310&#xff0c;并配备两块UBX160射频子板以及GPSDO。TQTT X310可以直接使用官方的固件&#xff0c;但是不支持官方的固件升级命令。这篇BLOG提供烧写刷新FLASH的方法。 1&#xff0c;使用的是WINDOWS系统。首先给X310接入电源并开机…

vue3 中使用 TinyMCE 富文本编辑器

1. TinyMCE 官方网站地址&#xff08;可能需要魔法上网才能访问&#xff09; 我们直接找到 TinyMCE 关于 vue 的下载地址&#xff0c;其他框架的下载也在这里 2. 向下找&#xff0c;找到关于vue3下载的地方 下载命令 npm install --save "tinymce/tinymce-vue^5" 例…

Linux 模拟实现shell【简单实现】

shell的模拟实现 我们知道shell是一个永不退出的程序&#xff0c;所以他应该是一个死循环&#xff0c;并且shell为了防止影响到自己&#xff0c;我们在命令行上输入的所有命令都是由shell的子进程来执行的&#xff0c;所以它应该要有创建子进程的相关函数&#xff0c;当然也会…

loadrunner lr解决参数化一次取多条记录【一对多问题】

场景&#xff1a;批量进行工作汇报&#xff0c;一个项目下选择三个工作项进行汇报&#xff1b; 问题&#xff1a;项目GUID变化一次&#xff0c;工作项GUID要取三个值&#xff0c;也就是变化三次&#xff1b; 我们知道&#xff0c;在Parameter List中可以设置参数取值规则&…

Tomcat(二) 动静分离

一、(TomcatNginx)动静分离 1、单机反向代理 利用 nginx 反向代理实现全部转发至指定同一个虚拟主机 客户端curl www.a.com 访问nginx服务&#xff0c;nginx服务通过配置反向代理proxy_pass www.a.com:8080&#xff0c;最终客户端看到的是www.a.com 实验中&#xff1a;7-3 做客…