通过C++对【图】进行抽丝剥茧(包括广度、深度优先遍历,求最小生成树,求最短路径)

news2024/11/18 0:31:27

目录 

一.图的基本概念

二.图的存储结构

1.邻接矩阵

(1)无向图、有向图矩阵存储

(2)实现:

2.邻接表

(1)无向图邻接表存储

(2)有向图邻接表存储

(3)实现

三.图的遍历

1.广度优先遍历

2.深度优先遍历

四.最小生成树

1.Kruskal(克鲁斯卡尔算法)

2.Prim(普里姆算法)

五.最短路径

1.单源最短路径

(1)Dijkstra(迪杰斯特拉算法)

(2)Bellman-Ford(贝尔曼-福特算法)

2.多源最短路径

(1)Floyd-Warshall(弗洛伊德算法)

六.总代码


前言:图是一个非常抽象的数据结构,之前学过的二叉树也属于图。想要理解图,就要一步一步的去分析理解。

一.图的基本概念

(1)图:图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中V是顶点集合,V = {x | x属于某个数据对象集} 是有穷非空集合;E是边的集合,E = {(x, y) | x, y属于V} 或者 E = {<x, y> | x, y属于V && Path(x, y)}是顶点间关系的有穷集合。(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即Path(x, y)是有方向的。

(2)顶点和边:图的结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi, vj) 或 <vi, vj>

(3)有向图和无向图:有向图中顶点对<x, y>是有序的,顶点对<x, y>称为顶点x到顶点y的一条边(弧)<x, y>和<y, x>是两条不同的边。在无向图中顶点对(x, y)是无序的,顶点对(x, y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y, x)是同一条边。【无向边(x, y)等于有向边<x, y>和<x, y>】。

(4)完全图:有n个顶点的无向图中,若有n * (n-1) / 2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图;在有n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图

(5)邻接顶点:无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点并称边(u, v)依附于顶点u和v;在有向图G中若<u, v>是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联。

(6)顶点的度:顶点v的度是指与它相关联的边的条数记作deg(v)。在有向图中顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v)顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。

(7)路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。

(8)路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。

(9)简单路径:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径

(10)回路:若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。

(11)子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。

(12)连通图:无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。

(13)强连通图:有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径也存在一条从vj到vi的路径,则称此图是强连通图。

(14)生成树:一个连通图的最小连通子图称作该图的生成树有n个顶点的连通图的生成树有n个顶点和n-1条边。

二.图的存储结构

        图既有结点又有边,因此在图的存储中,需要保存:结点和边的关系

1.邻接矩阵

        邻接矩阵(二维数组):先用一个数组将定点保存,然后采用矩阵来表示结点与结点之间的关系。

(1)无向图、有向图矩阵存储

无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度,有向图的邻接矩阵则不一定是对称的,第i行(列)元素之和就是顶点i的出(入)度。

② 如果边带有权值,并且两个结点之间是连通的那么边的关系就用权值代替,如果两个顶点不连通则使用无穷大代替

③ 用邻接矩阵存储图的优点是能够快速知道两个顶点是否连通缺点是如果顶点比较多,边比
较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路
径不是很好求

(2)实现:

namespace matrix
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;
	public:
		// 图的创建
		// 1.IO输入		-- 不方便测试
		// 2.图结构关系写到文件,读取文件
		// 3.手动添加边
		Graph() = default;

		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)
		{
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				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 (size_t i = 0; i < _vertexs.size(); ++i)
			{
				//cout << i << " ";
				printf("%4d", i);
			}
			cout << endl;

			for (size_t i = 0; i < _matrix.size(); ++i)
			{
				cout << i << " "; // 竖下标
				for (size_t j = 0; j < _matrix[i].size(); ++j)
				{
					//cout << _matrix[i][j] << " ";
					if (_matrix[i][j] == MAX_W)
					{
						//cout << "* ";
						printf("%4c", '*');
					}
					else
					{
						//cout << _matrix[i][j] << " ";
						printf("%4d", _matrix[i][j]);
					}
				}
				cout << endl;
			}
			cout << endl;
		}
    private:
		vector<V> _vertexs;			// 顶点集合
		map<V, int> _indexMap;		// 顶点映射下标
		vector<vector<W>> _matrix;  // 邻接矩阵
	};
}

        首先,定义变量时,要能存下顶点,以及矩阵,同时要能通过对应的下标找到顶点。因此私有成员为vector的顶点集合,map的顶点映射下标,以及vector<vector<W>>的邻接矩阵。

        上面的模板中,V是存入的数据的类型,W是权值的类型,MAX_W是类型最大值(默认是int最大值),Direction来区分有向图和无向图(false为无向图,true为有向图)。

         然后需要实现构造函数,为顶点和矩阵开辟足够的空间,然后将顶点值存入顶点集合中,同时用map映射对应的下标,最后再将矩阵全部初始化为类型最大值。又因为需要用到默认构造函数,所以就Graph() = default;强制编译器生成默认构造函数。

        要得到元素位置,就要利用之前的map,通过值,利用map的find找到对应的迭代器,然后it->second就可以得到该值对应的下标,如果没找到(即it == _indexMap.end()),就抛异常,同时返回-1。

        我们要在矩阵中保存结点和边的关系,因此实现函数AddEdge,这里我们通过子函数的方法实现,因为后面需要用到子函数。首先根据传入的两个数据,得到这两个数据对应的位置,再将该位置的值变为其传来的权值。如果为无向图,因为无向图是对称的,所以在矩阵中将之前得到的两个数据的位置互换,再更改为权值。

        最后,为了看到矩阵,我们实现打印函数。

2.邻接表

        邻接表:使用数组表示顶点的集合,使用链表表示边的关系

        如果上一个邻接矩阵懂了,那么这个邻接表在逻辑上也差不多,也很容易弄懂。

