【数据结构】图和基本算法

发布于:2024-05-16 ⋅ 阅读:(178) ⋅ 点赞:(0)

1. 图的基本概念

1.1 图本身的定义

图(Graph)是由顶点和顶点之间的关系组成的一种结构,其中顶点(Vertex)和边(Edge)是图的两个要素,所以我们把一个图表示为G=(V, E)

在一个图里面会有若干个顶点,我们描述这些顶点用的是一个集合,在数学上的表示方式就是:**顶点集合V = {x | x术语某个数据对象集} **, V是一个有穷非空集合。

要描述一个边的时候,本质上是在描述两个顶点之间的关系,所以一条边的要素就是对应的两个顶点。对于这个边来说,他有可能是单向的也有可能是双向的,如果这个边是双向的,就用**(x,y)来表示x和y之间的一条双向边(无向边),如果是单向的,就用path(x,y)**来描述一条从x到y的边

在一个图里面会有若干个边,所以边也要组成一个集合,描述方式为**边的集合 E = {(x,y) | x,y属于V} **或者 E = {<x, y> | x,y属于V && path(x, y)}E 是顶点间关系的有穷集合

(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即 Path(x, y)是有方向的。

1.2 相关概念

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

  • 有向图和无向图:有向和无向是边的属性,如果一个图的边是有向的path(x,y)和path(y,x)不是同一条边),那么这个图就被称为是有向图,反之则是无向图. 比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边<x, y>和<y, x>。比

    注意:无向边(x, y)等于有向边<x,y>和<y,x>。

  • 完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边, 则称此图为无向完全图,比如下图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)

image-20240503113859178

  • 路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径
  • 路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和
  • 权值: 边附带的数据信息

image-20240503114413579

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

image-20240503114523926

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

image-20240503114539451

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

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

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

2. 图的存储结构

在上面我们了解到图的基础概念,那么要使用图的话,就需要把他在计算机内描述出来,由于图的要素有节点和边两个,对于图的描述,也就是存储结构,只需要保存节点和边的关系即可. 对于节点的表示,非常简单,使用一段连续的空间即可.主要是对于边的保存怎么处理? 我们有两种方式邻接矩阵邻接表

2.1 邻接矩阵

首先使用一个数组保存所有的顶点,因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。

image-20240503122629890

如上图中G1,A-B之间有一条边,所以在矩阵内[A,B]和[B,A]对应的位置就会被置为1

值得注意的是:

  1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。
  2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替
  3. 用邻接矩阵存储图的有点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路径不是很好求

image-20240503122733489

使用邻接矩阵存储的图结构设计

//  V:顶点类型    W:权值类型   MAX_W:权值的默认值     Direction:是否为有向图 
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
private:
	map<V, int> _vIndexMap; // 顶点和对应下标的映射
	vector<V> _vertexs; // 顶点的集合
	vector<vector<W>> _matrix; // 邻接矩阵
public:
	typedef Graph<V, W, MAX_W, Direction> Self;

	Graph(const V* vertexs, int n)
	{
		// 初始化顶点集合
		for (int i = 0; i < n; ++i)
		{
			_vertexs.push_back(vertexs[i]);
			_vIndexMap.insert({ vertexs[i] , i });
		}
		// 初始化所有的边,默认没有边,所有的权值都为INT_MAX
		int weight = MAX_W;
		if (Direction == false)
			weight = 0; // 无向图用01表示即可
		_matrix.resize(n);
		for (auto& e : _matrix)
		{
			e.resize(n, weight);
		}
	}
	int GetVertexIndex(const V& v)
	{
		auto it = _vIndexMap.find(v);
		if (it == _vIndexMap.end())
			return -1;
		else
			return it->second;
	}
	void AddEdge(const V& src, const V& dst, W w = 1)
	{
		int srcidx = GetVertexIndex(src);
		int dstidx = GetVertexIndex(dst);
		if (srcidx == -1 || dstidx == -1)
		{
			cout << "输入的边有误" << endl;
			return;
		}
		if (Direction == false)
		{
			_matrix[srcidx][dstidx] = _matrix[dstidx][srcidx] = w;
		}
		else
		{
			_matrix[srcidx][dstidx] = w;
		}
	}
	void Print()
	{
		// 打印顶点和下标映射关系
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			cout << _vertexs[i] << "-" << i << " ";
		}
		cout << endl << endl;
		cout << "  ";
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			cout << i << " ";
		}
		cout << endl;
		// 打印矩阵
		for (size_t i = 0; i < _matrix.size(); ++i)
		{
			cout << i << " ";
			for (size_t j = 0; j < _matrix[i].size(); ++j)
			{
				if (_matrix[i][j] != MAX_W)
					cout << _matrix[i][j] << " ";
				else
					cout << "#" << " ";
			}
			cout << endl;
		}
		cout << endl << 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 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();
}

2.2 邻接表

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

  1. 无向图邻接表存储

image-20240503124919599

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

  1. 有向图邻接表存储

image-20240503125047851

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

使用邻接矩阵存储的图结构设计

template<class W>
struct Edge // 边
{
    int srci; // 边的起点编号
    int dsti; // 边的终点编号
    W w; // 边的权值
    Edge(int srci_, int dsti_, W w_) : srci(srci_), dsti(dsti_), w(w_) {}
};

