【高阶数据结构】图

news2024/11/17 15:35:38

  • 1. 图的基本概念
  • 2. 图的存储结构
    • 2.1 邻接矩阵
    • 2.2 邻接表
    • 2.3 邻接矩阵的实现
    • 2.4 邻接表的实现
  • 3. 图的遍历
    • 3.1 图的广度优先遍历
    • 3.2 图的深度优先遍历
  • 4. 最小生成树
    • 4.1 Kruskal算法
    • 4.2 Prim算法
  • 5. 最短路径
    • 5.1 单源最短路径--Dijkstra算法
    • 5.2 单源最短路径--Bellman-Ford算法
    • 5.3 多源最短路径--Floyd-Warshall算法

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1. 图的基本概念

图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:

顶点集合V = {x|x属于某个数据对象集}是有穷非空集合

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)是有方向的。

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

有向图和无向图:在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边<x, y>和<y, x>

下面是一些常见的图,G2看着是一颗二叉树为什么也说是图呢?
可以这样理解,树是一种特殊(无环连通)的图,图不一定是树。树关注的节点(顶点)中存的值以及连通关系,图关注的是顶点及边的权值。(边由三部分组成:两个顶点、权值)
在这里插入图片描述

树是一种存储式数据结构,节点内存值,然后构成二叉搜索树,AVL树,红黑树。
图是一种表示型数据结构,表示某种场景。

比如说下面的图,顶点可能表示城市,边表示城市之间一个关系(高铁距离、高铁价格、高铁时间。。。)

有了这个东西,提出DFS,BFS遍历,最小生成树(最小代价把图连图),最短路径(一个顶点到其他顶点 或者 多个顶点之间 最短路径)的问题。

在这里插入图片描述

图还可以用来表示社交关系

顶点:人
边:表示两个人是好友
边权值:亲密度等

微信,qq等关系->无向图(强社交关系)
微博,抖音等关系->有向图(弱社交关系、媒体社交)

在这里插入图片描述

完全图(任意两个顶点都有边):在有n个顶点的无向图中,若有n * (n-1)/2条边(n个顶点 1->n-1,2->n-2 … n->0 等差数列),即任意两个顶点之间有且仅有一条边,则称此图为无向完全图,比如上图G1;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如上图G4。

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

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

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

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

在这里插入图片描述

简单路径与回路:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环

在这里插入图片描述

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

在这里插入图片描述

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

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

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

2. 图的存储结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?

顶点我们可以像并查集哪里一样用vector和map保存,那边如何保存呢?

//V 顶点类型,  W 权值类型, Direction  表示有向/无向
template<class V,class W,bool Direction>
class Graph
{

private:
	vector<V> _vertexs;//顶点集合
	map<V, int> _IndexMap;//顶点与下标映射
};

2.1 邻接矩阵

因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将顶点保存(将顶点转化成对应的下标,比如说顶点是abcd编号就是0123),然后采用矩阵来表示节点与节点之间的关系。

在这里插入图片描述

注意:

  1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。
  2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替

在这里插入图片描述
3. 用邻接矩阵存储图的有点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路径不是很好求。

优点:

邻接矩阵存储方式非常适合稠密图
邻接矩阵O(1)判断两个顶点的连接关系,并取到权值

缺点:

相对而言不适合查找一个顶点连接所有边 — O(N)

假设有n个顶点,是不是要所有顶点遍历一遍才知道某个顶点到底和那些顶点相连。
时间复杂度是O(N),N是顶点个数。

假设有100个顶点,我这个顶点只和三个顶点相连只有三条边,但也要遍历100次,能不能有个方法快速把与之相连的三条边都找到呢?

2.2 邻接表

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

邻接表和哈希桶类似。使用一个指针数组,把自己和连接的顶点边都挂在下面。

  1. 无向图邻接表存储

在这里插入图片描述

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

优点:

适合存储稀疏图
适合查找一个顶点连接出去的边

缺点:

不适合确定两个顶点是否相连及权值

  1. 有向图邻接表存储

在这里插入图片描述

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

一般情况下有向图,存储一个出边表即可。

总结一下:邻接矩阵和邻接表其实属于相辅相成,各有优缺点的互补结构。具体还是看场景选择用邻接矩阵和邻接表

2.3 邻接矩阵的实现

//类型模板参数: V 顶点类型(int,char...),  W 权值类型(int,double...), Direction  表示有向/无向(默认无向)
//非类型模板参数: MAX_W  默认权值
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:

private:
	vector<V> _vertexs;        //顶点集合
	map<V, int> _IndexMap;    //顶点与下标映射
	vector<vector<W>> _matrix; //邻接矩阵(边的集合)
};

图的创建有下面几种方式:

  1. IO输入 ------ (自己写不方便测试,oj中更适合)
  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 < n; ++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(const size_t& srci, const 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 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();
}

在这里插入图片描述
完整代码

