1 介绍
单源最短路径针对的是带权重的有向图;
单源最短路径有解的前提是:有向图中不存在从起点可达的权重和为负值的环路。
2 求解单源最短路径的方法
(1)Bellman-Foxd算法:求解一般情况下的单源最短路径问题,权重可以为负值。
(2)针对有向无环图(DAG)的一种线性方法:使用拓扑排序,然后进行relax。
(3)针对权重为非负值的有向图:dijkstra算法
3 dijkstra算法
3.1 dijkstra算法描述
算法直接参考《算法导论3rd-P383》即可。
3.2 dijkstra算法实现
class Dijkstra {
private:
struct cmp
{
//[0]: 当前节点, [1]: 从start到达当前节点的最小路径权重, distFromStart 较小的排在前面
bool operator () (const pair<int, int>& a, const pair<int, int>& b)
{
return a.second > b.second;
}
};
//起始节点的索引
int start;
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];
}
};
4 dijkstra算法应用
4.1 网络延迟时间
/*
让你求所有节点都收到信号的时间,你把所谓的传递时间看做距离,实际上就是问你
「从节点k到其他所有节点的最短路径中,最长的那条最短路径距离是多少」,
说白了就是让你算从节点k出发到其他所有节点的最短路径,就是标准的 Dijkstra 算法。
*/
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<pair<int,int>>> graph;
buildGraph(times, n, graph);
//启动 dijkstra 算法计算以节点 k 为起点到其他节点的最短路径
//由于创建图的邻接表使用的是从0开始,所以这里对k进行减一操作
vector<int> r = dijkstra(n, k-1, graph);
//找到最长的那一条最短路径
int res = 0;
for (int i = 0; i < n; ++i)
{
if (r[i] == INT_MAX)
{
// 有节点不可达,返回 -1
return -1;
}
res = std::max(res, r[i]);
}
return res;
}
void buildGraph(vector<vector<int>>& times, int n, vector<vector<pair<int,int>>>& graph)
{
graph.resize(n);
for (auto& v : times)
{
//n 个网络节点,标记为 1 到 n,所以做了减一操作
graph[v[0] - 1].push_back(make_pair(v[1] - 1, v[2]));
}
}
//graph是图的邻接边实现,第一维是起点,二维是<终点、开销>
//为了避免图中有孤立的节点,建议传入图中节点的数量
vector<int> dijkstra(int n, int start, vector<vector<pair<int,int>>>& graph)
{
// 定义:distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重
vector<int> distTo(n, INT_MAX);
// base case,start 到 start 的最短距离就是 0
distTo[start] = 0;
// 优先级队列,distFromStart 较小的排在前面
struct cmp
{
//[0]: 当前节点, [1]: 从start到达当前节点的最小路径权重
bool operator () (const pair<int,int>& a, const pair<int,int>& b)
{
return a.second > b.second;
}
};
//构建基于权重的最小堆
priority_queue<pair<int,int>, vector<pair<int,int>>, cmp> minHeap;
// 从起点 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 = distTo[curNodeID] + neighbor.second;
// 更新 distTo
if (distTo[nextNodeID] > distToNextNode)
{
distTo[nextNodeID] = distToNextNode;
minHeap.push(make_pair(nextNodeID, distToNextNode));
}
}
}
return distTo;
}
};
使用Dijkstra类进行求解:
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<pair<int, int>>> graph;
buildGraph(times, n, graph);
//启动 dijkstra 算法计算以节点 k 为起点到其他节点的最短路径
//由于创建图的邻接表使用的是从0开始,所以这里对k进行减一操作
Dijkstra dijkstra(n, k - 1, graph);
//找到最长的那一条最短路径
int res = 0;
for (int i = 0; i < n; ++i)
{
if (dijkstra.getDistanceTo(i) == INT_MAX)
{
// 有节点不可达,返回 -1
return -1;
}
res = std::max(res, dijkstra.getDistanceTo(i));
}
return res;
}
};
4.2 最小体力消耗路径
class Solution {
public:
int minimumEffortPath(vector<vector<int>>& heights) {
vector<vector<pair<int,int>>> graph;
buildGraph(heights, graph);
return dijkstra(heights.size() * heights[0].size(), 0, graph);
}
void buildGraph(vector<vector<int>>& heights, vector<vector<pair<int,int>>>& graph)
{
int r = heights.size(), c = heights[0].size(), from = 0, to = 0, diff = 0;
graph.resize(r*c);
for (int i=0;i<r;++i)
{
for (int j=0;j<c;++j)
{
from = c*i + j;
if (i > 0)
{
to = c*(i-1) + j;
diff = abs(heights[i][j] - heights[i-1][j]);
graph[from].push_back(make_pair(to, diff));
}
if (i < r-1)
{
to = c*(i+1) + j;
diff = abs(heights[i][j] - heights[i+1][j]);
graph[from].push_back(make_pair(to, diff));
}
if (j > 0)
{
to = c*i + j - 1;
diff = abs(heights[i][j] - heights[i][j-1]);
graph[from].push_back(make_pair(to, diff));
}
if (j < c-1)
{
to = c*i + j + 1;
diff = abs(heights[i][j] - heights[i][j+1]);
graph[from].push_back(make_pair(to, diff));
}
}
}
}
//graph是图的邻接边实现,第一维是起点,二维是<终点、开销>
//为了避免图中有孤立的节点,建议传入图中节点的数量
int dijkstra(int n, int start, vector<vector<pair<int,int>>>& graph)
{
// 定义:distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重
vector<int> distTo(n, INT_MAX);
// base case,start 到 start 的最短距离就是 0
distTo[start] = 0;
// 优先级队列,distFromStart 较小的排在前面
struct cmp
{
//[0]: 当前节点, [1]: 从start到达当前节点的最小路径权重
bool operator () (const pair<int,int>& a, const pair<int,int>& b)
{
return a.second > b.second;
}
};
//构建基于权重的最小堆
priority_queue<pair<int,int>, vector<pair<int,int>>, cmp> minHeap;
// 从起点 start 开始进行 BFS
minHeap.push(make_pair(start, 0));
while (!minHeap.empty())
{
auto currNode = minHeap.top();
minHeap.pop();
int curNodeID = currNode.first;
if (curNodeID == n-1)
return distTo[curNodeID];
// 将 curNode 的相邻节点装入队列
for (auto& neighbor : graph[curNodeID])
{
int nextNodeID = neighbor.first;
// int distToNextNode = distTo[curNodeID] + neighbor.second;
int distToNextNode = std::max(distTo[curNodeID], neighbor.second);
// 更新 distTo
if (distTo[nextNodeID] > distToNextNode)
{
distTo[nextNodeID] = distToNextNode;
minHeap.push(make_pair(nextNodeID, distToNextNode));
}
}
}
return distTo[n-1];
}
};
4.3 概率最大的路径
class Solution {
public:
double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {
vector<vector<pair<int, double>>> graph;
buildGraph(n, edges, succProb, graph);
return dijkstra(n, start, end, graph);
}
void buildGraph(int n, vector<vector<int>>& edges, vector<double>& succProb, vector<vector<pair<int, double>>>& graph)
{
graph.resize(n);
for (int i = 0; i < edges.size(); ++i)
{
int from = edges[i][0], to = edges[i][1];
graph[from].push_back(make_pair(to, succProb[i]));
graph[to].push_back(make_pair(from, succProb[i]));
}
}
//graph是图的邻接边实现,第一维是起点,二维是<终点、开销>
//为了避免图中有孤立的节点,建议传入图中节点的数量
double dijkstra(int n, int start, int end, vector<vector<pair<int, double>>>& graph)
{
// 定义:distTo[i] 的值就是起点 start 到达节点 i 的最小概率(概率此处转换成了负数,负数最小也就是正数最大)
vector<double> distTo(n, INT_MAX);
// base case,start 到 start 的最小概率设置为-1,主要起到一个变号的作用
distTo[start] = -1;
// 优先级队列,distFromStart 较小的排在前面
struct cmp
{
//[0]: 当前节点, [1]: 从start到达当前节点的最小概率
bool operator () (const pair<int, double>& a, const pair<int, double>& b)
{
return a.second > b.second;
}
};
//构建基于权重的最小堆
priority_queue<pair<int, double>, vector<pair<int, double>>, cmp> minHeap;
// 从起点 start 开始进行 BFS
minHeap.push(make_pair(start, -1));
while (!minHeap.empty())
{
auto currNode = minHeap.top();
minHeap.pop();
int curNodeID = currNode.first;
if (curNodeID == end)
return -distTo[curNodeID];
// 将 curNode 的相邻节点装入队列
for (auto& neighbor : graph[curNodeID])
{
int nextNodeID = neighbor.first;
// int distToNextNode = distTo[curNodeID] + neighbor.second;
double distToNextNode = distTo[curNodeID] * neighbor.second;
if (distTo[nextNodeID] > distToNextNode)
{
distTo[nextNodeID] = distToNextNode;
minHeap.push(make_pair(nextNodeID, distToNextNode));
}
}
}
return (int)distTo[end] == INT_MAX ? 0 : -distTo[end];
}
};
5 Bellman-Ford算法
5.1 介绍
Bellman-Ford算法更具有一般性,适用于图中含有负值权重的情况。同样的,如果是单元最短路径有解,图中也不能存在负权值的环。
具体的算法可以参考《算法导论3rd-P379》,中文版的图24-4有少许错误,请注意。
5.2 实现
class BellmanFord {
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;
}
}
}
bool checkMinusCycle(int u, int v, int w)
{
do
{
if (hasMinusCycle)
{
break;
}
if (distanceTo[u] == INT_MAX)
{
break;
}
int distToV = distanceTo[u] + w;
if (distanceTo[v] > distToV)
{
hasMinusCycle = true;
break;
}
} while (0);
return hasMinusCycle;
}
public:
//为了避免图中有孤立的节点,建议传入图中节点的数量
BellmanFord(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;
//遍历每一个节点,使其作为起始点进行一轮relax,共进行n轮relax
for (int i = 0; i < n; ++i)
{
//对图中的所有边进行relax
for (int j = 0; j < graph.size(); ++j)
{
for (auto& item : graph[j])
{
relax(j, item.first, item.second);
}
}
}
//检测图中是否存在负权重的环,需要遍历所有的边
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;
}
};
5.3 应用-网络延迟时间
/*
让你求所有节点都收到信号的时间,你把所谓的传递时间看做距离,实际上就是问你
「从节点k到其他所有节点的最短路径中,最长的那条最短路径距离是多少」,
说白了就是让你算从节点k出发到其他所有节点的最短路径,就是标准的 Dijkstra 算法。
*/
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<pair<int, int>>> graph;
buildGraph(times, n, graph);
//由于创建图的邻接表使用的是从0开始,所以这里对k进行减一操作
BellmanFord bellman_ford(n, k-1, graph);
//找到最长的那一条最短路径
int res = 0, dist = 0;
for (int i = 0; i < n; ++i)
{
if (i == k-1)
{
continue;
}
dist = bellman_ford.getDistanceTo(i);
if (dist == INT_MAX)
{
// 有节点不可达,返回 -1
return -1;
}
res = std::max(res, dist);
}
return res;
}
void buildGraph(vector<vector<int>>& times, int n, vector<vector<pair<int, int>>>& graph)
{
graph.resize(n);
for (auto& v : times)
{
//n 个网络节点,标记为 1 到 n,所以做了减一操作
graph[v[0] - 1].push_back({ v[1] - 1, v[2] });
}
}
};
6 Bellman-Ford SPFA
6.1 原理
参考链接: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次。
6.2 实现
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)
{
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;
}
};
7