template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
    typedef Edge<W> Edge;
    private:
    map<V, int> _vIndexMap; // 存储顶点到顶点编号的下标
    vector<list<Edge>> _linkTable; // 存储以对应编号为起点的边的链表
    public:
    Graph(const V* vertexs, int n)
    {
        _linkTable.resize(n);
        for (int i = 0; i < n; ++i)
        {
            _vIndexMap[vertexs[i]] = i;
        }
    }
    int GetIndex(const V& v)
    {
        auto ret = _vIndexMap.find(v);
        if (ret != _vIndexMap.end())
            return ret->second;
        else
            return -1;
    }
    void AddEdge(const V& src, const V& dst, W w)
    {
        int srci = GetIndex(src);
        int dsti = GetIndex(dst);
        if (srci == -1 || dsti == -1)
        {
            cout << "找不到指定顶点,插入失败" << endl;
        }
        _linkTable[srci].push_back(Edge(srci, dsti, w));
        if (Direction == false) // 无向图需要添加dsti->srci的边
        {
            _linkTable[dsti].push_back(Edge(dsti, srci, w));
        }
    }
    void Print()
    {
        for (auto& e : _vIndexMap)
        {
            cout << e.first << "-" << e.second << " ";
        }
        cout << endl;
        for (int i = 0; i < _linkTable.size(); ++i)
        {
            cout << i << " : [";
            for (auto& edge : _linkTable[i])
            {
                cout << edge.srci << "->" << edge.dsti << ":" << edge.w << "  ";
            }
            cout << "]" << endl;
        }
    }
};

3. 图的遍历

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

请思考树以前是怎么遍历的,此处可以直接用来遍历图吗?为什么?

3.1 广度优先遍历(BFS)

广度优先遍历就是从一个位置出发,根据他的边的连接关系,一层一层的遍历所有节点

image-20240503125216632

void _BFS(int idx, vector<bool>& check)
{
    queue<int> q;
    q.push(idx);
    check[idx] = true;
    while (!q.empty())
    {
        int tmp = q.front();
        q.pop();
        cout << GetVertex(tmp) << " ";
        for (int i = 0; i < _vertexs.size(); ++i)
        {
            if (_matrix[tmp][i] != MAX_W && check[i] == false)
            {
                q.push(i);
                check[i] = true;
            }
        }
    }
}
void BFS(const V& v)
{
    vector<bool> check(_vertexs.size(), false);
    int idx = GetIndex(v);
    if (idx == -1)
        return;
    _BFS(idx, check); // 从指定节点v处开始遍历、
    // 当一次BFS走完之后,如果在图内有节点与v不联通,那么这些节点将不会被访问,所以接下来找到没有访问的节点进行BFS
    for (int i = 0; i < check.size(); ++i)
    {
        if (check[i] == false)
            _BFS(i, check);
    }
    cout << endl;
}
void Test()
{
	string people[] = { "张三", "李四", "王五", "赵六" };
	Graph<string, int, INT_MAX, false> g(people, 4);
	g.AddEdge("张三", "赵六", 6);
	g.AddEdge("张三", "李四", 9);
	//g.AddEdge("王五", "赵六", 0);
	g.BFS("张三");
}

image-20240513003545923

3.2 深度优先遍历(DFS)

image-20240503132536205

void _DFS(int srci, vector<bool>& check)
{
    cout << GetVertex(srci) << " ";
    check[srci] = true;
    for (int i = 0; i < _vertexs.size(); ++i)
    {
        if (_matrix[srci][i] != MAX_W && check[i] == false)
        {
            _DFS(i, check);
        }
    }
}
void DFS(const V& v)
{
    int idx = GetIndex(v);
    if (idx == -1)
        return;
    vector<bool> check(_vertexs.size(), false); // 标记数组
    _DFS(idx, check);
    // 此时,可能还有一些顶点是没有遍历的(孤岛)
    for (int i = 0; i < check.size(); ++i)
    {
        if (check[i] == false) _DFS(i, check);
    }
    cout << endl;
}
void Test()
{
	string people[] = { "张三", "李四", "王五", "赵六" };
	Graph<string, int, INT_MAX, false> g(people, 4);
	g.AddEdge("张三", "赵六", 6);
	g.AddEdge("张三", "李四", 9);
	//g.AddEdge("王五", "赵六", 0);
	g.Print();
	g.DFS("张三");
}

image-20240513003639018

4. 最小生成树

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

若连通图由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中。如此重复,直到所有顶点在同一个连通分量上为止。

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

image-20240503134623233

W Kruskal(Self& minTree)
{
	// 初始化,minTree中包含所有的顶点,清空所有的边
	minTree = *this;
	for (int i = 0; i < _vertexs.size(); ++i)
	{
		for (int j = 0; j < _vertexs.size(); ++j)
		{
			minTree._matrix[i][j] = MAX_W;
		}
	}
	// 此时,这n个顶点组成、不含任何边的图G={V,NULL}  《==》 minTree
	// 接下来要添加边了
	priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
	int 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)
			{
				pq.push(Edge(i, j, _matrix[i][j]));
			}
		}
	}
	// 此时,所有的边都在pq中,并且排序完成了
	// 从小到大拿出来n-1条边,添加到minTree里面,就完成了最小生成树的构造
	int EdgeCount = n - 1;
	UnionFindSet ufs(n); // 使用并查集判断两个顶点是否已经联通
	W total = W(); // 计算最小生成树的权值
	while (!pq.empty() && EdgeCount)
	{
		Edge front = pq.top(); // 选出当前的最小边
		pq.pop();
		if (ufs.Same(front.srci, front.dsti) == false) // 如果这条边的两个顶点不在同一个集合,就添加这条边
		{
			minTree._AddEdge(front.srci, front.dsti, front.w);
			ufs.Union(front.srci, front.dsti);
			EdgeCount--;
			total += front.w;
			cout << "添加边:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;
		}
		else // 如果这条边的两个顶点在同一个集合,添加将会构成环,所以不能添加
		{
			cout << "构成环:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;
		}
	}
	if (EdgeCount != 0) // 如果最后边的个数不是n-1,那么就证明没有构造完成最小生成树
		return W();
	return total;
}