//类型模板参数: V 顶点类型(int,char...),  W 权值类型(int,double...), Direction  表示有向/无向(默认无向)
//非类型模板参数: MAX_W  默认权值
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
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;
		}

		_matrix.resize(n);
		for (size_t i = 0; i < n; ++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(const size_t& srci, const 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; //邻接矩阵(边的集合)
};

2.4 邻接表的实现

邻接表实际也是一个哈希桶,这里实现很简单

//存储边的信息
template<class W>
struct Edge
{
	size_t _srci;//起始点
	size_t _dsti;//目标点的下标
	W _w;//权值
	Edge<W>* _next;

	Edge(const size_t& srci,const size_t& dsti,const W& w)
		:_srci(srci)
		,_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 size_t& srci, const size_t& dsti,const W& w)
	{
		//头插
		Edge* edge = new Edge(srci, dsti, w);
		edge->_next = _tables[srci];
		_tables[srci] = edge;
		if (Direction == false)  // 无向图
		{
			Edge* new_edge = new Edge(dsti, srci, w);
			new_edge->_next = _tables[dsti];
			_tables[dsti] = new_edge;
		}
	}

	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;

		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; //邻接表(哈希桶)
};

接下来图的遍历,最小生成树,最短路径我们都以邻接矩阵构的图去实现。

3. 图的遍历

给定一个图G和其中任意一个顶点v0,从v0出发,沿着图中各边访问图中的所有顶点,且每个顶点仅被遍历一次。"遍历"即对顶点进行某种操作的意思。

3.1 图的广度优先遍历

有树的基础就知道广度优先遍历必然要借助队列来实现。广度优先遍历就是以某个点为起点,当这个顶点出队列就把和这个顶点的邻接顶点都入队列,然后一层一层往外走。但是要注意的是已经入过队列的顶点下一次不能在入队列,否则就会死循环,因此还要来一个标记bool类型数组。当一个顶点入队列后就标记一下。

在这里插入图片描述

void BFS(const V& src)
{
	size_t srci = GetVertexindex(src);
	size_t n = _vertexs.size();
	//队列和标记数组
	queue<int> q;
	vector<bool> vis(n, false);

	q.push(srci);
	vis[srci] = true;

	while (!q.empty())
	{
		size_t front = q.front();
		q.pop();
		cout << front << ":" << _vertexs[front] << endl;
		//把和front顶点的临界顶点入队列
		for (size_t i = 0; i < n; ++i)
		{
			if (_matrix[front][i] != MAX_W && !vis[i])
			{
				q.push(i);
				vis[i] = true;
			}
		}
	}
}

void TestBDFS()
{
	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("张三");
	//g1.DFS("张三");
}

在这里插入图片描述

接下来看一道题图的BFS应用题

在这里插入图片描述

举一个例子,告诉我们一度好友、二度好友。。。是什么样的,让我们找到小点的六度好友。这就是一个典型的图BFS应用。回想一下刚才我们的BFS顶点出队列是怎么出的?是一个一个出的。对于这道题我们出队列的就要求一层一层出。那怎么一层一层出呢?很简单出队列之前计算一下当前队列内元素的个数。每次出队列内元素个数次。

void BFS(const V& src)
{
	size_t srci = GetVertexindex(src);
	size_t n = _vertexs.size();
	queue<int> q;
	vector<bool> vis(n, false);

	q.push(srci);
	vis[srci] = true;

	while (!q.empty())
	{
		//出队列之前计算队列内元素个数,一层一层出
		size_t k = q.size();
		while (k--)
		{
			size_t front = q.front();
			q.pop();
			cout << front << ":" << _vertexs[front] << endl;
			for (size_t i = 0; i < n; ++i)
			{
				if (_matrix[front][i] != MAX_W && !vis[i])
				{
					q.push(i);
					vis[i] = true;
				}
			}
		}
		
	}
}

3.2 图的深度优先遍历

图的深度优先遍历和树的前序遍历一样。先往深走,当走到不能走的就回溯换条路走,最终直到所有顶点遍历完然后返回。因此我们用递归来实现,这里我们还是需要一个标记bool类型数组,已经访问过的不能在访问否则就会死递归。

在这里插入图片描述

void _DFS(size_t srci, const size_t& n, vector<bool>& vis)
{
	cout << srci << ":" << _vertexs[srci] << endl;
	vis[srci] = true;
	//找一个srci相邻的且没有被访问过的顶点,去往深度遍历
	for (size_t i = 0; i < n; ++i)
	{
		if (_matrix[srci][i] != MAX_W && !vis[i])
		{
			_DFS(i, n, vis);
		}
	}
}

void DFS(const V& src)
{
	size_t srci = GetVertexindex(src);
	size_t n = _vertexs.size();
	vector<bool> vis(n, false);
	_DFS(srci, n, vis);
}

其实这里还有一个遗漏的问题,如果无向图是一个连通图或者有向图是一个强连通图,一次BFS和DFS就可以把所有顶点遍历一遍。但是如果图不是连通的。那以某个点为起点就没有办法一次BFS或者DSF把所有顶点遍历一遍,那如何把图中所有顶点都访问到呢?

在这里插入图片描述

其实很简单,不是有标记数组吗。把标记数组在遍历一遍,如果还有顶点没有被遍历到那就以这个顶点在做一次BFS或DFS。

