所有节点对的最短路径(《算法导论3rd-P399》)

1 介绍

        求解一个图中所有节点对的最短路径,假设有解,也就是:图中不存在负权重的环。根据前面的文章,主要有两种解法:

        (1)图中存在负权重边,则遍历图中的节点,使其成为起始节点,调用BellmanFord算法即可。 

        (2)图中不存在负权重边,则遍历图中的节点,使其成为起始节点,调用Dijkstra算法即可。 

         BellmanFord和Dijkstra算法:

        单源最短路径(《算法导论3rd-P374》)_hclbeloved的博客-CSDN博客

        但是上面的两种算法的效率不见得高,遂出现了下面的方法。

2 针对稠密图所使用的FolydWarshall算法

2.1 介绍

        有解的前提是:图中可以存在负权重的边,但是不能存在负权重的环。

        求解的思想是:动态规划,且是从小到大的动态规划。

        参考链接:【图论】计算机科学:Floyd-Warshall算法(弗洛伊德算法) - 哔哩哔哩

2.1.1 状态定义

        dp[k][i][j]:表示从源节点i到目的节点j路径的中间节点只能从[0,k-1]的索引范围内进行选取时的最短路径权重。

        注意:

        (1)也就是说此处的k指的就是图的前k个节点,对应的索引范围为:[0,k-1]。       

        (2)中间节点并不一定使用了索引[0,k-1]范围内的所有节点,可能仅使用了其中的一部分。

        (3)若k=0则表示从源节点i到目的节点j路径的中间节点的数量为0,也就是节点i直连节点j,此时dp[0][i][j] = w(i,j)。

2.1.2 状态转移方程

        dp[k-1][i][j]:表示使用索引范围[0,k-2]的节点作为中间节点构成的由i->j的一条最短路径的权重。

        现增加一个索引为k-1的节点,来求解dp[k][i][j]。

        如果由i->j的一条最短路径并没有使用索引为k-1的节点,此时dp[k][i][j] = dp[k-1][i][j];

        如果由i->j的一条最短路径使用了索引为k-1的节点,此时dp[k][i][j] = dp[k-1][i][k] + dp[k-1][k][j];

        所以状态转移方程为:dp[k][i][j] = std::min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j]);

2.1.3 状态转移方程的降维

        k是逐步递增的,并且当前状态只与上一次的状态相关,故状态转移方程降维后为:

dp[i][j] = std::min(dp[i][j], dp[i][k] + dp[k][j]);

2.2 实现

        FloydWarshall算法使用的是图的邻接矩阵实现。

//floyd使用的最优条件是稠密图
class Floyd_Warshall
{
private:
	int n;
	int minPath;
	int maxPath;
	bool hasMinusWeightCycle;
    //图的邻接矩阵实现
	vector<vector<int>> graphMatrix;
	vector<vector<int>> path;
public:
	Floyd_Warshall(int n, vector<vector<pair<int, int>>>& graph);
	Floyd_Warshall(vector<vector<int>>& graph);
	bool HasMinusWeightCycle() { return hasMinusWeightCycle;}
	void PrintPath();
	int GetMinPath() { return minPath; }
	int GetMaxPath() { return maxPath; }
private:
	void Floyd_Warshall_Internal();
	void generatePath(int i, int j, vector<int>& v);
};

//graph是图的邻接边实现,第一维是起点,二维是<终点、开销>
//为了避免图中有孤立的节点,建议传入图中节点的数量
Floyd_Warshall::Floyd_Warshall(int n, vector<vector<pair<int, int>>>& graph)
{
	this->n = n;
	minPath = INT_MAX, maxPath = INT_MIN;
	hasMinusWeightCycle = false;
	//如果两个节点之间没有边相连,则这两个节点之间的权重赋值为INT_MAX
	graphMatrix.assign(n, vector<int>(n, INT_MAX));
	path.assign(n, vector<int>(n, -1));

	for (int i=0;i< graph.size();++i)
	{
		for (auto& item : graph[i])
		{
			graphMatrix[i][item.first] = item.second;
		}
	}

	for (int i=0;i<n;++i)
	{
		//自己到自己的权重为0
		graphMatrix[i][i] = 0;
	}

	Floyd_Warshall_Internal();
}