image-20240513121508386

4.2 Prim算法

image-20240503140202050

W Prim(const V& v, Self& minTree) // Prim算法需要一个起始点
{
	// 初始化,minTree中包含所有的顶点,清空所有的边
	minTree = *this;
	for (int i = 0; i < _vertexs.size(); ++i)
	{
		for (int j = 0; j < _vertexs.size(); ++j)
		{
			minTree._matrix[i][j] = MAX_W;
		}
	}
	W total = W();
	set<int> inSet; // 保存连通在最小生成树中的节点
	int srci = GetIndex(v);
	inSet.insert(srci);
	int n = _vertexs.size();
	priority_queue<Edge, vector<Edge>, greater<Edge>> pq; // 保存边的堆
	for (int i = 0; i < n; ++i)
	{
		if (_matrix[srci][i] != MAX_W) // 让所有以srci为起点的边进堆
		{
			pq.push(Edge(srci, i, _matrix[srci][i]));
		}
	}
	while (!pq.empty())
	{
		// 循环,每次从堆顶拿权值最小的边
		Edge front = pq.top();
		pq.pop();
		if (inSet.find(front.dsti) == inSet.end()) // 如果当前节点的终点不在连通的顶点集合里面
		{
			// 就添加这条边进来
			minTree._AddEdge(front.srci, front.dsti, front.w);
			inSet.insert(front.dsti);
			total += front.w;
			// 添加这条边为起点的所有边
			for (int i = 0; i < n; ++i)
			{
				if (_matrix[front.dsti][i] != MAX_W)
				{
					pq.push(Edge(front.dsti, i, _matrix[front.dsti][i]));
				}
			}
			cout << "添加边:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;
		}
		else
		{
			// cout << "构成环:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;
		}
	}
	if (inSet.size() == n)
		return total;
	else
		return W();
}
void Test_MinTree()
{
	char str[] = "abcdefghi";
	Graph<char, int> g(str, strlen(str));
	g.AddEdge('a', 'b', 4);
	g.AddEdge('a', 'h', 8);
	g.AddEdge('h', 'b', 11);
	g.AddEdge('h', 'i', 7);
	g.AddEdge('h', 'g', 1);
	g.AddEdge('g', 'f', 2);
	g.AddEdge('i', 'c', 2);
	g.AddEdge('c', 'd', 7);
	g.AddEdge('b', 'c', 8);
	g.AddEdge('c', 'f', 4);
	g.AddEdge('d', 'e', 9);
	g.AddEdge('d', 'f', 14);
	g.AddEdge('f', 'e', 10);
	g.AddEdge('i', 'g', 6);
	Graph<char, int> g1, g2;
	cout << "Kruskal:" << endl;
	auto ret = g.Kruskal(g1);
	cout << "权值=" << ret << endl;
	cout << "Prim:" << endl;
	ret = g.Prim('a', g2);
	cout << "权值=" << ret << endl;
}

image-20240513134803535


我们可以发现,不管是Kruskal还是Prim都能够找到最小生成树,但是选的边却并不相同,这是因为对于一个连通图来说,能够产生的最小生成树并不唯一

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和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q 为其余未确定最短路径的结点集合,每次从Q 中找出一个从起点s到该结点代价最小的结点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算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。

image-20240511180310526

// Dijkstra算法是单源的最短路径算法,需要传起点,dist[i]表示编号为i的顶点到起点的最短路径
// 为了方便能够找到某一个节点的路径,这里保存最短路径的情况下的每个节点的上一个节点路径
void Dijkstra(const V& src, vector<W>& dist, vector<int>& parentPath)
{
	int n = _vertexs.size();
	vector<bool> S(n, false); // 标记已经确定最短路径的节点
	int srci = GetIndex(src); // 起点
	dist.resize(n, MAX_W);
	parentPath.resize(n, -1);
	dist[srci] = W(); // 初始化起点到起点的路径
	parentPath[srci] = srci;
	// 更新所有与src顶点相连的顶点的最短路径
	for (int j = 0; j < n; ++j)
	{
		// 选择当前未确定的最短路径去更新新路径
		W minW = MAX_W;
		int u = srci;
		for (int i = 0; i < n; ++i)
		{
			if (S[i] == false && dist[i] < minW)
			{
				u = i;
				minW = dist[i];
			}
		}
		// 此时u就是未确定的最短路径节点,我们现在认为现在的dist[u]就是到u的最短路径
		S[u] = true; 
		// 把所以以u为起点的边对应的终点的最短路径更新   s->u   u->v   ===>   s->v
		for (int v = 0; v < n; ++v)
		{
			if (_matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
			{
				dist[v] = dist[u] + _matrix[u][v];
				parentPath[v] = u;
			}
		}
	}
}
// 将选出的最短路径打印出来
void PrintShortPath(const V& src, const vector<int>& dist, const vector<int>& pPath)
{
	int n = _vertexs.size();
	int srci = GetIndex(src);
	for (int i = 0; i < n; ++i)
	{
		if (i != srci)
		{
			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& idx : path)
			{
				cout << GetVertex(idx) << "->";
			}
			cout << dist[i] << endl;;
		}
	}
}
void Test_Dijkstra()
{
	char str[] = "stxyz";
	Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('s', 't', 10);
	g.AddEdge('s', 'y', 5);
	g.AddEdge('y', 't', 3);
	g.AddEdge('y', 'z', 2);
	g.AddEdge('y', 'x', 9);
	g.AddEdge('t', 'y', 2);
	g.AddEdge('t', 'x', 1);
	g.AddEdge('z', 'x', 6);
	g.AddEdge('z', 's', 7);
	g.AddEdge('x', 'z', 4);
	g.Print();
	vector<int> dist, pPath;
	g.Dijkstra('s', dist, pPath);
	g.PrintShortPath('s', dist, pPath);
}