4. 最小生成树

首先生成树对应的一定是连通图。连通图中找生成树。

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

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

最小生成树:构成生成树的边的权值加起来是最小的。(最小的成本让n个顶点连通)

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路

连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:

  1. 只能使用图中的权值最小的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路

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

贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体

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

最小生成树不唯一,但是权值是唯一的。

4.1 Kruskal算法

给一个有n个顶点的连通网络N={V,E}

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

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

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

其实上面说了这么多,Kruskal算法核心思想:就是每次都从边中选权值最小的边(全局选最小)。

那怎么去选最小的边呢?可以把所有的边拿出来排序。但是这不是好方法。更好的方法就是用优先级队列建一个小堆。每次拿堆顶元素,然后pop堆顶元素,再拿次小的边。

但是这里还有一个问题,可能选的边会造成回路

比如选择 i - g 权值为6的这条边,构成了回路!

如何判断选择的边构成了回路呢?

在这里插入图片描述
使用并查集 -> 判环

刚开始每一个顶点都是一个独立的集合,选边的时候判断一下构成这个边的两个顶点是否是一个集合,如果不是一个集合就可以选这个边。然后把对应的两个顶点合并成一个集合。

在这里插入图片描述

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

	Edge(size_t& srci, size_t& dsti, 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);
	}


	//建小堆,因为Edge是自定义类型,库中的greater不支持自定义类型比较,所以写一个对应的仿函数
	priority_queue<Edge, vector<Edge>, greater<Edge>> heap;
	//并查集
	UnionFindSet ufs(n);

	//将所有边入堆
	for (size_t i = 0; i < n; ++i)
	{
		for (size_t j = 0; j < n; ++j)
		{
			//无向图的邻接矩阵是一个对称矩阵
			//因此只用存矩阵上半部分或者下半部分就行了
			if (i < j && _matrix[i][j] != MAX_W)
			{
				heap.push(Edge(i, j, _matrix[i][j]));
			}
		}
	}

	//选出n-1条边
	size_t sz = 0;
	W total = W();
	while (!heap.empty())
	{
		Edge minedge = heap.top();
		heap.pop();
		//构成边的两个顶点不在一个集合,说明不构成环,可以选
		if (!ufs.IsSet(minedge._srci, minedge._dsti))
		{
			//可以打印一下看选了那条边
			cout << _vertexs[minedge._srci] << "->" << _vertexs[minedge._dsti] << ":" << minedge._w << endl;
			minTree._AddEdge(minedge._srci, minedge._dsti, minedge._w);
			ufs.Union(minedge._srci, minedge._dsti);
			sz++;
			total += minedge._w;
		}
		else
		{
			cout << "构成环:";
			cout << _vertexs[minedge._srci] << "->" << _vertexs[minedge._dsti] << ":" << minedge._w << endl;
		}
	}

	if (sz == n - 1)
	{
		return total;
	}
	else
	{
		return -1;
	}
}



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;
	cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
	kminTree.Print();


	//Graph<char, int> pminTree;
	//cout << "Prim:" << g.Prim(pminTree, 'a') << endl;
	//pminTree.Print();
	//cout << endl;

	//for (size_t i = 0; i < strlen(str); ++i)
	//{
	//	cout << "Prim:" << g.Prim(pminTree, str[i]) << endl;
	//}
}

4.2 Prim算法

Prim算法也是用的贪心,但是它跟Kruskal算法不一样,Kruskal的核心思想是每次都在全局选最小,Prim是给一个起点,然后从这个起点开始去找最小边(局部选最小)。它选边是先把所有顶点归成两个集合,一个集合是已经被选择的顶点(已经加入到最小生成树的顶点),剩下顶点是一个集合。它是在两个集合之间去选最小边。每次都从两个集合各选一个顶点构成的最小边。

在这里插入图片描述

为什么会这样选边的呢?也就说这个地方贪心不是一个全局的贪心,是一个局部的贪心。以某个点为起点去找周围最小的边。而之前是全局贪心。那局部贪心的优势是什么?

它的优势就是选边不会构成环。

它是在两个集合之间去选最小边。每次都从两个集合各选一个顶点构成的最小边。天然避环。

那怎么去区分已经加入到最小生成树的顶点集合和剩余顶点的集合呢?
我们可以搞两个vector,一个X集合,一个Y集合。
X表示已经加入最小生成树顶点的结合
Y表示剩余顶点的集合

刚开始所以顶点都没加入到最小生成树也就是都在Y集合,因此Y集合的所有顶点都标记成true,如果某个顶点加入到最小生成树就把对应顶点从Y中变成false,而在X中变为true。

那如何从X->Y集合连接的边里面选出最小的边?
搞一个优先级队列(小堆)把已经加入最小生成树顶点相连的边加入到队列中,这样去选最小边可不可以?其实不行!

在这里插入图片描述

加入h到X集合的时候 a - h 就已经在一个集合了,这条边就不该在这个队列里面了,但是你又不能控制把它删除。

所以直接用优先级队列也不太好。