(1)无向图邻接表存储

        无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目。

(2)有向图邻接表存储

        有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其它所有顶电对应的边链表,看有多少边顶点的dst取值是i。

(3)实现

namespace link_table
{
	template <class W>
	struct Edge
	{
		// int _srci;
		int _dsti;  // 目标点的下标
		W _w;		// 权值
		Edge<W>* _next;

		Edge(int dsti, const W& w)
			: _dsti(dsti)
			, _w(w)
			, _next(nullptr)
		{}
	};

	template <class V, class W, bool Direction = false>
	class Graph
	{
		typedef Edge<W> Edge;
	public:
		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;
			}

			_tables.resize(n, nullptr);
		}

		// 得到元素位置
		size_t GetVertexIndex(const V& v)
		{
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				throw invalid_argument("顶点不存在");

				return -1;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_t dsti = GetVertexIndex(dst);

			// 1->2
			Edge* eg = new Edge(dsti, w);
			eg->_next = _tables[srci];
			_tables[srci] = eg;

			// 2->1
			// 无向图
			if (Direction == false)
			{
				Edge* eg = new Edge(srci, w);
				eg->_next = _tables[dsti];
				_tables[dsti] = eg;
			}
		}

		void Print()
		{
			// 顶点
			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			for (size_t i = 0; i < _tables.size(); ++i)
			{
				cout << _vertexs[i] << "[" << i << "]->";
				Edge* cur = _tables[i];
				while (cur)
				{
					cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->";
					cur = cur->_next;
				}
				cout << "nullptr" << endl;
			}
		}
	private:
		vector<V> _vertexs;		// 顶点集合
		map<V, int> _indexMap;  // 顶点映射下标
		vector<Edge*> _tables;	// 邻接表
	};
}

        这里我们只实现一个出度表。

        首先,私有成员为vector的顶点集合,map的顶点映射下标,以及vector<Edge*>的邻接表。注意这里邻接表存的是Edge*,因为这个是链表,所以不仅要有权值,还要有next指针等,因此要额外实现一个类Edge,这里存Edge*,就可以通过该指针找到对应的目标点下标,权值和next指针。

        上面的模板中,V是存入的数据的类型,W是权值的类型,Direction来区分有向图和无向图(false为无向图,true为有向图)。

         然后需要实现构造函数,为顶点开辟足够的空间,然后将顶点值存入顶点集合中,同时用map映射对应的下标,最后再将链表表全部初始化为nullptr。

        要得到元素位置,就要利用之前的map,通过值,利用map的find找到对应的迭代器,然后it->second就可以得到该值对应的下标,如果没找到(即it == _indexMap.end()),就抛异常,同时返回-1。(这个与邻接矩阵完全相同)

        邻接表要想添加边,首先得到数据对应的位置,然后采用头插的方法,先创建新结点,然后头插链接。如果是无向图,就再次将对称的位置头插链接。

        最后,为了看到邻接表,我们实现打印函数。

三.图的遍历

        注意:后面的操作都是在邻接矩阵中完成的。 

1.广度优先遍历

        广度优先遍历类似于层序遍历。

        是一层一层遍历的,我们通过队列和一个用来标记是否遍历过的数组来实现。 

实现:

void BFS(const V& src)
{
	size_t srci = GetVertexIndex(src);

	// 队列和标记数组
	queue<int> q;
	vector<bool> visited(_vertexs.size(), false);

	q.push(srci);
	visited[srci] = true;
	int levelSize = 1;

	size_t n = _vertexs.size();
	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 < n; ++i)
			{
				if (_matrix[front][i] != MAX_W)
				{
					if (visited[i] == false)
					{
						q.push(i);
						visited[i] = true;
					}
				}
			}
		}
		cout << endl;

		levelSize = q.size();
	}

	cout << endl;
}

        与层序遍历相同都是通过队列来完成。

        首先根据传来的数据找到该数据对应的位置,然后分别创建一个队列和一个用来标记是否遍历过的数组(初始化为false)。

        先将该数据对应的位置入队列,同时将标记数组中该位置变为true,代表该位置已经被遍历。同时levelSize来记录上一层的顶点数量,定义变量n得到当前顶点的数量。

        然后一个while循环条件是队列是否为空,一个for循环使之一层一层的遍历(根据levelSize来进行)。先保存队列头,然后删掉该头,并把该头的邻接顶点入队列(通过for循环实现)。

        入队列的方法就是通过利用矩阵的特点(与front同一行的矩阵中不是int的最大值的就是邻接的顶点),同时如果该位置标记数组为false,就可以将其入队列,并将该位置标记数组变为true。最后把levelSize变为当前队列中的数量(即是下一层的遍历次数)。

2.深度优先遍历

        深度优先遍历类似于二叉树的前中后序遍历。

         通过递归的方法来实现。

实现:

void _DFS(size_t srci, vector<bool>& visited)
{
	cout << srci << ":" << _vertexs[srci] << endl;
	visited[srci] = true;

	// 找一个srci相邻的没有访问过多点,去往深度遍历
	for (size_t i = 0; i < _vertexs.size(); ++i)
	{
		if (_matrix[srci][i] != MAX_W && visited[i] == false)
		{
			_DFS(i, visited);
		}
	}
}

void DFS(const V& src)
{
	size_t srci = GetVertexIndex(src);
	vector<bool> visited(_vertexs.size(), false);

	_DFS(srci, visited);
}

        首先得到数据的对应位置,并将标记数组全部初始化为false,进入_DFS子函数中。

        将当前数据位置的标记数组变为true,接着找该位置相邻的(与当前数据位置同一行的,且不为int的最大值的)没有访问过的点(标记数组为false的),去进行深度遍历(递归)。