image-20240514000813796

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

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

image-20240511180434490

void BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath)
{
	dist.resize(_vertexs.size(), MAX_W);
	parentPath.resize(_vertexs.size(), -1);
	int srci = GetIndex(src);
	int n = _vertexs.size();
	dist[srci] = 0;
	parentPath[srci] = srci;
	for (int k = 0; k < n; ++k) // 一遍暴力更新可能会出现问题
		// 问题:由于更新的边顺序不确定,如果节点x的最短路径包含边y->x,在更新完x的最短路径后y的最短路径又更新,此时x的最短路径的权值将不会再次更新
		// 解决方案:将上述暴力更新再进行1次,将会让x的最短路径权值被更新到正确状态,但是依赖x的下一条边将会出问题,所以还需要更新,最终由于一条路径最多有n-1条边,所以需要多更新n-1次
	{
		bool flag = false;
		// 直接进行暴力更新
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				// 更新 i->j的边
				if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
				{
					dist[j] = dist[i] + _matrix[i][j];
					parentPath[j] = i;
					cout << "更新边:" << GetVertex(i) << "->" << GetVertex(j) << ":" << dist[j] << endl;
					flag = true;
				}
			}
		}
		if (flag == false) // 优化:如果本次循环没有更新边,那么后续也就不再需要更新边了
			break;
	}
}

与不带负权的图求最短路径不同的是,带有负权图的最短路径问题可能是没有解的,如果在这个图中形成了一条负权回路(这条回路的路径为负数),此时所有点的最短路径都会无限次更新,因为经过这个负权路径多一次,就会让最短路径边小,所以BellmanFord算法的代码需要更改一点

bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath)
{
	dist.resize(_vertexs.size(), MAX_W);
	parentPath.resize(_vertexs.size(), -1);
	int srci = GetIndex(src);
	int n = _vertexs.size();
	dist[srci] = 0;
	parentPath[srci] = srci;
	for (int k = 0; k < n; ++k) // 一遍暴力更新可能会出现问题
		// 问题:由于更新的边顺序不确定,如果节点x的最短路径包含边y->x,在更新完x的最短路径后y的最短路径又更新,此时x的最短路径的权值将不会再次更新
		// 解决方案:将上述暴力更新再进行1次,将会让x的最短路径权值被更新到正确状态,但是依赖x的下一条边将会出问题,所以还需要更新,最终由于一条路径最多有n-1条边,所以需要多更新n-1次
	{
		bool flag = false;
		// 直接进行暴力更新
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				// 更新 i->j的边
				if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
				{
					dist[j] = dist[i] + _matrix[i][j];
					parentPath[j] = i;
					cout << "更新边:" << GetVertex(i) << "->" << GetVertex(j) << ":" << dist[j] << endl;
					flag = true;
				}
			}
		}
		if (flag == false) // 优化:如果本次循环没有更新边,那么后续也就不再需要更新边了
			break;
	}
	for (int i = 0; i < n; ++i)
	{
		for (int j = 0; j < n; ++j)
		{
			// 检查有没有负权回路:如果更新了n轮之后,还能找到更短的路径,那么就证明图中存在负权回路
			if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
			{
				return false;
			}
		}
	}
	return true;
}

测试代码:

void Test_BellmanFord()
{
	char str1[] = "stxyz";
	Graph<char, int, INT_MAX, true> g1(str1, strlen(str1));
	g1.AddEdge('s', 't', 6);
	g1.AddEdge('s', 'y', 7);
	g1.AddEdge('y', 'z', 9);
	g1.AddEdge('y', 'x', -3);
	g1.AddEdge('z', 's', 2);
	g1.AddEdge('z', 'x', 7);
	g1.AddEdge('t', 'x', 5);
	g1.AddEdge('t', 'y', 8);
	g1.AddEdge('t', 'z', -4);
	g1.AddEdge('x', 't', -2);
	vector<int> dist;
	vector<int> parentPath;
	if (g1.BellmanFord('s', dist, parentPath))
		g1.PrintShortPath('s', dist, parentPath);
	else
		cout << "存在负权回路" << endl;
	cout << endl;
	// 微调图结构,带有负权回路的测试
	char str2[] = "syztx";
	Graph<char, int, INT_MAX, true> g2(str2, strlen(str2));
	g2.AddEdge('s', 't', 6);
	g2.AddEdge('s', 'y', 7);
	g2.AddEdge('y', 'x', -3);
	g2.AddEdge('y', 'z', 9);
	g2.AddEdge('y', 'x', -3);
	g2.AddEdge('y', 's', 1); // 新增
	g2.AddEdge('z', 's', 2);
	g2.AddEdge('z', 'x', 7);
	g2.AddEdge('t', 'x', 5);
	g2.AddEdge('t', 'y', -8); // 更改
	g2.AddEdge('t', 'z', -4);
	g2.AddEdge('x', 't', -2);
	vector<int> dist2;
	vector<int> parentPath2;
	if (g2.BellmanFord('s', dist2, parentPath2))
		g2.PrintShortPath('s', dist2, parentPath2);
	else
		cout << "存在负权回路" << endl;
}