第二种方式就是每次去遍历,因为我们这里是矩阵很方便,每次去遍历去找X集合的顶点与Y集合的顶点构成最小的边。但是时间复杂度挺高的。

其实我们还是用优先级队列,不过选边的时候要判一下环,如果选出来最小的边的两个顶点在一个集合是构成环的,不能选!

W Prim(Self& minTree, const V& 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);

	//从X->Y集合中连接的边里面选出最小的边
	vector<bool> X(n,false);
	vector<bool> Y(n, true);
	priority_queue<Edge, vector<Edge>, greater<Edge>> heap;
	size_t srci = GetVertexindex(src);
	//先把srci连接的边添加到队列中
	for (size_t i = 0; i < n; ++i)
	{
		if(_matrix[srci][i] != MAX_W)
			heap.push(Edge(srci, i, _matrix[srci][i]));
	}
	X[srci] = true;
	Y[srci] = false;

	size_t sz = 0;
	W total = W();
	while (!heap.empty())
	{
		Edge minedge = heap.top();
		heap.pop();
		if (!X[minedge._dsti])//每次从两个集合中各选一个顶点构成的最小边,防止成环
		{
			cout << _vertexs[minedge._srci] << "->" << _vertexs[minedge._dsti] << ":" << minedge._w << endl;
			minTree._AddEdge(minedge._srci, minedge._dsti, minedge._w);
			sz++;
			total += minedge._w;
			X[minedge._dsti] = true;
			Y[minedge._dsti] = false;

			for (size_t i = 0; i < n; ++i)
			{
				//已经选过的最小边,不要重复添加
				if (_matrix[minedge._dsti][i] != MAX_W && Y[i])
					heap.push(Edge(minedge._dsti, i, _matrix[minedge._dsti][i]));
			}
		}
		else
		{
			cout << "构成环:";
			cout << _vertexs[minedge._srci] << "->" << _vertexs[minedge._dsti] << ":" << minedge._w << endl;
		}
	}

	if (sz == n - 1)
	{
		return total;
	}
	else
	{
		return -1;
	}

}

5. 最短路径

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

一般而言:
最小生成树 -> 无向图
最短路径 -> 有向图

5.1 单源最短路径–Dijkstra算法

单源最短路径:一个起点到其他所有点,最短路径。

单源最短路径问题:给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图中每个结点v ∈ V v∈Vv∈V的最短路径。Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。

针对一个带权有向图G,将所有结点分为两组S和QS是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点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中,所以该算法使用的是贪心策略。

Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径

比如这里是以s为源点去找和其他点的最短路径。
刚开始S集合里面只有起点s,s到其他起点初始都是∞指的是还没有最短路径。s是自己到自己可以初始为0,初始选择a这个起点,以这个点去做松弛操作,去遍历与s相连的顶点去更新s到其他顶点的最短路径,然后从未被加入到S里面的点里面去选一个从源点到这些顶点的最短路径。以这个顶点开始在去做松弛操作。

Dijkstra算法贪心策略:每次去选从起点->还没加入到最短路径的顶点中去选最短路径那个顶点,去更新其连接的路径(做松弛操作)

用最小的边在去更新其他边也是相对很小。
在这里插入图片描述

如何存储起点到其他顶点最短路径的权值和存储最短路径呢?

它这里用的是抽象表示,用了两个数组,本来是二维的。但是压缩成了一维。
每个顶点都确定过一个下标。根据下标搞了一个数组,把起点到其他顶点的最短路径的权值存储到这个dist数组里。在根据下标搞一个Ppath记录起点到其他顶点的路径,数组里面存的是路径前一个顶点下标。

在这里插入图片描述

//顶点个数是 N, 时间复杂度 O(N^2)  空间复杂度 O(N)
void Dijkstra(const V& src, vector<W>& dist, vector<int>& Ppath)
{
	size_t n = _vertexs.size();
	size_t srci = GetVertexindex(src);
	//dist,记录srci-其他顶点最短路径权值数组
	dist.resize(n, MAX_W);
	//Ppath 记录srci-其他顶点最短路径父顶点数组
	Ppath.resize(n, -1);
	//初始化
	dist[srci] = 0;
	Ppath[srci] = srci;
	//已经确定最短路径的顶点集合
	vector<bool> S(n, false);

	// n个顶点更新N次
	for (size_t i = 0; i < n; ++i)
	{
		//每次去选从起点->还未加入到最短路径的顶点中去选最短路径的那个顶点,去更新其连接的路径(松弛操作)
		W min = MAX_W;
		size_t u = -1;
		for (size_t j = 0; j < n; ++j)
		{
			if (!S[j] && dist[j] < min)
			{
				u = j;
				min = dist[j];
			}
		}
		S[u] = true;//选到的顶点加入到最短路径

		for (size_t v = 0; v < n; ++v)
		{
			//松弛操作,已经加入到最短路径的顶点路径已经是最小不用更新,其他顶点如果 s -> u + u -> v < s -> v 更新
			if (!S[v] && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
			{
				dist[v] = dist[u] + _matrix[u][v];
				Ppath[v] = u;
			}
		}
	}		
}

打印最短路径,各个顶点的最短路径是倒着存的,存的是前一个顶点的下标。我们把路径算出来之后还要逆置一下才能把路径找到。

在这里插入图片描述

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;
			int parent = i;
			while (parent != srci)
			{
				path.push_back(parent);
				parent = Ppath[parent];//前一个顶点的下标
			}
			path.push_back(srci);

			reverse(path.begin(), path.end());

			for (auto& e : path)
			{
				cout << _vertexs[e] << "->";
			}
			cout << "权值和 " << dist[i] << endl;

		}
	}
}