四.最小生成树

       求最小生成树的都是无向图。

       最小生成树特点:从其中删去任何一条边,生成树就不再连通;反之,在其中引入任何一条新边,都会形成一条回路。

       若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。

       构成最小生成树的三个准则:

① 只能使用图中的边来构造最小生成树。

② 只能使用恰好n-1条边来连接图中的n个顶点。

③ 选用的n-1条边不能构成回路。

构成最小生成树的方法:Kruskal算法Prim算法,这两个算法都采用了逐步求解的贪心策略

贪心算法:在问题求解时,总是做出当前看起来最好的选择。

        也就是说贪心算法做出是不是整体最优的选择,而是某种意义上的局部最优解,贪心算法不是对所有问题都能得到整体最优解的。

1.Kruskal(克鲁斯卡尔算法)

        任给一个由n个顶点组成的图。

        求最小生成树方法:

① 首先构造一个由这n个顶点组成,不含任何边的图G = {V, nullptr},其中每个顶点自成一个连通分量。

② 然后不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。

算法核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

Kruskal算法需要用到上一篇的并查集:

        

 实现:

 并查集:

#include <vector>

class UnionFindSet
{
public:
	UnionFindSet(size_t n)
		: _ufs(n, -1)
	{}

	size_t FindRoot(int x)
	{
		int root = x;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}

		// 路径压缩
		while (_ufs[x] >= 0)
		{
			int parent = _ufs[x];
			_ufs[x] = root;

			x = parent;
		}

		return root;
	}

	void Union(int x1, int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);

		// 如果本身就在一个集合就没必要合并了
		if (root1 == root2)
		{
			return;
		}

		// 控制数据量小的往大的集合合并
		if (abs(_ufs[root1]) < abs(_ufs[root2]))
		{
			swap(root1, root2);
		}

		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
	}

	bool InSet(int x1, int x2)
	{
		return FindRoot(x1) == FindRoot(x2);
	}

	size_t SetSize()
	{
		size_t size = 0;
		for (size_t i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0)
			{
				++size;
			}
		}

		return size;
	}
private:
	std::vector<int> _ufs;
};

_AddEdge函数: 

void _AddEdge(size_t srci, size_t dsti, const W& w)
{
	_matrix[srci][dsti] = w;
	// 无向图
	if (Direction == false)
	{
		_matrix[dsti][srci] = w;
	}
}

Kruskal算法:

typedef Graph<V, W, MAX_W, Direction> Self;

struct Edge
{
	size_t _srci;
	size_t _dsti;
	W _w;

	Edge(size_t srci, size_t dsti, const W & w)
		: _srci(srci)
		, _dsti(dsti)
		, _w(w)
	{}

	bool operator>(const Edge & e) const
	{
		return _w > e._w;
	}
};

W Kruskal(Self& minTree)
{
	size_t n = _vertexs.size();

	minTree._vertexs = _vertexs;
	minTree._indexMap = _indexMap;
	minTree._matrix.resize(n);
	for (size_t i = 0; i < n; ++i)
	{
		minTree._matrix[i].resize(n, MAX_W);
	}

	priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
	for (size_t i = 0; i < n; ++i)
	{
		for (size_t j = 0; j < n; ++j)
		{
			if (i < j && _matrix[i][j] != MAX_W)
			{
				minque.push(Edge(i, j, _matrix[i][j]));
			}
		}
	}

	// 选出n-1条边
	int size = 0;
	// W一般是整数、浮点数,W()是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);
			++size;
			totalW += min._w;
		}
		else
		{
			cout << "构成环:";
			cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
		}
	}

	if (size == n - 1)
	{
		return totalW;
	}
	else
	{
		return W();
	}
}

        先创建一个Edge的类,用来存放该边的权值、以及该边的两个顶点。

        首先将minTree初始化,让minTree的顶点集合和顶点映射下标map与要求最小生成树的图相同,同时为minTree的邻接矩阵开辟好空间,并将该矩阵全部初始化为类型最大值。

        创建一个优先级队列(传greater,使其从小到大排好序),然后循环遍历矩阵,将i < j(矩阵的一半【无向图是对称矩阵】),存放Edge类。

        接着定义一个size,用来判断该最小生成树是否为n-1条边,totalW记录最小生成树权值,并创建一个并查集类的ufs。

        利用优先级队列是否为空来实现一个while循环,得到优先级队列顶部元素(最小的元素),并在优先级队列中删除掉该元素,然后根据并查集的InSet函数(判断是否在一个集合),如果不在一个集合,就调用_AddEdge函数,将矩阵的该位置的大小变为该边的权值大小,然后通过并查集的Union函数(合并),将边的两点合并到一个集合。同时++边数,总权值+=该边权值;如果在一个集合,就不需要进行操作,进行下一轮循环(这里我们打印构成环,方便观察)。

        最后如果边数是n-1,就符合最小生成树的条件,返回总权值,如果不是,那么就没有最小生成树,就返回W(),这个一般是0。

2.Prim(普里姆算法)

        Prim算法与Kruskal算法类似,只不过这个是从任意一个顶点开始,一直覆盖V中所有顶点时为止。



实现:

