该博客已更新到:单源最短路问题
学习时参考的博客:https://blog.csdn.net/qibofang/article/details/51594673
一、DFS或BFS搜索(单源最短路径)
思想:遍历所有从起点到终点的路径,选取一条权值最短的路径。
下面代码是参考博客中的代码,加上本人一些注释
void DFS(int u,int dist) //u 为当前节点; dist 为当前点到起点的距离
{
//min 表示目前起点到终点的最短距离,初始化为 无穷(用一个很大的数去表示)
if(dist>min) //当前距离已经大过目前起点终点的最短距离,没必要往下
return ;
if(u==dest) //到达目的地
if(min>dist)
{
min=dist;
return ;
}
for(int i=1;i<=n;++i) //遍历每一条边,标记点
{
//对于同一个点,edge[i][i] = 0,下面条件中让 edge[u][i]大于0,是为了防止陷入无限循环
//即老是搜索同一个点。
if(edge[u][i]!=INF&&edge[u][i]&&!vis[i])
{
vis[i]=1;
DFS(i,dist+edge[u][i]); //更新当前距离
vis[i]=0;
}
}
return ;
}
二、弗洛伊德算法(多源最短路径)
思想:通过比较得出图中的任意两个点,依次通过0个,1个,2个…中介点所形成的路径中最短的那一条。
一开始如果两点(U和V)之间的路径(路径①)上没有其他点,那么这条路径必定是这两点过上的最短路径。现在如果两点之间有另外一条路径(路径②),该路径上存在第三个点(T),若这条路径比原来的路径①短,则将该路径②更新为U和V之前的最短路径,而这条路径②,是由U-T,T-U两条直接相连的路径构成。依次往下,当存在一条路径连接U、T、V且上面有第四个点时,在这条路径上U到V的距离是否更小?…
核心代码:
用了三层循环,时间复杂度较高
(这篇博文对弗洛伊德算法解释很详细:https://blog.csdn.net/qq_34374664/article/details/52261672)
void Floyd(int all) //all 共有多少节点
{
//三层循环表示的是:从 j 到 k 经过点 i 后 j和k之间的距离有没有缩小
for(int i=1;i<=all;++i)
for(int j=1;j<=all;++j)
for(int k=1;k<=all;++k)
//最好加入 egde[j][i] 和 egde[i][k] 都小于inf的条件,防止数据爆掉
if(egde[j][k]>egde[j][i]+egde[i][k]) //判断借助中转点后,路程是否缩小
egde[j][k]=egde[j][i]+egde[i][k]; //每个 egde[i][j]都表示当前时刻下 i 到 j 的最短距离
}
使用Floyd算法后,二维数组edge表示的边都是两点之间的最短路,所以可以输出任意两点之间最短的路径。
三、迪杰斯特拉算法(单源最短路径)
思想: 通过 n-1 层循环,每一次,寻找距离起点最近的点,做标记(不再访问),然后用该点作为其他点到起点路径上的中介,更新其他点到起点之间的距离。即,假设起点为U,中介点为T,图上任意没有被标记过的点为V,判断U-V的距离是否大于U-T-V距离,是的话更新U-V的距离为U-T-V的距离。(用一个一维数组存放在图上其他点到起点的距离),这种对两点直接距离的更新称为松弛操作(记住一些专业名词对以后的阅读有好处)。然后再次从未标记的点中寻找和起点最近的点,做同样的操作,知道遍历所有的点。最终得到的 dist数组中存放的是图上任意点到起点的最短距离。
为什么可以实现找到最短路?
因为这个算法先是找到和起点直接相连的距离最短的点,在更新其他点到起点的距离的时候,都是利用已计算过和起点的最短距离的点作为中介点去比较,所以每次得到的必然都是最短距离。
void Dijkstra(int u)
{
int min;//表示某一点到起点的最短距离
int k;// k 记录为访问的点中距离起点最近的点
for(int i=1; i<=n; ++i)
{
flag[i]=0;// flag 用于标记节点是否已经访问过;
//dist 数组,存储图上各点到起点的距离
dist[i]= edge[u][i]; // edge 存放的是两个节点之间的距离,若两个点之间没有边,则赋值为 INF(极大的数)
pre[i] = 0; //pre 数组用来存储 最短路径中节点 i 的上一个节点是哪一个
}
flag[u]=1;//标记起点,没必要访问
dist[u]=0;
//开始Dijkstra算法,遍历 n-1次,即遍历除起点外其他所有的点
for(int i=1; i<n; ++i)
{
//寻找离起点最近的点
min = INF;
for(int j=1; j<=n; ++j)
if(!flag[j]&&min>dist[j])
{
min=dist[j];
k = j;
}
//相应的点后,更新图上其他点到起点的路径距离
flag[k]=1;
for(int j=1; j<=n; ++j)
if(edge[k][j]!=INF)// k 和 j 之间存在边时
{
int temp = edge[k][j]+dis[k]; //i 到 j 的新路径
if(!flag[j]&&temp<dis[j]) //如果该路径距离小于原本 j到i 的距离,刷新替换
{
dis[j]=temp;
pre[j]=k;
}
}
}
}
此处补充四个小概念:(N表示节点数,M表示边数)
①稀疏图:M远比N²小的图称为稀疏图
②稠密图:M相对较大的图称为稠密图
③大顶堆:堆顶元素最大的堆
④小顶堆:堆顶元素最小的堆
紫书上对该算法进行了优化并且封装,其中涉及优先队列的知识,基本思想一致,用优先队列只是进行了优化,使得即使是稠密图,使用该优化算法的速度也比用邻接矩阵的算法快。因为执行 push存在前提。
(用优先队列,是因为算法中d[i]越小,越应该先出队(每次先取和起点距离最近的点))
下面给出紫书把此算法封装成结构体的代码
链接所指大佬提供了迪杰斯特拉算法堆优化的另一种思路:不需要定义结构体重载 <,而是直接对距离取相反数
//把整个算法以及所用到的数据结构封装到一个结构体中
struct Dijkstra
{
struct Edge
{
int from,to,dist;
Edge(int u,int v,int d):from(u),to(v),dist(d) {}
}; //存放边的结构体
int n,m;
vector<Edge> edges;
vector<int> G[MAXN];
bool done[MAXN];// 标记是否访问过
int d[MAXN];//起点到各个点的距离
int p[MAXN];// 记录最短路径
void inti(int n)
{
this->n=n; //赋值节点数
//清空操作,是为了可以重复使用此结构体
for(int i=1; i<=n; ++i)
G[i].clear();
edges.clear();
}
void AddEdge(int f,int t,int d)
{
edges.push_back(Edge(f,t,d));
m = edges.size(); //每次添加边后都把边数赋值给 m 最后得到总边数
G[f].push_back(m-1); //m-1是保存了 边在edges中的编号(下标),可以通过此编号访问边
}
//定义一个结构体作为优先队列中的元素类型
struct HeapNode{
int d,u; //d 是距离, u 是节点
//表示优先级为从小到大
bool operator<(const HeapNode& a)const
{
return d>a.d; //小堆顶
//个人理解,队列的每次push操作都要根据优先级判断再放置队顶元素,
//即是否 队顶<新入队元素的操作(STL默认操作是大根堆)
//重载<运算符后使得比较结果颠倒,优先级变成从小到大
}
};
//主算法
void dijkstra(int u)
{
priority_queue<HeapNode> q; //创建一个优先队列
for(int i=1;i<=n;++i)
d[i] = INF;
d[u] = 0;//起点到起点的距离当然为0
memset(done,0,sizeof(done));
HeapNode v(0,u); //起点
q.push(v);
while(!q.empty())
{
v=q.top();
q.pop();
if(done[v.u]) //已经访问过的点不再访问
continue;
done[v.u] = true;
//可以省去done数组,改为 if(v.d!=d[v.u]) 防止节点重复扩展
//思路同,只是实现方式改变
for(int i=0;i<G[v.u].size();++i) //这种情况下每条边只被访问了一次
{
Edge& e = edges[G[v.u][i]];
if(d[e.to]>d[v.u]+e.dist)
{
d[e.to]=d[v.u]+e.dist;
p[e.to]=v.u;
q.push((HeapNode){d[e.to],e.to});
}
}
}
}
};
注意:迪杰斯特拉算法不可以求含有负权边的最短路!
具体原因等我搞明白了再写一篇博客。
四、Bellman-Ford(贝尔曼 - 福特)算法(单源最短路径,可以求解含负权边的图)
思想:(节点数N,边数M)进行至多N-1次循环,每一次都更新图上任意点到起点的距离(松弛操作)。其实个人觉得类似Dijkstra算法,对任意点到起点的距离进行更新,只是每一次都遍历所有边去更新。至多N-1次,是考虑了最坏的情况,即每一次遍历所有边,只有和起点相连的边得到更新 。
核心代码:
// u[i]和v[i] 分别存放第 i 条边的前后顶点,w[i]存放权值
bool BellmanFord()
{
for(int i=1;i<=n;++i)
d[i]=(i==1?0:INF);
//至多遍历 n-1 次
//考虑最坏的情况,即每一次遍历所有边,只有和起点相连的边得到更新
for(int i=1;i<n;++i)
for(int j=1;j<=m;++j)
{
int x=u[i],y=v[i];
if(d[x]<INF)
d[y]=min(d[y],d[x]+w[i]);
}
//判断是否存在 含有负权的环
for(int i=1;i<=m;++i)
if(d[v[i]]>d[u[i]]+w[i])
return false;
return true;
}
参考此博客(https://www.cnblogs.com/xiu68/p/7993514.html)
此算法的缺点是效率不高。因为要进行N-1次循环,但可能在第一次就已经得到答案了,亦即是做了很多无用的工作。
下面是对该算法的改进,一般称为SPFA算法。
思想:在Bellman-Ford算法的基础上,使用队列保存待松弛的点,对每个出队的点,遍历跟它相邻的点,如果这两点间的边能够松弛且相邻点不在队列中,则把相邻点存入队列。(因为松弛操作可能对该相邻点的相邻点产生影响)
核心代码:
void SPFA()
{
vector<int> d(n+1,INF); //距离
vector<int> cnt(n+1,0); //记录每个点遍历的次数
vector<int> inq(n+1,0); //记录节点是否在队列内
d[s]=0;
cnt[s]=inq[s]=1;
deque<int> q(1,s);
while(!q.empty())
{
int u=q.front(),i=0,to,dist;
inq[u] = 0; //出队
q.pop_front();
for(; i<g[u].size(); ++i)
{
to=g[u][i].to;//g[u][i].to表示u-i边箭头端(末端)的点
dist=g[u][i].dist;
if(d[to]>d[u]+dist)
{
d[to]=d[u]+dist;
if(!inq[to]) //如果点不在队列内再入队
{
//若有负权环,则会无限循环下去,所以肯定有一个点遍历的次数大于 n
if(n==++cnt[to])return 0; //判断是否为负权环
inq[to]=1;
//SLF优化:减少重复扩展的次数。
if(!q.empty()&&d[to]<q.front()) //确保队列非空才能访问q.front()
q.push_front(to);
else
q.push_back(to);
}
}
}
}
其中运用了SLF优化(关于此优化,好像还没有确切的证明,但实际运用上确实优化了不少,如果有读者知晓麻烦评论告知一下,多谢)
SLF优化:判断将要入队的节点到源点的距离是否比队首元素小,是的话插入队首,否则插入队尾。所以上面代码用了双向队列。这样做可能会减少原队首的某些邻接点或者一些本来没有SLF优化时需要入队的点的入队次数,即减少了扩展次数。但是存在反例使得使用了SLF优化反而使程序变慢。所以一般可以用,但不是绝对都能用。(我对此的理解还很浅薄)
学习、学好图论的路,还很长…