在这里插入图片描述

Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径

可以看到 s -> y 并不是最短路径。Dijkstra算法本身用了一个贪心,如果对已加入到最小路径的顶点更新这个贪心就失效了。如果边的权值都是正的,以其他边去更新已经加入最小路径的顶点就比之前更大没有必要更新,但是有负数就不一样了,贪心就失效了。

在这里插入图片描述

5.2 单源最短路径–Bellman-Ford算法

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

在这里插入图片描述

s -> { j } 其他顶点的集合,要么是直接相连,要么是找中间边边去更新,Dijkstra是去找s->某个顶点特别短那就优先拿这条边去作为起始边去更新,它是我到这个顶点边是最短的,那我从这个顶点到其他顶点也是最短的。Bellman-Ford是去找终止边暴力去更新。

Dijkstra 最小起始边,贪心
Bellman-Ford 终止边,暴力

终止边就是以 i -> j 去进行更新,i -> j 就是图中所有边

s -> { j } 其他顶点,要么直接相连,要么 s -> i i -> j,这个时候仅需要探测s -> i 是通的 i -> j 也是通的 它们加起来比 s -> j 更小,就松弛更新一下。i -> j 代表图中所有边,拿图中所有边去暴力更新一遍。

Bellman-Ford算法借助终止边(i -> j ,图中所有边)暴力更新起点 -> { j } 所有顶点。要么直接相连,要么借助终止边。

但是拿所有边走一遍并不是说就一定能更新出来!

void BellmanFord(const V& src, vector<W>& dist, vector<int>& Ppath)
{
	size_t srci = GetVertexindex(src);
	size_t n = _vertexs.size();
	// vector<W> dist, 记录srci -> 其他顶点最短路径权值数组
	dist.resize(n, MAX_W);
	// vector<int> pPath 记录srci -> 其他顶点最短路径父顶点数组
	Ppath.resize(n, -1);
	// 先更新srci->srci为缺省值
	dist[srci] = W();
	Ppath[srci] = srci;


	// i -> j 更新一轮
	// 借助终止边i->j(图中所有顶点之间的边),更新srci到所有顶点的最小路径(做松弛操作)
	for (size_t i = 0; i < n; ++i)
	{
		for (size_t j = 0; j < n; ++j)
		{
			//srci -> i + i -> j < srci -> j 
			if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
			{
				dist[j] = dist[i] + _matrix[i][j];
				Ppath[j] = i;
			}
		}
	}

}

最短路径似乎更新出来了, 但是为什么s->z的权值不对呢?

在这里插入图片描述

接下来画图分析一下

第一次更新是 s->s s->y,因为更新规则是 dist[i] + _matrix[i][j] < dist[j],我们初始的时候是把 dist[srci] = W()给了初始值。先先更新与s直连的边。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

s->x x->t,这一句的更新就导致了问题。4 + (-2)< 6, s->t 最短路径更新成2,t的前一个顶点变成x。也就是从s到t的路径变成了 s -> y -> x -> t, s -> t 最短路径更新成2。

但是注意到 s -> t 的最短路径更新到2 ,而从 s -> z 要经过t,s -> z 路径因为我们更新了 s -> t 的路径,而变成了 s -> y -> x -> t -> z,但是s -> z 最短路径可没有更新,依旧是上次 s ->(直连) t -> z的最短路径2。所以 s -> t 有了路径更新,但是 s -> t 最短路径没有更新。权值和路径对不上。

在这里插入图片描述

只要你更新出了一条更短路径,可能就会影响其他路径。 如何解决?

s -> z, 在更新一次就变成了 s -> y -> x -> t -> z 的权值 -2了。

在更新一次就修正了,但是新更新路径又可能会影响其他路径,所以还要继续更新,最多更新n轮(极端情况下最多用n条边去更新某一个顶点)。

这里还有一个优化,可能某一轮就不会在更新了也不会影响其他路径。因此可以增加一个标记位,某一轮没有更新就结束更新。