Floyd_Warshall::Floyd_Warshall(vector<vector<int>>& graph)
{
	n = graph.size();
	minPath = INT_MAX, maxPath = INT_MIN;
	hasMinusWeightCycle = false;
	graphMatrix.swap(graph);
	path.assign(n, vector<int>(n, -1));
	for (int i = 0; i < n; ++i)
	{
		//自己到自己的权重为0
		graphMatrix[i][i] = 0;
	}
	Floyd_Warshall_Internal();
}

void Floyd_Warshall::Floyd_Warshall_Internal()
{
	for (int k=0;k<n;++k)
	{
		// graphMatrix[i][k] 等于 INT_MAX 说明从节点 i 到节点 k 不可达;
		// 同理 graphMatrix[k][j] 等于 INT_MAX 说明从节点 k 到节点 j 不可达;
		// 所以没有必要去做进一步的对比;
		for (int i = 0; i < n; ++i)
		{
			if (graphMatrix[i][k] == INT_MAX)
			{
				continue;
			}

			for (int j = 0; j < n; ++j)
			{
				if (graphMatrix[k][j] == INT_MAX)
				{
					continue;
				}

				int dist = graphMatrix[i][k] + graphMatrix[k][j];
				if (graphMatrix[i][j] > dist)
				{
					graphMatrix[i][j] = dist;
					path[i][j] = k;
					minPath = std::min(minPath, graphMatrix[i][j]);
					maxPath = std::max(maxPath, graphMatrix[i][j]);
					if (i == j && graphMatrix[i][j] < 0)
					{
						hasMinusWeightCycle = true;
						return;
					}
				}
			}
		}
	}
}

void Floyd_Warshall::PrintPath()
{
	for (int i = 0; i < n; ++i)
	{
		for (int j = 0; j < n; ++j)
		{
			if (i == j || graphMatrix[i][j] == INT_MAX)
			{
				continue;
			}

			cout << "The minimum distance from " << i << " to " << j << " is " << graphMatrix[i][j] << endl;
			cout << "The path is: " << i << "->";

			vector<int> v;
			generatePath(i, j, v);
			for (auto& item : v)
			{
				cout << item << "->";
			}

			cout << j << endl << endl;
		}
	}
}

void Floyd_Warshall::generatePath(int i, int j, vector<int>& v)
{
	int k = path[i][j];
	if (k == -1)
	{
		return;
	}
	generatePath(i, k, v);
	v.push_back(k);
	generatePath(k, j, v);
}

2.3 最长递增路径

剑指 Offer II 112. 最长递增路径

class Floyd_Warshall
{
private:
	int n;
	int minPath;
	int maxPath;
	bool hasMinusWeightCycle;
	vector<vector<int>> graphMatrix;
	vector<vector<int>> path;
public:
	Floyd_Warshall(int n, vector<vector<pair<int, int>>>& graph);
	Floyd_Warshall(vector<vector<int>>& graph);
	bool HasMinusWeightCycle() { return hasMinusWeightCycle;}
	void PrintPath();
	int GetMinPath() { return minPath; }
	int GetMaxPath() { return maxPath; }
private:
	void Floyd_Warshall_Internal();
	void generatePath(int i, int j, vector<int>& v);
};

//graph是图的邻接边实现,第一维是起点,二维是<终点、开销>
//为了避免图中有孤立的节点,建议传入图中节点的数量
Floyd_Warshall::Floyd_Warshall(int n, vector<vector<pair<int, int>>>& graph)
{
	this->n = n;
	minPath = INT_MAX, maxPath = INT_MIN;
	hasMinusWeightCycle = false;
	//如果两个节点之间没有边相连,则这两个节点之间的权重赋值为INT_MAX
	graphMatrix.assign(n, vector<int>(n, INT_MAX));
	path.assign(n, vector<int>(n, -1));

	int r = graph.size(), c = graph[0].size();

	for (int i=0;i<r;++i)
	{
		for (auto& item : graph[i])
		{
			graphMatrix[i][item.first] = item.second;
		}
	}

	for (int i=0;i<n;++i)
	{
		//自己到自己的权重为0
		graphMatrix[i][i] = 0;
	}

	Floyd_Warshall_Internal();
}

Floyd_Warshall::Floyd_Warshall(vector<vector<int>>& graph)
{
	n = graph.size();
	minPath = INT_MAX, maxPath = INT_MIN;
	hasMinusWeightCycle = false;
	graphMatrix.swap(graph);
	path.assign(n, vector<int>(n, -1));
	Floyd_Warshall_Internal();
}

