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 最长递增路径
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中所有其它节点的最短路径权重,使用公式:对原图G中所有边的权重进行修正,修正后原来的负权重边已经全部变成了非负权重边(原来的非负权重边仍然是非负权重边,不过权重值可能发生了变化);
第二步,针对修改权重后的图G,遍历每个节点,也就是使每个节点作为源节点,调用dijkatra算法,求出针对该节点的单源最短路径。这里的求解的是修改权重后的图G的单源最短路径,那如何求解原图G的?根据公式:
对修改权重后的图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