image-20240514161558082

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

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

image-20240511180453715

image-20240511180502855

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

image-20240511180520506

// 使用二维数组存储任意两点之间的最短路径,vvDist[i][j]表示从i到j的最短路径,vvpPath[i][j]表示i到j的最段路径中j的前驱顶点编号
void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
{
    int n = _vertexs.size();
    vvDist.resize(n);
    vvpPath.resize(n);
    for (int i = 0; i < n; ++i)
    {
        vvDist[i].resize(n, MAX_W);
        vvpPath[i].resize(n, -1); // 用-1表示没有连接路径
    }
    // 将所有直接连接的边初始化,
    for (int i = 0; i < n; ++i)
    {
        for (int j = 0; j < n; ++j)
        {
            if (_matrix[i][j] != MAX_W)
            {
                vvDist[i][j] = _matrix[i][j];
                vvpPath[i][j] = i; // 这里认为i->j直接相连
            }
            // 顶点到自己本身的路径为0
            if (i == j)
            {
                vvDist[i][j] = 0;
                vvpPath[i][j] = -1; // 这里认为路径为-1
            }
        }
    }
    // 认为i->j中间最多经过n个节点,依次使用这n个节点进行更新,如果遇到更短的路径就更新
    for (int k = 0; k < n; ++k)
    {
        for (int i = 0; i < n; ++i)
        {
            for (int j = 0; j < n; ++j)
            {
                // i->j 和  i->k  +  k->j 
                if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][j] > vvDist[i][k] + vvDist[k][j])
                {
                    vvDist[i][j] = vvDist[i][k] + vvDist[k][j]; // 更新最短路径
                    // 更新前驱节点,这里我们认为更新之后i到j的路径为i到k再到j,所以i到j中j的前驱节点和k到j中j的前驱节点相同
                    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 Test_FloydWarshall()
{
	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;
	vector<vector<int>> dist, pPath;
	g.FloydWarshall(dist, pPath);
	// 打印从任意一个位置开始的所有节点的最短路径
	for (int i = 0; i < strlen(str); ++i)
	{
		g.PrintShortPath(str[i], dist[i], pPath[i]);
		cout << endl;
	}
}

image-20240515151736981


最后附上本节的所有代码

#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <queue>
using namespace std;

class UnionFindSet
{
private:
	vector<int> _ufs;
public:
	UnionFindSet(int n)
	{
		_ufs.resize(n, -1);
	}
	void Union(int x, int y)
	{
		int xroot = Find(x);
		int yroot = Find(y);
		if (xroot == yroot) return;
		_ufs[xroot] += _ufs[yroot];
		_ufs[yroot] = xroot;
	}
	int Find(int x)
	{
		int root = x;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}
		return root;
	}
	bool Same(int x, int y)
	{
		return Find(x) == Find(y);
	}
};


template<class W>
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 e2) const
	{
		return w > e2.w;
	}
};
//        顶点类型  权值类型  边不存在时的权值        是否是有向图,true表示是
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
	typedef Graph<V, W, MAX_W, Direction> Self;
	typedef Edge<W> Edge;
private:
	map<V, int> _vIndexMap;			// 从顶点映射到下标
	map<int, V> _iVertexMap;        // 从编号映射到顶点
	vector<int> _vertexs;           // 顶点的集合
	vector<vector<W>> _matrix;      // 邻接矩阵