void Floyd_Warshall::Floyd_Warshall_Internal()
{
	for (int k=0;k<n;++k)
	{
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				// graphMatrix[i][k] 等于 INT_MAX 说明从节点 i 到节点 k 不可达;
				// 同理 graphMatrix[k][j] 等于 INT_MAX 说明从节点 k 到节点 j 不可达;
				// 所以没有必要去做进一步的对比;
				if (graphMatrix[i][k] == INT_MAX || graphMatrix[k][j] == INT_MAX)
				{
					continue;
				}

				if (graphMatrix[i][j] > graphMatrix[i][k] + graphMatrix[k][j])
				{
					graphMatrix[i][j] = graphMatrix[i][k] + graphMatrix[k][j];
					path[i][j] = k;
					minPath = std::min(minPath, graphMatrix[i][j]);
					maxPath = std::max(maxPath, graphMatrix[i][j]);
					if (i == j && graphMatrix[i][j] < 0)
					{
						hasMinusWeightCycle = true;
						return;
					}
				}
			}
		}
	}
}

void Floyd_Warshall::PrintPath()
{
	for (int i = 0; i < n; ++i)
	{
		for (int j = 0; j < n; ++j)
		{
			if (i == j || graphMatrix[i][j] == INT_MAX)
			{
				continue;
			}

			cout << "The minimum distance from " << i << " to " << j << " is " << graphMatrix[i][j] << endl;
			cout << "The path is: " << i << "->";

			vector<int> v;
			generatePath(i, j, v);
			for (auto& item : v)
			{
				cout << item << "->";
			}

			cout << j << endl << endl;
		}
	}
}

void Floyd_Warshall::generatePath(int i, int j, vector<int>& v)
{
	int k = path[i][j];
	if (k == -1)
	{
		return;
	}
	generatePath(i, k, v);
	v.push_back(k);
	generatePath(k, j, v);
}

class Solution {
public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        // 使用Floyd_Warshall算法,发现数据量上去后仍然超时,甚至还不如针对每一个节点调用一次 dijkstra 逆运用算法
        // floyd使用的最优条件是稠密图
		vector<vector<pair<int, int>>> graph;
		buildGraph(matrix, graph);

		Floyd_Warshall floyd(graph.size(), graph);		
		return floyd.GetMaxPath()+1;
    }

    void buildGraph(vector<vector<int>>& matrix, vector<vector<pair<int,int>>>& graph)
    {
        int n = matrix.size(), m = matrix[0].size(), from = 0, to = 0;
        vector<pair<int,int>> neighbours{{-1,0},{1,0},{0,-1},{0,1}};
        if (n * m == 0)
        {
            graph.resize(n + m);
        }
        else
        {
            graph.resize(n * m);
        }        
    
        for (int i=0;i<n;++i)
        {
            for (int j=0;j<m;++j)
            {
                from = m * i + j;
                for (auto& item : neighbours)
                {
                    int x = i + item.first, y = j + item.second;
                    if (x < 0 || x >= n || y < 0 || y >= m || matrix[x][y] <= matrix[i][j])
                        continue;                
                    
                    to = m * x + y;
                    graph[from].push_back({to, 1});
                }                
            }
        }        
    } 
};

3 针对稀疏图所使用的Johnson算法

        参考链接:Johnson算法学习笔记 - dsjkafdsaf - 博客园

        该链接中有些错误,请注意,下面已经进行了修正。

3.1 介绍

        johnson算法在原图G的基础上增加s节点构成图G1,通过将s节点到图G中的所有节点的权重设置为0来构建图G1。

        针对图G1,以s作为源节点,利用BellmanFordSpfa算法,求解单源最短路径。根据s节点到图G1中所有其它节点的最短路径权重,使用公式:\hat{w}(u,v)=w(u,v)+dist(s,u)-dist(s,v)对原图G中所有边的权重进行修正,修正后原来的负权重边已经全部变成了非负权重边(原来的非负权重边仍然是非负权重边,不过权重值可能发生了变化);

        第二步,针对修改权重后的图G,遍历每个节点,也就是使每个节点作为源节点,调用dijkatra算法,求出针对该节点的单源最短路径\hat{dist}(u,v)。这里的\hat{dist}(u,v)求解的是修改权重后的图G的单源最短路径,那如何求解原图G的dist(u,v)?根据公式:dist(u,v)=\hat{dist}(u,v)-(dist(s,u) - dist(s,v))=\hat{dist}(u,v)+dist(s,v)-dist(s,u)

        对修改权重后的图G中的每个节点调用完dijkatra算法后,又通过上述公式修正到了原图G的单源最短路径的值。至此,可以求解出所有节点对的最短路径。

