【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;
			}
			//图不连通,直接返回0
			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(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;
			}
			//图不连通,直接返回0
			else
			{
				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;
			}
			//图不连通,直接返回0
			else
			{
				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算法是从一个初始顶点开始,逐步扩展形成最小生成树。它每次会选择连接当前已加入生成树的顶点和未加入生成树的顶点中权值最小的边,并将其对应的顶点加入生成树。
  • 因此,尽管这两种算法都可以得到最小生成树,但由于它们的执行方式和选择边的方式不同,所以得到的最小生成树的权值和可能不相同。只有在某些特殊情况下,它们才会得到相同的最小生成树权值和。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

这本书太好了!150页就能让你上手大模型应用开发

如果问个问题&#xff1a;有哪些产品曾经创造了伟大的奇迹&#xff1f;ChatGPT 应该会当之无愧入选。 仅仅发布 5 天&#xff0c;ChatGPT 就吸引了 100 万用户——当然&#xff0c;数据不是关键&#xff0c;关键是其背后的技术开启了新的 AI 狂潮&#xff0c;成为技术变革的点火…

强势改进!基于改进多目标灰狼算法的冷热电联供型微电网运行优化程序代码!

适用平台&#xff1a;MatlabYalmipCplex 程序以综合能源系统/微电网为研究对象&#xff0c;将微电网的运行费用和环境污染成本作为优化目标&#xff0c;考虑冷热电负荷和设备运行要求的约束&#xff0c;建立的微电网的多目标优化模型&#xff0c;使用改进多目标灰狼算法算法进…

有个朋友被骗了,大家要擦亮眼睛

1.引言 大家好&#xff0c;我是Leo哥&#x1fae3;&#x1fae3;&#x1fae3;&#xff0c;昨天凌晨有个粉丝朋友找到Leo哥&#xff0c;咨询一些问题&#xff0c;现在的朋友们真卷呐&#xff0c;大半夜还在挑灯夜战。可无奈Leo哥12点之前已经睡了&#xff0c;身体为重&#xf…

智慧社区养老:Java与SpringBoot的技术融合

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

Day31|贪心算法1

贪心的本质是选择每一阶段的局部最优&#xff0c;从而达到全局最优。 无固定套路&#xff0c;举不出反例&#xff0c;就可以试试贪心。 一般解题步骤&#xff1a; 1.将问题分解成若干子问题 2.找出适合的贪心策略 3.求解每一个子问题的最优解 4.将局部最优解堆叠成全局最…

C语言第三十五弹---文件操作(上)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 文件操作 1、为什么使用文件&#xff1f; 2、什么是文件&#xff1f; 2.1、程序文件 2.2、数据文件 2.3、文件名 3、二进制文件和文本文件 4、文件的打开和…

YOLO v9训练自己数据集

原以为RT-DETR可以真的干翻YOLO家族&#xff0c;结果&#xff0c;&#xff01;&#xff01;&#xff01;&#xff01; 究竟能否让卷积神经网络重获新生&#xff1f; 1.数据准备 代码地址&#xff1a;https://github.com/WongKinYiu/yolov9 不能科学上网的评论区留言 数据集…

【Python】新手入门(2):避免将关键字作为标识符

Python新手入门&#xff08;2&#xff09;&#xff1a;避免将关键字作为标识符 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1…

蓝桥杯-单片机组基础7-存储器映射扩展与PWM脉冲调制(附小蜜蜂课程代码)

蓝桥杯单片机组备赛指南请查看这篇文章&#xff1a;戳此跳转蓝桥杯备赛指南文章 本文章针对蓝桥杯-单片机组比赛开发板所写&#xff0c;代码可直接在比赛开发板上使用。 型号&#xff1a;国信天长4T开发板&#xff08;绿板&#xff09;&#xff0c;芯片&#xff1a;IAP15F2K6…

【Python】matplotlib绘制图像时增加颜色条

一、需求 plt.imshow()是matplotlib中的一个函数&#xff0c;用于显示图像。它可以传递一个二维或三维数组作为image参数&#xff0c; 并将图像数据显示为图形&#xff0c;并对图像进行不同的可视化设置。 在显示的过程中&#xff0c;我们如果需要增加一个图例显示颜色条&…

Word转Excel怎么操作?4个实用技巧别忘了!

“我在处理一个Word文件时&#xff0c;需要将里面的一些表格内容转化为Excel。有什么比较好用的Word转Excel方法可以推荐吗&#xff1f;” 在互联网时代&#xff0c;数据处理和信息整合是工作中不可或缺的一部分。有时&#xff0c;我们可能会遇到需要将Word文档中的数据或内容转…

高性能深度学习库luminal

一、概述 Luminal是一个深度学习库&#xff0c;它使用可组合的编译器来实现高性能。 当前的机器学习库往往很庞大复杂&#xff0c;因为它们试图直接将高级操作映射到底层手工编写的内核上&#xff0c;并且专注于立刻执行&#xff08;eager模式&#xff09;。像PyTorch这样的库…

Java Web开发---复试Tips复习

&#xff08;自用&#xff0c;摘录自各种文章和自己总结&#xff09; 小知识点理解 Web Web应用开发主要是基于浏览器的应用程序开发。一个Web应用由多部分组成 Web应用程序编写完后&#xff0c;若想提供给外界访问&#xff0c;需要服务器来统一管理 常用的动态网页语言——…

react native中如何使用webView调用腾讯地图选点组件

react native中如何使用webView调用腾讯地图选点组件 效果示例图代码示例备注 效果示例图 代码示例 import React, {useEffect, useRef, useState} from react; import {Modal, StyleSheet} from react-native; import {pxToPd} from ../../common/js/device; import {WebView…

私有化部署自己的ChatGPT,免费开源的chatgpt-next-web搭建

随着AI的应用变广&#xff0c;各类AI程序已逐渐普及&#xff0c;尤其是在一些日常办公、学习等与撰写/翻译文稿密切相关的场景&#xff0c;大家都希望找到一个适合自己的稳定可靠的ChatGPT软件来使用。 ChatGPT-Next-Web就是一个很好的选择。它是一个Github上超人气的免费开源…

产品介绍二维码怎么做?多种内容组合二维码的方法

现在扫描二维码来获取内容的方式越来越常见&#xff0c;比如很多的产品介绍都会做成二维码图片后&#xff0c;在产品包装、传单、展板等印刷展示。而一般展示的内容类型多为文本、图片、视频等内容&#xff0c;那么这些类型的内容放入一个二维码中展示如何实现呢&#xff1f; …

白酒:制曲工艺的环境因素与微生物生态关系

在豪迈白酒的酿造过程中&#xff0c;制曲工艺是非常关键的一环。而环境因素与微生物生态关系对于制曲工艺的成功与否起着决定性的作用。云仓酒庄深谙此道&#xff0c;在制曲过程中注重环境因素的调控&#xff0c;并深入研究微生物生态关系&#xff0c;以提升豪迈白酒的品质和风…

Jmeter 命令启动 —— 动态参数化!

Jmeter命令行参数 1、在Linux中&#xff0c;使用非GUI的方式执行Jmeter。若需更改参数&#xff0c;必须先编辑jmx文件&#xff0c;找到对应变量进行修改&#xff0c;比较麻烦。 因此&#xff0c;可以参数化一些常用的变量&#xff0c;直接在Jmeter命令行进行设置 2、参数 -J…

draw.io设置矩形四个边的有无

第一步创建一个矩形 选中矩形&#xff0c;并编辑样式 然后在编辑框中输入 verticalLabelPositionbottom;verticalAligntop;html1;shapemxgraph.basic.rect;right1;将原来的内容覆盖掉 然后点击应用 然后发现 点击那四个设置就可以了

MyCAT集群——MyCAT2如何配置读写分离

先搭载MySQL一主两从 192.168.20.110MyCAT192.168.20.111Master192.168.20.112slave1192.168.20.113slave2 配置就不写了&#xff0c;比较基础&#xff0c;写一下步骤 1.进入mysql配置文件或者其子配置文件&#xff0c;添加server_id,开启gtidgtid_modeON,enforce-gtid-cons…