//时间复杂度 O(N^3), 空间复杂度 O(N)
void BellmanFord(const V& src, vector<W>& dist, vector<int>& Ppath)
{
	size_t srci = GetVertexindex(src);
	size_t n = _vertexs.size();
	// vector<W> dist, 记录srci -> 其他顶点最短路径权值数组
	dist.resize(n, MAX_W);
	// vector<int> pPath 记录srci -> 其他顶点最短路径父顶点数组
	Ppath.resize(n, -1);
	// 先更新srci->srci为缺省值
	dist[srci] = W();
	Ppath[srci] = srci;

	// 总体最多更新n轮
	for (size_t k = 0; k < n; ++k)
	{
		//优化
		bool update = false;

		// i -> j 更新一轮
		// 借助终止边i->j(图中所有顶点之间的边),更新srci到所有顶点的最小路径(做松弛操作)
		for (size_t i = 0; i < n; ++i)
		{
			for (size_t j = 0; j < n; ++j)
			{
				//srci -> i + i -> j < srci -> j 
				if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
				{
					//只要更新出一条更短路径,可能会影响其他路径,在更新一次就修正了
					//但是新更新的路径又可能会影响其他路径,所以还要继续更新,最多更新n轮
					dist[j] = dist[i] + _matrix[i][j];
					Ppath[j] = i;
					update = true;
				}
			}
		}
		// 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了
		if (update == false)
			break;
	}
}

在这里插入图片描述

还有一个优化思路,第一个轮次所有边都会参与更新,但是第二个轮次并一定所有边都参与更新,只有那些第一个轮次更新的最短路径的会影响其他路径的,然后第二轮去更新就好了。具体可以搞一个队列优化。

第一轮更新:所有边入队列
后面的轮次:更新出最短路径的边入队列

在这里插入图片描述

在这里插入图片描述

Bellman-Ford算法它的优点是可以解决有负权边的单源最短路径问题,但是解决不了带负权回路的的单源最短路径问题。因此可以用来判断是否有负权回路

s->s 每次都会更新。

在这里插入图片描述

bool BellmanFord(const V& src, vector<W>& dist, vector<int>& Ppath)
{
	size_t srci = GetVertexindex(src);
	size_t n = _vertexs.size();
	// vector<W> dist, 记录srci -> 其他顶点最短路径权值数组
	dist.resize(n, MAX_W);
	// vector<int> pPath 记录srci -> 其他顶点最短路径父顶点数组
	Ppath.resize(n, -1);
	// 先更新srci->srci为缺省值
	dist[srci] = W();
	Ppath[srci] = srci;

	// 总体最多更新n轮
	for (size_t k = 0; k < n; ++k)
	{
		//优化
		bool update = false;

		// i -> j 更新一轮
		// 借助终止边i->j(图中所有顶点之间的边),更新srci到所有顶点的最小路径(做松弛操作)
		for (size_t i = 0; i < n; ++i)
		{
			for (size_t j = 0; j < n; ++j)
			{
				//srci -> i + i -> j < srci -> j 
				if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
				{
					//只要更新出一条更短路径,可能会影响其他路径,在更新一次就修正了
					//但是新更新的路径又可能会影响其他路径,所以还要继续更新,最多更新n轮
					dist[j] = dist[i] + _matrix[i][j];
					Ppath[j] = i;
					update = true;
				}
			}
		}
		// 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了
		if (update == false)
			break;
	}
	
	//更新n轮后还能更新就是带负权回路
	for (size_t i = 0; i < n; ++i)
	{
		for (size_t j = 0; j < n; ++j)
		{
			//srci -> i + i -> j < srci -> j 
			if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
			{
				return false;
			}
		}
	}

	return true;
}

在这里插入图片描述

5.3 多源最短路径–Floyd-Warshall算法

Floyd-Warshall算法是解决图中任意两点间的最短路径的一种算法,也可以解决带负权路径

Dijkstra算法和BellmanFord算法也可以以所有点为起点也可以求出任意两点之间的最短距离。但是Dijkstra算法不能带负权,BellmanFord算法效率低一点。

Floyd-Warshall算法真正优势在于同时更新多源,既然要记录多源的权值和数组那就意味着一维已经不行了,那这个时候就要搞成一个二维的。二维就能记录任意两个点。它和一维的区别就是以前算 srci -> i i -> j 要去矩阵里面取,现在就去dist这个矩阵里面去取。

在这里插入图片描述

Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节点。

任意两点之间要么直接相连,要么最多经过其它点(n - 2个顶点)。

Dijkstra算法是用最小起始边来算
BellmanFord算法是用终止边来算
Floyd-Warshall算法使用中间点来算

设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}取得的一条最短路径。

在这里插入图片描述

Floyd-Warshall算法本质还是用了动态规划。距离都在dsti里面去取,i -> j 要么直接相连,要么经过 ((1…k)集合中的顶点(n-2个顶点)) i -> k,k - > j。取两种情况中的最小值为最短路径。

具体做法如下:

  1. 先将直接相连的 i -> j 的 dist ,Ppath初始化
  2. 最短路径更新,i -> 中间点 -> j,k作为中间点尝试更新 i -> j 的路径, 如果 i -> k,k -> j < i -> j 更新 dist[i][j]和Ppath[i][j],注意如果是i -> k,k -> j < i -> j,Ppath[i][j] 更新要注意,Ppath要的是跟 j 相连的上一个邻接顶点,如果 k 与 j 直接相连 Ppath[k][j]存的就是 k ,如果 k -> j 没有直接相连,k -> … -> x - > j,Ppath[k][j] 存的就是 x。所以Ppath[i][j] = Ppath[k][j] ,而不是直接等于 k。