3.2 实现      

        Johnson算法使用的是图的邻接链表的实现。

#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <queue>
using namespace std;

//参考链接:https://zhuanlan.zhihu.com/p/357580063
/* SPFA(shortest path faster algorithm)是对 bellmon-ford 的一个改进
从上面的介绍我们知道bellmon-ford算法是带着一定的盲目性的,作为对它的优化,spfa采用类似bfs的思想,使用一个队列,只松弛那些可能更新点的距离的边。算法的流程为:

将除源点之外的所有的点当前距离初始化为无穷,并标记为未入队。源点的当前距离为0,将源点入队。
取出队首u,遍历u的所有出边,检查是否能更新所连接的点v的当前距离。如果v的当前距离被更新并且v不在队中,则将v入队。重复该操作直到队列为空。
检查是否存在负权环的方法为:记录一个点的入队次数,如果超过V-1次说明存在负权环,因为最短路径上除自身外至多V-1个点,故一个点不可能被更新超过V-1次。
*/
class BellmanFordSpfa {
private:
	//判断图中是否存在负权重环
	bool hasMinusCycle;
	//起始节点的索引
	int start;
	int num;
	// distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重
	vector<int> distanceTo;
	// graph 是用邻接表表示的一幅图,原来的邻接表保存的是邻接节点,这里保存的是邻接边,邻接边包含一个终点和一个权重
	// graph[s] 记录节点 s 所有相邻的边,s就是这条边的起点,pair[to:weight]
	vector<vector<pair<int, int>>> graph;

private:
	//u->v的有向图
	void relax(int u, int v, int w)
	{
		if (distanceTo[u] != INT_MAX)
		{
			int distToV = distanceTo[u] + w;
			if (distanceTo[v] > distToV)
			{
				distanceTo[v] = distToV;
			}
		}
	}

	void relaxSpfa(int u, int v, int w, queue<int>& q, vector<bool>& inque)
	{
		if (distanceTo[u] != INT_MAX)
		{
			int distToV = distanceTo[u] + w;
			if (distanceTo[v] > distToV)
			{
				distanceTo[v] = distToV;
				//如果v不在队列 q 中,则加入到 q 中
				if (!inque[v])
				{
					q.push(v);
					inque[v] = true;
				}
			}
		}
	}

	bool checkMinusCycle(int u, int v, int w)
	{
		do
		{
			if (hasMinusCycle)
			{
				break;
			}

			if (distanceTo[u] == INT_MAX)
			{
				hasMinusCycle = true;
				break;
			}

			int distToV = distanceTo[u] + w;
			if (distanceTo[v] > distToV)
			{
				hasMinusCycle = true;
				break;
			}
		} while (0);

		return hasMinusCycle;
	}

public:
	//为了避免图中有孤立的节点,建议传入图中节点的数量
	BellmanFordSpfa(int n, int start, vector<vector<pair<int, int>>>& graph)
	{
		hasMinusCycle = false;
		num = n;
		this->graph = graph;
		this->start = start;
		distanceTo.assign(n, INT_MAX);
		// base case,start 到 start 的最短距离就是 0
		distanceTo[start] = 0;

		queue<int> q;
		vector<bool> inque(n, false);
		q.push(start);
		inque[start] = true;

		while (!q.empty())
		{
			int u = q.front();
			q.pop();
			inque[u] = false;

			for (auto& item : graph[u])
			{
				relaxSpfa(u, item.first, item.second, q, inque);
			}
		}

		//检测图中是否存在负权重的环,需要遍历所有的边
		for (int i = 0; i < graph.size(); ++i)
		{
			for (auto& item : graph[i])
			{
				if (checkMinusCycle(i, item.first, item.second))
				{
					return;
				}
			}
		}
	}

	int getDistanceTo(int end)
	{
		return distanceTo[end];
	}

