最短路:从一个点到另一个点的最短距离(边权和最小)
经典的最短路问题大概这几种算法:
一、前缀知识
- 图的基本概念、有向图无向图、DAG、图的表示(邻接矩阵、邻接表、链式前向星)
- 单源最短路问题:从一个点出发到其他能到达的任意点的最短路径
二、Bellman-Ford算法
- 适用范围:没有负圈存在的图寻找单源最短路,可以用这个算法来检验是否有负圈
- 算法复杂度:O(VE) V为点数,E为边数
- 原理:
这个算法给我一种瞎搞的感觉。。。
大致分为几个步骤:
1.开一个数组dist[V]用于存各个点到出发点的最短距离,赋初值INF(出发点为0)
2.每一次都循坏一遍所有的边,若dist[终点]>dist[起点]+边权,则更新dist[终点],这样可以保证每一次的更新都能使dist[终点]变小,上一次循环的结果又可以保证这一次这个点已经到达过了
3.进行多次循环,直到不再更新,说明所有的dist[]都已经被更新到最短 - 单看原理比较难以理解,以下面的图举个例子,大致模拟一下算法的流程
求编号为1的点到所有点的最短路径
第一步:初始化dist[v]数组,所有的dist初始化为INF(dist[1]为0)
(一般INF取0x3f3f3f3f)
第二步:不断循环更新dist数组
然后我们就发现无法再更新了,这个时候跳出循环就好啦~
然后就完美地得到了初始点到每个点的最短路径长度了,如果是INF则说明无法到达 - 伪代码(这里的edge仅仅用一个结构体把每条边存下来而已)
struct edge{int from,to,cost};
edge edg[maxe];
void bellman()
{
/*V为点数,E为边数,start为出发点*/
fill(dist,dist+maxv,INF);
dist[start]=0;
while(1)
{
bool if_update=false;//记录此次循环是否更新
for(int i=0;i<E;i++)
{
edge e=edg[i];
if(dist[e.from]!=INF&&dist[e.from]+e.cost<dist[e.to])//若该边的起始点已经被路过且满足dist[起始]+cost<dist[终点],则更新
{
if_update=true;
dist[e.to]=dist[e.from]+e.cost;
}
}
if(!up_date) break;
}
}
- bellman算法虽然不能用来判断含有负圈的图(?:试想一下如果有负圈,那岂不是转了一圈又一圈停不下来了),但是可以通过这个性质来判断图中有没有负圈:
我们假设一种最坏情况,每一次的循环只更新一个值的时候(如图)
每次循环的时候只更新一个dist,这样整个过程的循环只进行V-1次,所以,只要不存在负圈,整个过程最多进行V-1次循环,我们可以在while里面加个计数器,这样就可以用来判断是否含有负圈了 - 例题传送门:虫洞问题
三、Dijkstra算法
- Bellman算法感觉有点浪费时间,就像上面最坏情况那样,总共五个点,需要进行四次循环,每次循环里面都需要遍历一遍所有的边。。。但每次循环实际上却只更新了一次,所以,在Bellman算法的基础上进行了一下优化,就有了Dijkstra算法。
然鹅在Dijkstra被优化之前他们时间复杂度是一样的,只不过Dijkstra算法看起来没有那么暴力了。。。。 - 适用情况:没有负边存在
- 时间复杂度O(V2)
- 原理:在Bellman算法的基础上,每一次更新的时候都会找一下当前情况下没有被用过并且dist最小的点,对于当前情况下用这个dist来更新其他的dist(这真的有一种dp的感觉,而且满足了无后效性)
还是上面那个例子,用Dijkstra算法循环的过程如图:
然后后面就是用dist[4]和dist[5]更新了,但是很明显他们没有边可以走下去了,同一个例子可以发现,Dijkstra算法每个点只用了一次,看起来比Bellman算法要少跑一点然鹅在堆优化之前时间复杂度并没有什么区别。。。 - 伪代码(用邻接表表示图)
struct edge{int to,cost};
vector<edge> e[maxv];
bool vis[maxv];//vis数组用于记录每个点是否被用来做过一次循环
void Dijkstra()
{
/*V为点数,E为边数,start为出发点*/
fill(dist,dist+V,INF);
fill(vis,vis+V,false);
dist[start]=0;
while(1)
{
int v=-1;
for(int u=0;u<V;u++)
if(!vis[u]&&(v==-1||dist[u]<dist[v])) v=u;//找到没有被用过并且最小的dist并记录下来
if(v==-1) break; //所有点都被遍历一遍以后就跳出循环
vis[v]=true;
for(int i=0;i<e[v].size();i++)
dist[e[v][i].to]=min(dist[e[v]+e[v][i].cost, dist[e[v][i].to]);//每次都更新一下dist[v]所能到达的点
}
}
- 但是这个算法不能用在存在负边的情况下,试想一下,如果存在一条负边,使得经过了这条边dist就会减小,那dp的无后效性原则就不满足了,Dijkstra算法本来就是在没有负边的情况下可以保证下一次的dist会越来越大而不影响前面已经得到的结果,无负边的条件被破坏掉了,相当于Dijkstra的前提也被破坏掉了
- 例题传送门:最短路模板题(无需优化)
四、Dijkstra算法的堆优化
- Dijkstra算法之所以最后的时间复杂度跟Bellman差不多,主要是找到最小边的这个过程的遍历使得时间复杂度变大,所以我们只需要对找到最小值这个过程进行优化,整个算法的时间复杂度就会被优化下来,这里我们用一下c++里的set容器来实现logn的查找,当然用priority_queue也可
- 时间复杂度O(ElogV)
- 先强推一下链式前向星,具体就不展开讲了,代码大致是这样子的,想一想应该就能想通,我把代码挂上来
struct edge{
int to,w,next;
}e[maxe];
int cnt=0;
int h[maxv];
void add(int i,int j,int w)//加边操作
{
e[++cnt].to=j;
e[cnt].w=w;
e[cnt].next=h[i];
h[i]=cnt;
}
堆优化代码
void dijkstra()
{
set<P>heap;
fill(dist,dist+1+n,INF);
dist[start]=0;
heap.insert(P(0,u));
while(!heap.empty())
{
int d=heap.begin()->first;
int v=heap.begin()->second;
heap.erase(heap.begin());
if(d>dist[v]) continue;
for(int i=h[v];i;i=e[i].next)
{
edge edg=e[i];
if(dist[edg.to]>dist[v]+edg.w)
{
dist[edg.to]=dist[v]+edg.w;
heap.insert(P(dist[edg.to],edg.to));
}
}
}
}
- 例题传送门:最短路模板题
五、Floyd算法
- 时间复杂度:O(V3)
- 该算法可以在O(V3)的时间复杂度下时间找到所有两点间的最短距离,而且实现起来较为简单,并且可以用于存在负边的情况
- 因为代码比较简单而且用dp的思路就很容易理解了,所以我就不详细展开讲了,直接上代码(邻接矩阵存图)
int dist[maxv][maxv];//数组dist用于存边上的权值,不存在为INF,dist[i][i]=0
void floyd()
{
for(int k=0;k<V;k++)//V为点数
for(int i=0;i<V;i++)
for(int j=0;j<V;j++)
dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
}
例题传送门:畅通工程