W Prim(Self& minTree, const W& src)
{
	size_t srci = GetVertexIndex(src);
	size_t n = _vertexs.size();

	minTree._vertexs = _vertexs;
	minTree._indexMap = _indexMap;
	minTree._matrix.resize(n);
	for (size_t i = 0; i < n; ++i)
	{
		minTree._matrix[i].resize(n, MAX_W);
	}

	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;
	// 先把srci连接的边添加到队列中
	for (size_t i = 0; i < n; ++i)
	{
		if (_matrix[srci][i] != MAX_W)
		{
			minq.push(Edge(srci, i, _matrix[srci][i]));
		}
	}

	cout << "Prim开始选边" << endl;
	size_t size = 0;
	W totalW = W();
	while (!minq.empty())
	{
		Edge min = minq.top();
		minq.pop();

		// 最小边的目标点也在X集合,则构成环
		if (X[min._dsti])
		{
			//cout << "构成环:";
			//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
		}
		else
		{
			minTree._AddEdge(min._srci, min._dsti, min._w);
			//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
			X[min._dsti] = true;
			Y[min._dsti] = false;
			++size;
			totalW += min._w;
			if (size == n - 1)
			{
				break;
			}

			for (size_t i = 0; i < n; ++i)
			{
				if (_matrix[min._dsti][i] != MAX_W && Y[i])
				{
					minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
				}
			}
		}
	}

	if (size == n - 1)
	{
		return totalW;
	}
	else
	{
		return W();
	}
}

        先得到数据对应的位置,然后对该图minTree进行初始化,让minTree的顶点集合和顶点映射下标map与要求最小生成树的图相同,同时为minTree的邻接矩阵开辟好空间,并将该矩阵全部初始化为类型最大值。

        接下来定义两个标记数组vector,X中把已经覆盖的顶点设为true,Y中把未被覆盖的顶点设为true,因此在初始化时将X全部初始化为false,将Y全部初始化为true,然后再将当前数据的位置的X变为true,Y变为false。

        再定义一个优先级队列,按照从小到大排序,然后把srci连接的边Edge类添加到队列中(与srci同行的数据中不为类型最大值的)。

        接着定义一个size,用来判断该最小生成树是否为n-1条边,totalW记录最小生成树权值。

        利用优先级队列是否为空来实现一个while循环,得到优先级队列顶部元素(最小的元素),并在优先级队列中删除掉该元素。如果最小边的目标点在X集合中,就说明构成了环;如果不在,就添加边,然后将dsti位置的X变成true,Y变成false(这里不用变srci,因为之前已经变了),同时++边数,总权值+=该边权值。如果size为n-1就满足最小生成树条件了,直接break,如果没有满足,就把dsti边的邻接顶点放入优先级队列中(与dsti同一行且不为类型最大值,同时在Y集合中【不在X集合中】)。

        最后如果边数是n-1,就符合最小生成树的条件,返回总权值,如果不是,那么就没有最小生成树,就返回W(),这个一般是0。

五.最短路径

        从带权的有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短就是沿路径各边的权值总和达到最小。

1.单源最短路径

单源最短路径问题:给定一个图,求源结点到图中每个结点的最短路径。

(1)Dijkstra(迪杰斯特拉算法)

注意:Dijkstra算法要求图中所有边的权重非负。

        针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,源节点到自己的代价是0),Q 为其余未确定最短路径
的结点集合
每次从Q 中找出一个起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S
中,对u 的每一个相邻结点v 进行松弛操作。

        松弛即对每一个相邻结点v ,判断源节点s到结点u的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样。如此一直循环直至集合Q 为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略

 实现:

  

        定义一个u变量,每次选s->u,去选最短路径边那个顶点 ,去更新其连接的路径,u是不是S中的点。

        两个数组:dist[],临时存放s->{syztx}的最短路径;pPath[],存放路径前一个顶点的坐标。

void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
{
	size_t srci = GetVertexIndex(src);
	size_t n = _vertexs.size();
	dist.resize(n, MAX_W);
	pPath.resize(n, -1);

	dist[srci] = 0;
	pPath[srci] = srci;

	// 已经确定最短路径的顶点集合
	vector<bool> S(n, false);
	for (size_t j = 0; j < n; ++j)
	{
		// 选最短路径顶点且不在S的去更新其它路径
		int u = 0;
		W min = MAX_W;
		for (size_t i = 0; i < n; ++i)
		{
			if (S[i] == false && dist[i] < min)
			{
				u = i;
				min = dist[i];
			}
		}

		S[u] = true;
		// 松弛更新u连接顶点v,srci->u + u->v < srci->v 更新
		for (size_t v = 0; v < n; ++v)
		{
			if (S[v] == false && _matrix[u][v] != MAX_W
				&& dist[u] + _matrix[u][v] < dist[v])
			{
				dist[v] = dist[u] + _matrix[u][v];
				pPath[v] = u;
			}
		}
	}
}

        首先得到数据所在位置,并将dist初始化为类型最大值,将pPath初始化为-1,再将dist[srci] = 0,因为自己到自己的最短路径是0,然后再将pPath[srci] = srci,这个是因为自己到自己的前一个顶点坐标就是自己。

        接着定义一个vector为已经确定最短路径的顶点集合,然后定义变量u(去最短路径的那个顶点),以及min(最短路径的大小)。再去选最短路径顶点并且不在S中的,得到去最短路径顶点和最短路径大小min,然后再将u对应的位置S[u]置为true。(因为已经确定了该最短路径)

        然后再进行松弛更新,首先要保证该顶点位置的S为false,并且该位置有边(不为类型最大值),如果 到u顶点的最短路径dist的值+这个两顶点边之间的值 比 前最短路径的值还要小,就说明有了新的最短路径,就对dist[v]进行更新,同时更新上一个顶点路径pPath[v] = u。

(2)Bellman-Ford(贝尔曼-福特算法)

        Dijkstra算法只能用来解决正权图的单源最短路径问题,但有些题目会出现负权图。这时这个算法就不能帮助我们解决问题了,而Bellman-Ford算法可以解决负权图的单源最短路径问题

        它的优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显的缺点,它的时间复杂度 O(N*E) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出来Bellman-Ford就是一种暴力求解更新。

实现:

        两个数组:dist[],存放s->{syztx}的最短路径;pPath[],存放路径前一个顶点的坐标。

bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{
	size_t n = _vertexs.size();
	size_t srci = GetVertexIndex(src);

	// vector<int> dist  记录srci-其它顶点最短路径权值数组
	dist.resize(n, MAX_W);

	// vector<int> pPath 记录srci-其它顶点最短路径父顶点数组
	pPath.resize(n, -1);

	// 先更新srci->srci为缺省值
	dist[srci] = W();

	//cout << "更新边:i->j" << endl;

	// 总体最多更新n轮
	for (size_t k = 0; k < n; ++k)
	{
		// i->j 更新松弛
		bool update = false;
		cout << "更新第:" << k << "轮" << endl;
		for (size_t i = 0; i < n; ++i)
		{
			for (size_t j = 0; j < n; ++j)
			{
				// srci->i + i ->j
				if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
				{
					update = true;
					cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl;
					dist[j] = dist[i] + _matrix[i][j];
					pPath[j] = i;
				}
			}
		}

		// 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了
		if (update == false)
		{
			break;
		}
	}

	// 还能更新就是带负权回路
	for (size_t i = 0; i < n; ++i)
	{
		for (size_t j = 0; j < n; ++j)
		{
			// srci -> i + i -> j
			if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
			{
				return false;
			}
		}
	}
}

        首先得到数据所在位置,并将dist初始化为类型最大值,将pPath初始化为-1,再更新dist[srci]为缺省值(0)。

        然后进行暴力更新,定义一个变量update(用作优化),如果这个轮次没有更新出更短路径,那么后续轮次就不需要继续了。一个双循环,如果该矩阵有边(不为类型最大值),并且到这个路径dist[i] + _matrix[i][j]这个边要小于dist[j]路径,就先将update改为true,再将dist[j]的最短路径改为新的最短路径大小,同时改变pPath为i。

        如果已经更新够了轮次还能继续更新,就说明出现了带负权回来,返回false。

2.多源最短路径

多源最短路径问题:给定一个图,解决任意两点间的最短路径 。 

        Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。
Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节
点。
设k是p的一个中间节点,那么从i到j的最短路径p就被分成i到k和k到j的两段最短路径p1,p2。p1
是从i到k且中间节点属于{1,2,…,k-1}取得的一条最短路径。p2是从k到j且中间节点属于{1,
2,…,k-1}取得的一条最短路径。

(1)Floyd-Warshall(弗洛伊德算法)

        Floyd-Warshall算法本质是三维动态规划,D[i][j][k]表示从点i到点j只经过0到k个点最短路径,然后建立起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得到所有点的最短路径。

实现:

void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
{
	size_t n = _vertexs.size();
	vvDist.resize(n);
	vvpPath.resize(n);

	// 初始化权值和路径矩阵
	for (size_t i = 0; i < n; ++i)
	{
		vvDist[i].resize(n, MAX_W);
		vvpPath[i].resize(n, -1);
	}

	// 直接相连的边更新一下
	for (size_t i = 0; i < n; ++i)
	{
		for (size_t j = 0; j < n; ++j)
		{
			if (_matrix[i][j] != MAX_W)
			{
				vvDist[i][j] = _matrix[i][j];
				vvpPath[i][j] = i;
			}

			if (i == j)
			{
				vvDist[i][j] = W();
			}
		}
	}

	// 最短路径的更新i -> {其它顶点} -> j
	for (size_t k = 0; k < n; ++k)
	{
		for (size_t i = 0; i < n; ++i)
		{
			for (size_t j = 0; j < n; ++j)
			{
				// k作为中间点尝试去更新i->j的路径
				if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W
					&& vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
				{
					vvDist[i][j] = vvDist[i][k] + vvDist[k][j];

					// 找跟j相连的上一个邻接顶点
					// 如果k->j 直接相连,上一个点就k,vvpPath[k][j]存就是k
					// 如果k->j 没有直接相连,k->...->x->j,vvpPath[k][j]存就是x

					vvpPath[i][j] = vvpPath[k][j];
				}
			}
		}
	}
}

        首先初始化权值和路径矩阵,将dist初始化为类型最大值,将pPath初始化为-1。

        然后对直接相连的边进行更新,矩阵不为类型最大值即可进行更新,让vvDist[i][j] = _matrix[i][j],同时更新路径vvpPath[i][j] = i。如果i和j相等说明是同一顶点,路径为0(缺省值)。

        再对其它路径进行更新,3个循环,让k作为中间点尝试去更新i->j的路径,满足vvDist[i][k] 和vvDist[k][j]都不为类型最大值,同时vvDist[i][k] + vvDist[k][j] < vvDist[i][j]时,就将原本的路径vvDist[i][j] = vvDist[i][k] + vvDist[k][j],最后再对路径进行更新,让vvpPath[i][j] = vvpPath[k][j]。

六.总代码

#pragma once

#include <vector>
#include <map>
#include <string>
#include <queue>
#include <functional>