	bool minusCycle()
	{
		return hasMinusCycle;
	}
};

class Dijkstra {
private:
	struct cmp
	{
		//[0]: 当前节点, [1]: 从start到达当前节点的最小路径权重
		bool operator () (const pair<int, int>& a, const pair<int, int>& b)
		{
			return a.second > b.second;
		}
	};
	//起始节点的索引
	int start;
	// 基于权重的最小堆,distFromStart 较小的排在前面
	priority_queue < pair<int, int>, vector<pair<int, int>>, cmp> minHeap;
	// distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重
	vector<int> distanceTo;
	// graph 是用邻接表表示的一幅图,原来的邻接表保存的是邻接节点,这里保存的是邻接边,邻接边包含一个终点和一个权重
	// graph[s] 记录节点 s 所有相邻的边,s就是这条边的起点,pair{to, weight}
	vector<vector<pair<int, int>>> graph;

public:
	//为了避免图中有孤立的节点,建议传入图中节点的数量
	Dijkstra(int n, int start, vector<vector<pair<int, int>>>& graph)
	{
		this->graph = graph;
		this->start = start;
		distanceTo.assign(n, INT_MAX);
		// base case,start 到 start 的最短距离就是 0
		distanceTo[start] = 0;
		// 从起点 start 开始进行 BFS
		minHeap.push(make_pair(start, 0));

		while (!minHeap.empty())
		{
			auto currNode = minHeap.top();
			minHeap.pop();
			int curNodeID = currNode.first;

			// 将 curNode 的相邻节点装入队列
			for (auto& neighbor : graph[curNodeID])
			{
				int nextNodeID = neighbor.first;
				int distToNextNode = distanceTo[curNodeID] + neighbor.second;
				// 更新 distanceTo
				if (distanceTo[nextNodeID] > distToNextNode)
				{
					distanceTo[nextNodeID] = distToNextNode;
					minHeap.push(make_pair(nextNodeID, distToNextNode));
				}
			}
		}
	}

	int getDistanceTo(int end)
	{
		return distanceTo[end];
	}
};

class Johnson
{
private:
	int n;
	// graph 是用邻接表表示的一幅图,原来的邻接表保存的是邻接节点,这里保存的是邻接边,邻接边包含一个终点和一个权重
	// graph[s] 记录节点 s 所有相邻的边,s就是这条边的起点,pair[to:weight]
	vector<vector<pair<int, int>>> sGraph;

public:
	Johnson(int n, vector<vector<pair<int, int>>>& graph)
	{
		this->n = n;
		this->sGraph = graph;
	}

	vector<vector<int>> AllNodesPairShortestPath()
	{
		vector<vector<int>> dist(n, vector<int>(n,0));
		//增加一个额外的 s 节点,节点的变号为 n,也就是说增加一个节点后,节点的数量变成了 n+1
		//增加节点 s 到图中所有其它节点的边,这些边权重全部为0
		sGraph.resize(n + 1);
		for (int i = 0; i < n; ++i)
		{
			sGraph[n].push_back({ i, 0 });
		}

		//BellmanFordSpfa算法
		BellmanFordSpfa spfa(n + 1, n, sGraph);
		//更新图中原始边的权重
		if (!spfa.minusCycle())
		{
			for (int i = 0; i < n; ++i)
			{
				for (auto& item : sGraph[i])
				{
					item.second += spfa.getDistanceTo(i) - spfa.getDistanceTo(item.first);
				}
			}

			//Dijkstra算法
			for (int i = 0; i < n; ++i)
			{
				Dijkstra dijkstra(n, i, sGraph);
				for (auto& item : sGraph[i])
				{
					if (dijkstra.getDistanceTo(item.first) == INT_MAX)
					{
						continue;
					}
					dist[i][item.first] = dijkstra.getDistanceTo(item.first) + spfa.getDistanceTo(item.first) - spfa.getDistanceTo(i);
				}
			}			
		}

		return dist;
	}
};

int main()
{
	//<算法导论-3rd-p410>测试例子
	vector<vector<pair<int, int>>> graph{ {{1,3},{2,8},{4,-4}}, {{3,1},{4,7}},{{1,4}},{{0,2},{2,-5}},{{3,6}} };
	int n = 5;
	Johnson json(n, graph);
	vector<vector<int>> r = json.AllNodesPairShortestPath();

	return 0;
}

4

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值