void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvPpath)
{
	size_t n = _vertexs.size();
	//初始化权值和路径矩阵
	vvDist.resize(n, vector<W>(n, MAX_W));
	vvPpath.resize(n, vector<int>(n, -1));

	//先将之间相连的 i->j 更新一下
	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
	// K严格来说最多是n-2个,但是不能循环n-2次,要循环n次,因为 i -> j 一直在变,要把所有点作为中间点
	// abcdef  a->f k这次是a或者f 对于a->f也没有影响,  a->a a->f,  a->f f->f,
	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];
				}
			}


			// 打印权值和路径矩阵观察数据
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (vvDist[i][j] == MAX_W)
					{
						printf("%3c", '*');
					}
					else
					{
						printf("%3d", vvDist[i][j]);
					}
				}
				cout << endl;

			}
			cout << endl;

			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					printf("%3d", vvPpath[i][j]);
				}
				cout << endl;
			}
			cout << "=================================" << endl;
		}
	}
}

void TestFloydWarShall()
{
	const char* str = "12345";
	Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('1', '2', 3);
	g.AddEdge('1', '3', 8);
	g.AddEdge('1', '5', -4);
	g.AddEdge('2', '4', 1);
	g.AddEdge('2', '5', 7);
	g.AddEdge('3', '2', 4);
	g.AddEdge('4', '1', 2);
	g.AddEdge('4', '3', -5);
	g.AddEdge('5', '4', 6);
	vector<vector<int>> vvDist;
	vector<vector<int>> vvParentPath;
	g.FloydWarshall(vvDist, vvParentPath);

	//打印任意两点之间的最短路径
	for (size_t i = 0; i < strlen(str); ++i)
	{
		g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
		cout << endl;
	}
}

在这里插入图片描述

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

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

相关文章

CVE-2024-34982 LyLme Spage六零导航页 任意文件上传漏洞漏洞分析

代码分析 文件位于/lylme_spage/include/file.php。 这是用于图片文件处理的 <?php /** Description: 图片文件处理* FilePath: /lylme_spage/include/file.php* Copyright (c) 2024 by LyLme, All Rights Reserved.*/ header(Content-Type:application/json); require_o…

【Linux基础】Linux中的开发工具(3)--make/makefile和git的使用

目录 前言一&#xff0c;Linux项目自动化构建工具-make/makefile1. 背景2. 依赖关系和依赖方法3. 项目清理4. 使用方法和原理5. .PHONY的作用6. makefile中符号的使用 二&#xff0c;进度条的实现1. 理解回车换行2. 理解行缓冲区3. 版本14. 版本2 三&#xff0c;Linux上git的使…

文件包含漏洞(一)

本文仅作为学习参考使用&#xff0c;本文作者对任何使用本文进行渗透攻击破坏不负任何责任。 一&#xff0c;漏洞简述。 文件包含漏洞&#xff0c;通常发生在Web应用程序中&#xff0c;特别是那些使用用户输入动态生成内容的部分。这种漏洞允许攻击者通过提交恶意的文件路径请…

AI预测福彩3D采取888=3策略+和值012路或胆码测试8月19日新模型预测第61弹

经过60期的测试&#xff0c;当然有很多彩友也一直在观察我每天发的预测结果&#xff0c;得到了一个非常有价值的信息&#xff0c;那就是9码定位的命中率非常高&#xff0c;60期一共只错了6次&#xff0c;这给喜欢打私房菜的朋友提供了极高价值的预测结果~当然了&#xff0c;大部…

【Neo4j系列】简化Neo4j数据库操作:一个基础工具类的开发之旅

作者&#xff1a;后端小肥肠 在Neo4j系列我打算写一个Neo4j同步关系数据库、增删改查及展示的基础小系统&#xff0c;这篇文件先分享系统里面的基础工具类&#xff0c;感兴趣的可以点个关注&#xff0c;看了文章的jym有更好的方法可以分享在评论区。 创作不易&#xff0c;未经允…

《勇敢的哈克》开发商:为Xbox移植游戏是一场噩梦

国产类银河恶魔城游戏《勇敢的哈克》开发商Blingame在尝试将游戏移植到Xbox上时&#xff0c;遭遇让人头痛的技术障碍&#xff0c;最终他们只能放弃。《勇敢的哈克》是一款融合了类银河恶魔城元素的末日废土风平台动作游戏。你需要利用多功能能量钩爪组合出独特技能&#xff0c;…

Jenkins使用Publish Over SSH插件远程部署程序到阿里云服务器

前言 使用Jenkins远程构建springboot服务到阿里云上&#xff0c;Jenkins版本&#xff1a;Version 2.462.1 1、准备 在可选插件中&#xff0c;搜索Maven Integration &#xff0c;Gitlab和 Publish Over SSH 三个插件&#xff1a; 如果需要多用户管理那就安装&#xff1a;Ro…

【MySQL进阶之路】表结构的操作