public:
	Graph() = default;
	Graph(V* v, int n) // 初始化图
	{
		_vertexs.resize(n);
		for (int i = 0; i < n; ++i)
		{
			//_IndexMap[v[i]] = i;
			_vIndexMap.insert({ v[i], i });
			_iVertexMap[i] = v[i];
			_vertexs[i] = i;
		}
		_matrix.resize(n);
		for (auto& v : _matrix)
		{
			v.resize(n, MAX_W);
		}
	}
	int GetIndex(const V& v) // 查找v对应的下标,如果没找到就返回-1
	{
		auto it = _vIndexMap.find(v);
		if (it != _vIndexMap.end())
		{
			return it->second;
		}
		else
		{
			return -1;
		}
	}
	void _AddEdge(int srci, int dsti, const W& w)
	{
		_matrix[srci][dsti] = w;
		if (Direction == false) // 无向图多的处理
		{
			_matrix[dsti][srci] = w; // 
		}
	}
	void AddEdge(const V& v1, const V& v2, const W& w) // 添加边
	{
		int idx1 = GetIndex(v1);
		int idx2 = GetIndex(v2);
		if (idx1 == -1 || idx2 == -1)
		{
			cout << "顶点不存在,添加失败" << endl;
		}
		_AddEdge(idx1, idx2, w);
	}
	V GetVertex(const int pos) // 通过下标找到对应顶点
	{
		auto it = _iVertexMap.find(pos);
		if (it != _iVertexMap.end())
		{
			return it->second;
		}
		else
		{
			return V();
		}
	}
	void _BFS(int idx, vector<bool>& check)
	{
		queue<int> q;
		q.push(idx);
		check[idx] = true;
		while (!q.empty())
		{
			int tmp = q.front();
			q.pop();
			cout << GetVertex(tmp) << " ";
			for (int i = 0; i < _vertexs.size(); ++i)
			{
				if (_matrix[tmp][i] != MAX_W && check[i] == false)
				{
					q.push(i);
					check[i] = true;
				}
			}
		}
	}
	void BFS(const V& v)
	{
		vector<bool> check(_vertexs.size(), false);
		int idx = GetIndex(v);
		if (idx == -1)
			return;
		_BFS(idx, check); // 从指定节点v处开始遍历、
		// 当一次BFS走完之后,如果在图内有节点与v不联通,那么这些节点将不会被访问,所以接下来找到没有访问的节点进行BFS
		for (int i = 0; i < check.size(); ++i)
		{
			if (check[i] == false)
				_BFS(i, check);
		}
		cout << endl;
	}
	void _DFS(int srci, vector<bool>& check)
	{
		cout << GetVertex(srci) << " ";
		check[srci] = true;
		for (int i = 0; i < _vertexs.size(); ++i)
		{
			if (_matrix[srci][i] != MAX_W && check[i] == false)
			{
				_DFS(i, check);
			}
		}
	}
	void DFS(const V& v)
	{
		int idx = GetIndex(v);
		if (idx == -1)
			return;
		vector<bool> check(_vertexs.size(), false); // 标记数组
		_DFS(idx, check);
		// 此时,可能还有一些顶点是没有遍历的(孤岛)
		for (int i = 0; i < check.size(); ++i)
		{
			if (check[i] == false) _DFS(i, check);
		}
		cout << endl;
	}

	W Kruskal(Self& minTree)
	{
		// 初始化,minTree中包含所有的顶点,清空所有的边
		minTree = *this;
		for (int i = 0; i < _vertexs.size(); ++i)
		{
			for (int j = 0; j < _vertexs.size(); ++j)
			{
				minTree._matrix[i][j] = MAX_W;
			}
		}
		// 此时,这n个顶点组成、不含任何边的图G={V,NULL}  《==》 minTree
		// 接下来要添加边了
		priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
		int 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)
				{
					pq.push(Edge(i, j, _matrix[i][j]));
				}
			}
		}
		// 此时,所有的边都在pq中,并且排序完成了
		// 从小到大拿出来n-1条边,添加到minTree里面,就完成了最小生成树的构造
		int EdgeCount = n - 1;
		UnionFindSet ufs(n); // 使用并查集判断两个顶点是否已经联通
		W total = W(); // 计算最小生成树的权值
		while (!pq.empty() && EdgeCount)
		{
			Edge front = pq.top(); // 选出当前的最小边
			pq.pop();
			if (ufs.Same(front.srci, front.dsti) == false) // 如果这条边的两个顶点不在同一个集合,就添加这条边
			{
				minTree._AddEdge(front.srci, front.dsti, front.w);
				ufs.Union(front.srci, front.dsti);
				EdgeCount--;
				total += front.w;
				cout << "添加边:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;
			}
			else // 如果这条边的两个顶点在同一个集合,添加将会构成环,所以不能添加
			{
				// cout << "构成环:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;
			}
		}
		if (EdgeCount != 0) // 如果最后边的个数不是n-1,那么就证明没有构造完成最小生成树
			return W();
		return total;
	}
	W Prim(const V& v, Self& minTree) // Prim算法需要一个起始点
	{
		// 初始化,minTree中包含所有的顶点,清空所有的边
		minTree = *this;
		for (int i = 0; i < _vertexs.size(); ++i)
		{
			for (int j = 0; j < _vertexs.size(); ++j)
			{
				minTree._matrix[i][j] = MAX_W;
			}
		}
		W total = W();
		set<int> inSet; // 保存连通在最小生成树中的节点
		int srci = GetIndex(v);
		inSet.insert(srci);
		int n = _vertexs.size();
		priority_queue<Edge, vector<Edge>, greater<Edge>> pq; // 保存边的堆
		for (int i = 0; i < n; ++i)
		{
			if (_matrix[srci][i] != MAX_W) // 让所有以srci为起点的边进堆
			{
				pq.push(Edge(srci, i, _matrix[srci][i]));
			}
		}
		while (!pq.empty())
		{
			// 循环,每次从堆顶拿权值最小的边
			Edge front = pq.top();
			pq.pop();
			if (inSet.find(front.dsti) == inSet.end()) // 如果当前节点的终点不在连通的顶点集合里面
			{
				// 就添加这条边进来
				minTree._AddEdge(front.srci, front.dsti, front.w);
				inSet.insert(front.dsti);
				total += front.w;
				// 添加这条边为起点的所有边
				for (int i = 0; i < n; ++i)
				{
					if (_matrix[front.dsti][i] != MAX_W)
					{
						pq.push(Edge(front.dsti, i, _matrix[front.dsti][i]));
					}
				}
				cout << "添加边:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;
			}
			else
			{
				// cout << "构成环:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;
			}
		}
		if (inSet.size() == n)
			return total;
		else
			return W();
	}
	void Print()
	{
		// 打印顶点和下标映射关系
		for (const auto& e : _vIndexMap)
		{
			cout << e.first << "-" << e.second << " ";
		}
		cout << endl << endl;

		cout << "  ";
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			cout << i << " ";
		}
		cout << endl;
		// 打印矩阵
		for (size_t i = 0; i < _matrix.size(); ++i)
		{
			cout << i << " ";
			for (size_t j = 0; j < _matrix[i].size(); ++j)
			{
				if (_matrix[i][j] != MAX_W)
					cout << _matrix[i][j] << " ";
				else
					cout << "#" << " ";
			}
			cout << endl;
		}
		cout << endl << endl;
		// 打印所有的边
		for (size_t i = 0; i < _matrix.size(); ++i)
		{
			for (size_t j = 0; j < _matrix[i].size(); ++j)
			{
				if (_matrix[i][j] != MAX_W)
				{
					cout << GetVertex(_vertexs[i]) << "-" << GetVertex(_vertexs[j]) << ":" <<
						_matrix[i][j] << endl;
				}
			}
		}
		cout << endl;
	}
	// Dijkstra算法是单源的最短路径算法,需要传起点,dist[i]表示编号为i的顶点到起点的最短路径
	// 为了方便能够找到某一个节点的路径,这里保存最短路径的情况下的每个节点的上一个节点路径
	void Dijkstra(const V& src, vector<W>& dist, vector<int>& parentPath)
	{
		int n = _vertexs.size();
		vector<bool> S(n, false); // 标记已经确定最短路径的节点
		int srci = GetIndex(src); // 起点
		dist.resize(n, MAX_W);
		parentPath.resize(n, -1);
		dist[srci] = W(); // 初始化起点到起点的路径
		parentPath[srci] = srci;
		// 更新所有与src顶点相连的顶点的最短路径
		for (int j = 0; j < n; ++j)
		{
			// 选择当前未确定的最短路径去更新新路径
			W minW = MAX_W;
			int u = srci;
			for (int i = 0; i < n; ++i)
			{
				if (S[i] == false && dist[i] < minW)
				{
					u = i;
					minW = dist[i];
				}
			}
			// 此时u就是未确定的最短路径节点,我们现在认为现在的dist[u]就是到u的最短路径
			S[u] = true; 
			// 把所以以u为起点的边对应的终点的最短路径更新   s->u   u->v   ===>   s->v
			for (int v = 0; v < n; ++v)
			{
				if (_matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
				{
					dist[v] = dist[u] + _matrix[u][v];
					parentPath[v] = u;
				}
			}
		}
	}
	bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath)
	{
		dist.resize(_vertexs.size(), MAX_W);
		parentPath.resize(_vertexs.size(), -1);
		int srci = GetIndex(src);
		int n = _vertexs.size();
		dist[srci] = 0;
		parentPath[srci] = srci;
		for (int k = 0; k < n; ++k) // 一遍暴力更新可能会出现问题
			// 问题:由于更新的边顺序不确定,如果节点x的最短路径包含边y->x,在更新完x的最短路径后y的最短路径又更新,此时x的最短路径的权值将不会再次更新
			// 解决方案:将上述暴力更新再进行1次,将会让x的最短路径权值被更新到正确状态,但是依赖x的下一条边将会出问题,所以还需要更新,最终由于一条路径最多有n-1条边,所以需要多更新n-1次
		{
			bool flag = false;
			// 直接进行暴力更新
			for (int i = 0; i < n; ++i)
			{
				for (int j = 0; j < n; ++j)
				{
					// 更新 i->j的边
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
					{
						dist[j] = dist[i] + _matrix[i][j];
						parentPath[j] = i;
						cout << "更新边:" << GetVertex(i) << "->" << GetVertex(j) << ":" << dist[j] << endl;
						flag = true;
					}
				}
			}
			if (flag == false) // 优化:如果本次循环没有更新边,那么后续也就不再需要更新边了
				break;
		}
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				// 检查有没有负权回路:如果更新了n轮之后,还能找到更短的路径,那么就证明图中存在负权回路
				if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
				{
					return false;
				}
			}
		}
		return true;
	}
	// 使用二维数组存储任意两点之间的最短路径,vvDist[i][j]表示从i到j的最短路径,vvpPath[i][j]表示i到j的最段路径中j的前驱顶点编号
	void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
	{
		int n = _vertexs.size();
		vvDist.resize(n);
		vvpPath.resize(n);
		for (int i = 0; i < n; ++i)
		{
			vvDist[i].resize(n, MAX_W);
			vvpPath[i].resize(n, -1); // 用-1表示没有连接路径
		}
		// 将所有直接连接的边初始化,
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				if (_matrix[i][j] != MAX_W)
				{
					vvDist[i][j] = _matrix[i][j];
					vvpPath[i][j] = i; // 这里认为i->j直接相连
				}
				// 顶点到自己本身的路径为0
				if (i == j)
				{
					vvDist[i][j] = 0;
					vvpPath[i][j] = -1; // 这里认为路径为-1
				}
			}
		}
		// 认为i->j中间最多经过n个节点,依次使用这n个节点进行更新,如果遇到更短的路径就更新
		for (int k = 0; k < n; ++k)
		{
			for (int i = 0; i < n; ++i)
			{
				for (int j = 0; j < n; ++j)
				{
					// i->j 和  i->k  +  k->j 
					if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][j] > vvDist[i][k] + vvDist[k][j])
					{
						vvDist[i][j] = vvDist[i][k] + vvDist[k][j]; // 更新最短路径
						// 更新前驱节点,这里我们认为更新之后i到j的路径为i到k再到j,所以i到j中j的前驱节点和k到j中j的前驱节点相同
						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 PrintShortPath(const V& src, const vector<int>& dist, const vector<int>& pPath)
	{
		int n = _vertexs.size();
		int srci = GetIndex(src);
		for (int i = 0; i < n; ++i)
		{
			if (i != srci)
			{
				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& idx : path)
				{
					cout << GetVertex(idx) << "->";
				}
				cout << dist[i] << endl;
			}
		}
	}
};

