#ifndef SHORTEST_PATH_H
#define SHORTEST_PATH_H
#include<queue>
#include<limits.h>
#include<memory>
#include"../data_struct/data_struct.h"
/// (1)当权值为非负时,用Dijkstra。
/// (2)当权值有负值,且没有负圈,则用SPFA,SPFA能检测负圈,但是不能输出负圈。
/// (3)当权值有负值,而且可能存在负圈,则用BellmanFord,能够检测并输出负圈。
class graph_shortest_path
{
public:
/// 迪杰斯特拉算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
/// 注意该算法要求图中不存在负权边
/// 在图论中一个典型的贪心法求最优解的例子就莫过于“最短路径”的问题。
/// 如果大家已经了解Prim算法,那么Dijkstra算法只是在它的上面延伸了下
/// 这与贪心选择性质有关(ps:貌似还是动态规划啊,晕了),每次都找一个距源点最近的点(dmin),
/// 然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),
/// 再通过这个负权边L(L小于0),使得路径之和更小(dmin'+L小于dmin),则dmin'+L成为最短路径,并不是dmin
/// 用斐波那契堆的复杂度O(E+VlgV)
static void dijkstra(const std::shared_ptr<graph_matrix>& graph, uint32_t s,
std::vector<int>& dis, std::vector<uint32_t>& pre)
{
dis.resize(graph->vertex.size());
pre.resize(graph->vertex.size());//记录前驱顶点
std::vector<bool> is_visited(graph->vertex.size(), false);
for (auto i: graph->vertex)
{
dis[i] = graph->matrix[s][i];
pre[i] = s;
}
is_visited[s] = true;
for (auto i: graph->vertex)
{
int mindist = INT_MAX;
auto u = s; // 找出当前未使用的点j的dist[j]最小值
for (auto j: graph->vertex)
{
if (is_visited[j] == false && dis[j] < mindist)
{
u = j;// u保存当前邻接点中距离最小的点的号码
mindist = dis[j];
}
}
is_visited[u] = true;
for (auto j: graph->vertex)
{
if (is_visited[j] != true && graph->matrix[u][j] < dis[j] - dis[u])
{
dis[j] = dis[u] + graph->matrix[u][j]; //更新dist
pre[j] = u; //记录前驱顶点
}
}
}
}
/// Floyd-Warshall算法(Floyd-Warshall algorithm)是解决任意两点间的最短路径的一种算法,
/// 可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。
/// Floyd算法是一个经典的动态规划算法
/// 弗洛伊德算法(解决多源最短路径):时间复杂度O(n^3),空间复杂度O(n^2)
static void floyd(const std::shared_ptr<graph_matrix>& graph,
std::vector<std::vector<int>>& dis, std::vector<std::vector<int>>& pre)
{
dis.reserve(graph->vertex.size());
pre.reserve(graph->vertex.size());
for (auto i: graph->vertex)
{
dis[i].reserve(graph->vertex.size());
pre[i].reserve(graph->vertex.size());
}
for (auto i: graph->vertex)
{
for (auto j: graph->vertex)
{
dis[i][j] = graph->matrix[i][j];
pre[i][j] = -1;
}
}
for (auto k: graph->vertex)
{
for (auto i: graph->vertex)
{
for (auto j: graph->vertex)
{
if (dis[i][j] - dis[i][k] > dis[k][j])
{
dis[i][j] = dis[i][k] + dis[k][ j];
pre[i][j] = k;
}
}
}
}
}
/// Bellman(贝尔曼,动态规划提出者)和Ford(福特)提出了从源点逐次绕过其他顶点,以缩短到达终点的最短路径长度的方法。
/// Bellman-ford算法是求含负权图的单源最短路径算法,效率很低。
/// 即进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,
/// Bellman-ford算法有一个小优化:每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。
/// Dijkstra算法和Bellman算法思想有很大的区别:Dijkstra算法在求解过程中,源点到集合S内各顶点的最短路径一旦求出,则之后不变了,
/// 修改的仅仅是源点到T集合中各顶点的最短路径长度。Bellman算法在求解过程中,每次循环都要修改所有顶点的dist[ ],
/// 也就是说源点到各顶点最短路径长度一直要到Bellman算法结束才确定下来。
/// 首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。
/// 其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。
/// Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。
/// 在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,
/// 找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,
/// 生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1条边,
/// 所以,只需要循环|v|-1 次。每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,
/// 此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。
/// (但是,每次还要判断松弛,这里浪费了大量的时间,这就是Bellman-Ford算法效率底下的原因,也正是SPFA优化的所在)。
/// 复杂度O(VE),空间复杂度O(E)
static bool BellmanFord(const std::shared_ptr<graph_matrix>& graph, uint32_t s,
std::vector<int>& dis, std::vector<uint32_t>& pre)
{
dis.reserve(graph->vertex.size());
pre.reserve(graph->vertex.size());
std::vector<bool> is_visited(graph->vertex.size(), false);
for (auto i: graph->vertex)
{
dis[i] = graph->matrix[s][i];
pre[i] = s;
}
auto edges = ToEdges(graph);
for (uint32_t i = 1; i < graph->vertex.size(); ++i)//松弛(顺序一定不能反~)
{
for(auto& edge : edges)
{
if (dis[edge->end] - dis[edge->start] > edge->weight)
{
dis[edge->end] = dis[edge->start] + edge->weight;
pre[edge->end] = edge->start;
}
}
}
for (auto& edge : edges)//判断是否含有负权回路
{
if (dis[edge->end] - dis[edge->start] > edge->weight)
{
return false;
}
}
return true;
}
/// Shortest Path Faster Algorithm
/// 给定一个加权连通图,选取一个顶点,称为起点,求取起点到其它所有顶点之间的最短距离,其显著特点是可以求含负权图的单源最短路径,且效率较高
/// spfa是求单源最短路径的一种算法,它还有一个重要的功能是判负环
/// 如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
/// 在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法
/// 期望的时间复杂度O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2,E为给定图的边集合
/// SPFA的最坏情况应该是O(VE) 把SPFA看成是个BFS的过程
static bool SPFA(const std::shared_ptr<graph_matrix>& graph, uint32_t s,
std::vector<int>& dis, std::vector<uint32_t>& pre)
{
dis.reserve(graph->vertex.size());
pre.reserve(graph->vertex.size());
std::vector<bool> is_visited(graph->vertex.size(), false);
std::vector<uint32_t> num(graph->vertex.size(), 0);
for (auto i: graph->vertex)
{
dis[i] = graph->matrix[s][i];
is_visited[i] = false;
pre[i] = s;
num[i] = 0;
}
std::queue<uint32_t> queue;
is_visited[s] = true; //表示第s个顶点进入数组队
num[s] = 1; //表示第s个顶点已被遍历一次
queue.push(s); //第s个顶点入队
while (!queue.empty())
{
auto u = queue.front(); //获取数组队中第一个元素
queue.pop();
for (auto i: graph->vertex)
{
if (dis[i] - dis[u] > graph->matrix[u][i])
{
dis[i] = dis[u] + graph->matrix[u][i];
pre[i] = u;
if (is_visited[i] == false)
{
queue.push(i);
is_visited[i] = true;//表示已进入数组队
if (++num[i] > graph->vertex.size()) //存在负环
{
return false;
}
}
}
}
is_visited[u] = false;
}
return true;
}
static std::vector<std::shared_ptr<edge>> ToEdges(const std::shared_ptr<graph_matrix>& graph)
{
std::vector<std::shared_ptr<edge>> edges;
for (auto i: graph->vertex)
{
for (auto j: graph->vertex)
{
if (i != j && graph->matrix[i][j] < INT_MAX)
{
edges.push_back(std::make_shared<edge>(i, j, graph->matrix[i][j]));
}
}
}
return edges;
}
};
#endif // SHORTEST_PATH_H