目录 创建表 查看表 查看数据库有哪些表 查看表结构 查看表的详细信息 修改表 表的重命名 添加一列 修改某一列的属性 删除某一列 对列进行重命名 删除表 个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 【MySQL进阶之路】MySQL基础——从零认识MySQL-CSDN博客 创…

LLM技术揭秘:Qwen的Function Calling实现解析

简介 Function Calling 是一种让 Chat Completion 模型调用外部函数的能力&#xff0c;可以让模型不仅仅根据自身的数据库知识进行回答&#xff0c;而是可以额外挂载一个函数库&#xff0c;然后根据用户提问去函数库检索&#xff0c;按照实际需求调用外部函数并获取函数运行结…

授权cleanmymac访问全部磁盘 Mac授权访问权限 cleanmymac缺少权限

CleanMyMac是Mac系统下的一款专业的苹果电脑清理软件&#xff0c;同时也是一款优秀的电脑系统管理软件。它能有效清理系统垃圾&#xff0c;快速释放磁盘内存&#xff0c;缓解卡顿现象&#xff0c;保障系统顺畅地运行。 全磁盘访问权限&#xff0c;就好比机场内进行的安全检查。…

Vue3页面组件化开发

本节讲解Vue3页面组件化开发的原因和操作 1.原因 1.业务需求 2.页面展示 根据菜单选项展示不同的页面布局和页面信息 3.页面源码 <template><div class"box"><div class"UserInfo"><div class"UserImg"><img src&…

云计算第三阶段---DBA数据库管理

Day1 一.数据库概述 计算机中数据存放的仓库&#xff0c;可以按数据类型划分数据库&#xff0c;又可以在数据库中划分更细节的分类存放到相应的表中。 抽象来说就是生活中的菜市场、五金店之类的场所&#xff0c;根据用途开设&#xff1b;划分广泛的还有百货商场、批发市场等…

HarmonyOS应用程序访问控制探究

关于作者 白晓明 宁夏图尔科技有限公司董事长兼CEO、坚果派联合创始人 华为HDE、润和软件HiHope社区专家、鸿蒙KOL、仓颉KOL 华为开发者学堂/51CTO学堂/CSDN学堂认证讲师 开放原子开源基金会2023开源贡献之星 一、引言 随着信息技术的飞速发展&#xff0c;移动应用程序已经成为…

【C++】实现日期类相关接口

C语法相关知识点可以通过点击以下链接进行学习一起加油&#xff01;命名空间缺省参数与函数重载C相关特性类和对象-上篇类和对象-中篇类和对象-下篇 本篇将介绍实现日期类中众多接口&#xff0c;同时这其中涉及到很多知识&#xff0c;务必将类和对象相关内容掌握再来实现日期类…

【数据结构】详细介绍栈和队列,解析栈和队列每一处细节

目录 一. 栈 1. 栈的概念 2. 栈的实现 2.1 栈的结构 2.2 初始化栈 2.3 入栈 2.4 出栈 2.5 获取栈顶元素 2.6 获取栈中有效个数 2.7 判断栈是否为空 2.8 销毁栈 二. 队列 1. 队列的概念 2. 队列的实现 2.1 队列的结构 2.2 队列初始化 2.3 销毁队列 2.4 入…

聊聊适配器模式

目录 适配器模式概念 主要实现方式 主要组成 UML用例图 代码示例 生活场景 应用场景 适配器模式概念 适配器模式属于结构型设计模式&#xff0c;它的主要目的是将一个类的接口转换成客户端所期望的另一种接口形式&#xff0c;使得原本接口不兼容的类可以一起工作。 主…

【New SQL】 -- CockroachDB license change

1、CockroachDB 发布了修改开源协议的 releases 北京时间 2024-08-16 &#xff0c;CockroachDB 发布了修改开源协议的 releases。 原文链接&#xff1a;Licensing FAQs Evolving our self-hosted offering and license model CockroachDB License change (again) | Product T…

Kali Linux 定制化魔改 添加常见60渗透工具

项目地址&#xff1a;https://github.com/CuriousLearnerDev/kali-linux-kde-beautify 系统版本&#xff1a;kali linux 2024.1 固件类型&#xff1a;BIOS 用户: zss 密码: ss 完整版 系统压缩大小&#xff1a;18.8 GB 解出来&#xff1a;36.00GB 左右 系统版 系统压缩大小&…

《Cloud Native Data Center Networking》(云原生数据中心网络设计)读书笔记 -- 04路由协议的选择

本章要回答的问题&#xff1a; 路由是如何工作的?有哪些类型的路由协议?Clos 拓扑中的路由协议是如何工作的?什么是无编号接口&#xff0c;以及为什么无编号接口如此重要?如何确定最适合自己需求的路由协议? 路由概述 用最简单的话来说&#xff0c;路由是使用数据包的目…

DESeq2差异基因分析和批次效应移除

差异基因鉴定 基因表达标准化 不同样品的测序量会有差异&#xff0c;最简单的标准化方式是计算counts per million (CPM)&#xff0c;即原始reads count除以总reads数乘以1,000,000。 这种计算方式的缺点是容易受到极高表达且在不同样品中存在差异表达的基因的影响&#xff…