namespace matrix
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;
	public:
		// 图的创建
		// 1.IO输入		-- 不方便测试
		// 2.图结构关系写到文件,读取文件
		// 3.手动添加边
		Graph() = default;

		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)
		{
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				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 (size_t i = 0; i < _vertexs.size(); ++i)
			{
				//cout << i << " ";
				printf("%4d", i);
			}
			cout << endl;

			for (size_t i = 0; i < _matrix.size(); ++i)
			{
				cout << i << " "; // 竖下标
				for (size_t j = 0; j < _matrix[i].size(); ++j)
				{
					//cout << _matrix[i][j] << " ";
					if (_matrix[i][j] == MAX_W)
					{
						//cout << "* ";
						printf("%4c", '*');
					}
					else
					{
						//cout << _matrix[i][j] << " ";
						printf("%4d", _matrix[i][j]);
					}
				}
				cout << endl;
			}
			cout << endl;
		}

		// 广度优先遍历
		void BFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);

			// 队列和标记数组
			queue<int> q;
			vector<bool> visited(_vertexs.size(), false);

			q.push(srci);
			visited[srci] = true;
			int levelSize = 1;

			size_t n = _vertexs.size();
			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 < n; ++i)
					{
						if (_matrix[front][i] != MAX_W)
						{
							if (visited[i] == false)
							{
								q.push(i);
								visited[i] = true;
							}
						}
					}
				}
				cout << endl;

				levelSize = q.size();
			}

			cout << endl;
		}

		// 深度优先遍历
		void _DFS(size_t srci, vector<bool>& visited)
		{
			cout << srci << ":" << _vertexs[srci] << endl;
			visited[srci] = true;

			// 找一个srci相邻的没有访问过的点,去往深度遍历
			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				if (_matrix[srci][i] != MAX_W && visited[i] == false)
				{
					_DFS(i, visited);
				}
			}
		}

		void DFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			vector<bool> visited(_vertexs.size(), false);

			_DFS(srci, visited);
		}

		struct Edge
		{
			size_t _srci;
			size_t _dsti;
			W _w;

			Edge(size_t srci, size_t dsti, const W& w)
				: _srci(srci)
				, _dsti(dsti)
				, _w(w)
			{}

			bool operator>(const Edge& e) const
			{
				return _w > e._w;
			}
		};

		// 克鲁斯卡尔算法求最小生成树
		W Kruskal(Self& minTree)
		{
			size_t n = _vertexs.size();

			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);
			for (size_t i = 0; i < n; ++i)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}

			priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (i < j && _matrix[i][j] != MAX_W)
					{
						minque.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}

			// 选出n-1条边
			int size = 0;
			// W一般是整数、浮点数,W()是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);
					++size;
					totalW += min._w;
				}
				else
				{
					cout << "构成环:";
					cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
			}

			if (size == n - 1)
			{
				return totalW;
			}
			else
			{
				return W();
			}
		}

		// 普里姆算法求最小生成树
		W Prim(Self& minTree, const W& src)
		{
			size_t srci = GetVertexIndex(src);
			size_t n = _vertexs.size();

			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);
			for (size_t i = 0; i < n; ++i)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}

			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;
			// 先把srci连接的边添加到队列中
			for (size_t i = 0; i < n; ++i)
			{
				if (_matrix[srci][i] != MAX_W)
				{
					minq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}

			cout << "Prim开始选边" << endl;
			size_t size = 0;
			W totalW = W();
			while (!minq.empty())
			{
				Edge min = minq.top();
				minq.pop();

				// 最小边的目标点也在X集合,则构成环
				if (X[min._dsti])
				{
					//cout << "构成环:";
					//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
				else
				{
					minTree._AddEdge(min._srci, min._dsti, min._w);
					//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
					X[min._dsti] = true;
					Y[min._dsti] = false;
					++size;
					totalW += min._w;
					if (size == n - 1)
					{
						break;
					}

					for (size_t i = 0; i < n; ++i)
					{
						if (_matrix[min._dsti][i] != MAX_W && Y[i])
						{
							minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
						}
					}
				}
			}

			if (size == n - 1)
			{
				return totalW;
			}
			else
			{
				return W();
			}
		}

		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& pPath)
		{
			size_t srci = GetVertexIndex(src);
			size_t n = _vertexs.size();
			for (size_t i = 0; i < n; ++i)
			{
				if (i != srci)
				{
					// 找出i顶点的路径
					vector<int> path;
					size_t parenti = i;
					while (parenti != srci)
					{
						path.push_back(parenti);
						parenti = pPath[parenti];
					}
					path.push_back(srci);
					reverse(path.begin(), path.end());

					for (auto index : path)
					{
						cout << _vertexs[index] << "->";
					}
					cout << dist[i] << endl;
				}
			}
		}

		// 迪杰斯特拉算法求单源最短路径
		void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
		{
			size_t srci = GetVertexIndex(src);
			size_t n = _vertexs.size();
			dist.resize(n, MAX_W);
			pPath.resize(n, -1);

			dist[srci] = 0;
			pPath[srci] = srci;

			// 已经确定最短路径的顶点集合
			vector<bool> S(n, false);
			for (size_t j = 0; j < n; ++j)
			{
				// 选最短路径顶点且不在S的去更新其它路径
				int u = 0;
				W min = MAX_W;
				for (size_t i = 0; i < n; ++i)
				{
					if (S[i] == false && dist[i] < min)
					{
						u = i;
						min = dist[i];
					}
				}

				S[u] = true;
				// 松弛更新u连接顶点v,srci->u + u->v < srci->v 更新
				for (size_t v = 0; v < n; ++v)
				{
					if (S[v] == false && _matrix[u][v] != MAX_W
						&& dist[u] + _matrix[u][v] < dist[v])
					{
						dist[v] = dist[u] + _matrix[u][v];
						pPath[v] = u;
					}
				}
			}
		}

		// 贝尔曼-福特算法求单源最短路径
		bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
		{
			size_t n = _vertexs.size();
			size_t srci = GetVertexIndex(src);

			// vector<int> dist  记录srci-其它顶点最短路径权值数组
			dist.resize(n, MAX_W);

			// vector<int> pPath 记录srci-其它顶点最短路径父顶点数组
			pPath.resize(n, -1);

			// 先更新srci->srci为缺省值
			dist[srci] = W();

			//cout << "更新边:i->j" << endl;

			// 总体最多更新n轮
			for (size_t k = 0; k < n; ++k)
			{
				// i->j 更新松弛
				bool update = false;
				cout << "更新第:" << k << "轮" << endl;
				for (size_t i = 0; i < n; ++i)
				{
					for (size_t j = 0; j < n; ++j)
					{
						// srci->i + i ->j
						if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
						{
							update = true;
							cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl;
							dist[j] = dist[i] + _matrix[i][j];
							pPath[j] = i;
						}
					}
				}

				// 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了
				if (update == false)
				{
					break;
				}
			}

			// 还能更新就是带负权回路
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					// srci -> i + i -> j
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
					{
						return false;
					}
				}
			}
		}

		// 弗洛伊德算法求多源最短路径
		void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
		{
			size_t n = _vertexs.size();
			vvDist.resize(n);
			vvpPath.resize(n);

			// 初始化权值和路径矩阵
			for (size_t i = 0; i < n; ++i)
			{
				vvDist[i].resize(n, MAX_W);
				vvpPath[i].resize(n, -1);
			}

			// 直接相连的边更新一下
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W)
					{
						vvDist[i][j] = _matrix[i][j];
						vvpPath[i][j] = i;
					}

					if (i == j)
					{
						vvDist[i][j] = W();
					}
				}
			}

			// 最短路径的更新i -> {其它顶点} -> j
			for (size_t k = 0; k < n; ++k)
			{
				for (size_t i = 0; i < n; ++i)
				{
					for (size_t j = 0; j < n; ++j)
					{
						// k作为中间点尝试去更新i->j的路径
						if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W
							&& vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
						{
							vvDist[i][j] = vvDist[i][k] + vvDist[k][j];

							// 找跟j相连的上一个邻接顶点
							// 如果k->j 直接相连,上一个点就k,vvpPath[k][j]存就是k
							// 如果k->j 没有直接相连,k->...->x->j,vvpPath[k][j]存就是x

							vvpPath[i][j] = vvpPath[k][j];
						}
					}
				}
			}
		}


	private:
		vector<V> _vertexs;			// 顶点集合
		map<V, int> _indexMap;		// 顶点映射下标
		vector<vector<W>> _matrix;  // 邻接矩阵
	};
}