void Test()
{
	string people[] = { "张三", "李四", "王五", "赵六" };
	Graph<string, int, INT_MAX, false> g(people, 4);
	g.AddEdge("张三", "赵六", 6);
	g.AddEdge("张三", "李四", 9);
	//g.AddEdge("王五", "赵六", 0);
	g.Print();
	// g.BFS("张三");
	g.DFS("张三");
}

void Test_MinTree()
{
	char str[] = "abcdefghi";
	Graph<char, int> g(str, strlen(str));
	g.AddEdge('a', 'b', 4);
	g.AddEdge('a', 'h', 8);
	g.AddEdge('h', 'b', 11);
	g.AddEdge('h', 'i', 7);
	g.AddEdge('h', 'g', 1);
	g.AddEdge('g', 'f', 2);
	g.AddEdge('i', 'c', 2);
	g.AddEdge('c', 'd', 7);
	g.AddEdge('b', 'c', 8);
	g.AddEdge('c', 'f', 4);
	g.AddEdge('d', 'e', 9);
	g.AddEdge('d', 'f', 14);
	g.AddEdge('f', 'e', 10);
	g.AddEdge('i', 'g', 6);
	Graph<char, int> g1, g2;
	cout << "Kruskal:" << endl;
	auto ret = g.Kruskal(g1);
	cout << "权值=" << ret << endl;
	cout << "Prim:" << endl;
	ret = g.Prim('a', g2);
	cout << "权值=" << ret << endl;
}
void Test_Dijkstra()
{
	char str[] = "stxyz";
	Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('s', 't', 10);
	g.AddEdge('s', 'y', 5);
	g.AddEdge('y', 't', 3);
	g.AddEdge('y', 'z', 2);
	g.AddEdge('y', 'x', 9);
	g.AddEdge('t', 'y', 2);
	g.AddEdge('t', 'x', 1);
	g.AddEdge('z', 'x', 6);
	g.AddEdge('z', 's', 7);
	g.AddEdge('x', 'z', 4);
	g.Print();
	vector<int> dist, pPath;
	g.Dijkstra('s', dist, pPath);
	g.PrintShortPath('s', dist, pPath);
}
void Test_BellmanFord()
{
	char str1[] = "stxyz";
	Graph<char, int, INT_MAX, true> g1(str1, strlen(str1));
	g1.AddEdge('s', 't', 6);
	g1.AddEdge('s', 'y', 7);
	g1.AddEdge('y', 'z', 9);
	g1.AddEdge('y', 'x', -3);
	g1.AddEdge('z', 's', 2);
	g1.AddEdge('z', 'x', 7);
	g1.AddEdge('t', 'x', 5);
	g1.AddEdge('t', 'y', 8);
	g1.AddEdge('t', 'z', -4);
	g1.AddEdge('x', 't', -2);
	vector<int> dist;
	vector<int> parentPath;
	if (g1.BellmanFord('s', dist, parentPath))
		g1.PrintShortPath('s', dist, parentPath);
	else
		cout << "存在负权回路" << endl;
	cout << endl;
	// 微调图结构,带有负权回路的测试
	char str2[] = "syztx";
	Graph<char, int, INT_MAX, true> g2(str2, strlen(str2));
	g2.AddEdge('s', 't', 6);
	g2.AddEdge('s', 'y', 7);
	g2.AddEdge('y', 'x', -3);
	g2.AddEdge('y', 'z', 9);
	g2.AddEdge('y', 'x', -3);
	g2.AddEdge('y', 's', 1); // 新增
	g2.AddEdge('z', 's', 2);
	g2.AddEdge('z', 'x', 7);
	g2.AddEdge('t', 'x', 5);
	g2.AddEdge('t', 'y', -8); // 更改
	g2.AddEdge('t', 'z', -4);
	g2.AddEdge('x', 't', -2);
	vector<int> dist2;
	vector<int> parentPath2;
	if (g2.BellmanFord('s', dist2, parentPath2))
		g2.PrintShortPath('s', dist2, parentPath2);
	else
		cout << "存在负权回路" << endl;
}
void Test_FloydWarshall()
{
	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;
	vector<vector<int>> dist, pPath;
	g.FloydWarshall(dist, pPath);
	// 打印从任意一个位置开始的所有节点的最短路径
	for (int i = 0; i < strlen(str); ++i)
	{
		g.PrintShortPath(str[i], dist[i], pPath[i]);
		cout << endl;
	}
}

int main()
{
	// Test();
	// Test_BellmanFord();
	Test_FloydWarshall();
	return 0;
}

最后注:本节算法图参考自《算法导论》
本节完…