namespace link_table
{
	template <class W>
	struct Edge
	{
		// int _srci;
		int _dsti;  // 目标点的下标
		W _w;		// 权值
		Edge<W>* _next;

		Edge(int dsti, const W& w)
			: _dsti(dsti)
			, _w(w)
			, _next(nullptr)
		{}
	};

	template <class V, class W, bool Direction = false>
	class Graph
	{
		typedef Edge<W> Edge;
	public:
		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;
			}

			_tables.resize(n, nullptr);
		}

		// 得到元素位置
		size_t GetVertexIndex(const V& v)
		{
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				throw invalid_argument("顶点不存在");

				return -1;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_t dsti = GetVertexIndex(dst);

			// 1->2
			Edge* eg = new Edge(dsti, w);
			eg->_next = _tables[srci];
			_tables[srci] = eg;

			// 2->1
			// 无向图
			if (Direction == false)
			{
				Edge* eg = new Edge(srci, w);
				eg->_next = _tables[dsti];
				_tables[dsti] = eg;
			}
		}

		void Print()
		{
			// 顶点
			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			for (size_t i = 0; i < _tables.size(); ++i)
			{
				cout << _vertexs[i] << "[" << i << "]->";
				Edge* cur = _tables[i];
				while (cur)
				{
					cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->";
					cur = cur->_next;
				}
				cout << "nullptr" << endl;
			}
		}
	private:
		vector<V> _vertexs;		// 顶点集合
		map<V, int> _indexMap;  // 顶点映射下标
		vector<Edge*> _tables;	// 邻接表
	};
}

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

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

相关文章

windows下运行ROSEFusion

其实LZ已经不再是两年前的Linux小白了&#xff0c;至于为什么要在windows下配置环境&#xff0c;是因为LZ在Linux下已经成功编译成功了ROSEFusion&#xff0c;但运行时一直报错Frame could not be processed&#xff0c;在github下的issue里面查询得知可能是CUDA版本问题。另外…

VM16 Pro+ubuntu20.04.5+GAMIT10.71安装(20230118更新包)

VMware Workstation 16 Proubuntu20.04GAMIT10.71安装测绘老中医&#xff08;qq8212714&#xff09;&#xff08;2023-01-20&#xff09;NOTE:安装环境&#xff1a;Windows 10 专业版&#xff0c;64 位操作系统, 基于 x64 的处理器1、首先在计算机中安装VMware Workstation 16 …

交叉编译(全志)

文章目录一、交叉编译概念二 、香橙派交叉编译3.带WiringPi库的交叉编译一、交叉编译概念 交叉编译&#xff0c;就是&#xff1a;在一种平台上编译&#xff0c;编译出来的程序&#xff0c;是放到别的平台上运行即编译的环境&#xff0c;和运行的环境不一样&#xff0c;属于交叉…

【基础】高低位和大小端

大小端模式出现是为了兼容不同CPU采用的不同的指令集 PowerPC架构和x86架构采用的是不同的CPU指令集。PowerPC采用精简指令集&#xff08;RISC&#xff0c;reduced instruction set computer&#xff09;&#xff0c;x86指的是特定微处理器执行的一些计算机语言指令集。 Powe…

Obsidian 插件(一):DataView 的使用

文章目录DataView 的使用一、 环境配置二、 入门介绍1、 快速开始2、 页面和字段3、 创建查询4、 系统字段三、 接口讲解1、 表达式1.1 概述1.2 表达式类型1.3 特定类型的交互2、 函数2.1 构造器2.2 常用函数2.3 工具函数DataView 的使用 一、 环境配置 首先&#xff0c;我们…

ESP32设备驱动-DHT11温度湿度传感器驱动

DHT11温度湿度传感器驱动 1、DHT11介绍 DHT11数字温湿度传感器是一种复合传感器,包含一个经过校准的温湿度数字信号输出。 应用了专用的数字模块采集技术和温湿度传感技术,确保产品具有高可靠性和优异的长期稳定性。 该传感器包括一个电阻感湿元件和一个 NTC 温度测量装置,…

LeetCode题目笔记——1814. 统计一个数组中好对子的数目

文章目录题目描述题目难度——中等方法一&#xff1a;模拟&#xff08;超时&#xff09;&#xff08;参考&#xff09;代码/Python方法二&#xff1a;优化代码总结题目描述 给你一个数组 nums &#xff0c;数组中只包含非负整数。定义 rev(x) 的值为将整数 x 各个数字位反转得…

Kong Api Gateway

Kong Api Gateway什么是API 网关为什么是 Kong安装Kong通过包管理器来安装启动Kong配置文件详解1、常规配置2、Nginx注入配置3、数据库存储配置4、数据库缓存配置DNS解析器配置其他杂项配置API 管理详解1、查看节点信息2、查看节点状态3、添加服务4、查询服务5、查询所有服务6、…

golang入门笔记——Hertz

文章目录Hertz介绍应用层路由层协议层传输层HZ脚手架Hertz的使用一个简单的案例&#xff1a;利用Hertz监听8080端口并编写/ping的get处理函数Hertz和gin一样&#xff0c;提供了分组路由的功能Hertz路由的匹配优先级&#xff1a;静态路由>命名路由>通配路由参数绑定&#…

SD卡读写实验(SPI模式)

对于 SD 卡的 SPI 模式而言&#xff0c;采用的 SPI 的通信模式为模式 3&#xff0c;即 CPOL1&#xff0c;CPHA1&#xff0c;在 SD 卡 2.0 版本协议中&#xff0c;SPI_CLK 时钟频率可达 50Mhz。SD 卡的 SPI 模式&#xff0c;只用到了 SDIO_D3&#xff08;SPI_CS&#xff09;、SD…

16投影矩阵和最小二乘法

投影矩阵和最小二乘法 投影矩阵 **投影矩阵P与向量b相乘将会把投影到的列空间A中。**那么现在我们来考虑两个极端的例子&#xff0c;这两个极端的例子将会加深我们对投影矩阵的理解。 如果b在矩阵A的列空间里&#xff0c;那么 Pb b 如果b垂直于矩阵A的列空间&#xff0c;那…

经典同步问题

同步问题是一个复杂的问题&#xff0c;但是它也有自己的方法去处理、去分析。PV操作系统的解题思路&#xff1a;关系分析。找出题目中描述的各个进程&#xff0c;分析它们之间的同步、互斥关系。(从事件的角度分析)整理思路。根据各进程的操作流程确定P、V操作的大致顺序。设置…

Java设计模式-备忘录模式、备忘录模式应用场景是什么、又怎么使用

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用&#xff01; 6.11 备忘录模式 6.11.1 定义 又称快照模式&#xff0c;在不破坏封装性的前提下&#xff0c;捕获一个对象的内部状态&#xff0c;并在该对象之外保存此状态&…

深入学习Vue.js(十一)内建组件和模块

文章目录KeepAlive组件的实现原理1.KeepAlive组件实现原理2.KeepAlive组件的代码实现&#xff08;1&#xff09;shouldKeepAlive&#xff08;2&#xff09;keepAliveInstance&#xff08;3&#xff09;keptAlive&#xff08;4&#xff09;move函数3.include和exclude4.缓存策略…

视频生成动画数据OpenPose+OpenCV

我们只是使用OpenPose&#xff0c;不包括深度学习和代码的部分&#xff0c;会用就OK。 1.打开OpenPose的官网&#xff0c;直接进入安装页面&#xff0c;地址如下&#xff1a; OpenPose: OpenPose Doc - Installation 2.安装的说明&#xff0c;大家要好好看&#xff0c;我们就…

吴恩达机器学习课程笔记:多元梯度下降法

1.吴恩达机器学习课程笔记&#xff1a;多元梯度下降法 笔记来源&#xff1a;吴恩达机器学习课程笔记&#xff1a;多元梯度下降法 仅作为个人学习笔记&#xff0c;若各位大佬发现错误请指正 1.1 多元特征&#xff08;变量&#xff09; 每一列代表一个特征&#xff0c;例如&…

【Github CLI】Take GitHub to the command line

目录儿~一、Git、Github、GitLab二、Github CLI——gh2.1 gh简介2.2 gh的使用2.21 Github身份验证&#xff08;必选&#xff09;2.22 常用命令&#xff08;1&#xff09;在Github仓库中打开当前项目&#xff08;2&#xff09;gh配置 gh config&#xff08;3&#xff09;克隆仓库…

(16)go-micro微服务jaeger链路追踪

文章目录一 jaeger链路追踪介绍什么是链路追踪&#xff1a;链路追踪主要功能&#xff1a;二 jaeger链路追踪作用三 jaeger链路追踪主要特性四 jaeger链路追踪原理图1.链路调用原理2. 一次调用链分析3.链路追踪存储与调用五 jaeger链路追踪五个重要组件六 jaeger链路追踪安装1.d…

Junit框架

JUnit 是一个 Java 编程语言的单元测试框架。环境配置创建maven项目&#xff0c;导入Junit配置<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <dependency><groupId>org.junit.jupiter</groupId><artifactId&g…

Linux常用命令——tail命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) tail 在屏幕上显示指定文件的末尾若干行 补充说明 tail命令用于输入文件中的尾部内容。tail命令默认在屏幕上显示指定文件的末尾10行。如果给定的文件不止一个&#xff0c;则在显示的每个文件前面加一个文件名…