您的位置:首页 > 其它

最短路算法 :Bellman-ford算法 & Dijkstra算法 & floyd算法 & SPFA算法详解&BFS

2016-03-12 21:38 495 查看
1、Bellman-Ford算法

2、Dijkstra算法(代码 以邻接矩阵为例) && Dijkstra + 优先队列的优化(也就是堆优化)

3、floyd-Warshall算法(代码 以邻接矩阵为例)

4、SPFA(代码 以前向星为例)

5、BFS 求解最短路+路径还原

松弛操作:

if(dis[i]>dis[k]+G[k][i])//其中dis[i]是其他的路径,dis[k]是现在的最小路径,G[k][i]是现在的最小路径的点到其他路径点的权值。
{
dis[i] = dis[k]+G[k][i];
}


源点用s表示,用数组 dis
来存储最短路径,dis
数组为源点到其他点的最小距离。那么最最开始的最短路径的估计值也就是对 dis
的初始化喽。一般我们的初始化都是初始化为 dis
= +∞ , But 在一些时候是初始化为dis
= 0的(“一些时候”后面再讲),dis[s] = 0;

fill(dis,dis+n,MAX);//不知此函数的可以百度
dis[s] = 0;


Bellman-Ford算法:解决的是一般情况下的单源最短路径问题,其边可以为负值。bellman-ford算法可以判断图是否存在负环,若存在负环会返回一个布尔值。当然在没有负环存在的 情况下会返回所求的最短路径的值。算法如下:

1)图的初始化等操作

2)for i = 1 to |G.V| - 1 //|G.V| 为图 G的点的总数(图中任一点最长的路径(边数)最大为 |G.V| - 1 )

3)for each edge(u,v)∈G.E //G.E 为图 G 的边

4) relax(u,v,w) //也就是if v.d>u.d+w(u,v) , v.d = u.d+w(u,v);

5)for each edge(u,v)∈G.E

6) if v.d>u.d+w(u,v) //v.d为出发源点到结点v的最短路径的估计值 u.d亦如此 w(u,v) 为u结点到v结点的权重值(通俗点就是u—>v的花费)。

7 return false;

8 return true

此算法分为3步:

1)第1行对图进行初始化,初始化dis
= +∞,dis[s] = 0;

2) 第2~4行为求最短路的过程,是对图的每一条边进行|V|-1次松弛操作,求取最短路径。

3)第5~8行为对每条边进行|V|-1次松弛后,检查是否存在负环并返回相应的布尔值,因为进行|V|-1次松弛后若没有负环则v.d的值确定不变,若有负环则会继续进行松弛操作,因为一个数+负数是一定比它本身要小的。

此算法的 时间复杂度为O(VE)。

用到的数据结构:

struct Edge
2 {
3     int u, v, w;//u 为起点,v为终点,w为u—>v的权值
4 }edge[maxn;


当已经明确没有负环的时候:

void bellman_ford()
{
bool flag;//用于优化的
int dis[maxn];//保存最短路径
//初始化
fill(dis,dis+n,INF);//其他点为+∞
dis[s] = 0;//源点初始化为0
//m = m<<1;//若为无向图,边的数量应该变为2倍
for(int i=1;i<n;i++)//进行|V|-1次
{
flag = false;//刚刚开始标记为假
for(int j=0;j<m;j++)//对每个边
{
//if  (v.d>u.d+w(u,v))
if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w)//进行松弛操作
{
dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛成功
flag = true;//若松弛成功则标记为真
}
}
if(!flag)//若所有的边i的循环中没有松弛成功的
break;//退出循环
//此优化可以大大提高效率。
}
printf("%d\n",dis[t]==INF?-1:dis[t]);//输出结果
}


对于优化的解释:若图中存在负环的情况下外循环需要|V|-1次循环,若不存在负环,平均情况下的循环次数是要小于|V|-1次,当所有边没有松弛操作的时候我们就得到了最后的答案,没有必要继续循环下去,So有了这个简单的优化。

当需要判断有没有负环的时候:

方法一:

bool bellman_ford(int s)
{
bool flag;
int dis[maxn];//保存最短路径
fill(dis,dis+n,INF);//初始化
dis[s] = 0;
for(int i=1;i<n;i++)//共需进行|V|-1次
{
flag = false;//优化   初始化为假
for(int j=0;j<m;j++)//对每一条边
{
// if  u.d>v.d+w(u,v) , u.d = v.d+w(u,v);
if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w)//进行松弛
{
dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功
flag = true;//松弛成功变为真
}
}
if(!flag)//若每条边没有松弛
break;//跳出循环
}
// 一下部分为 3) 第5~8行的操作
for(int i=0;i<m;i++)
if(dis[edge[i].u]>dis[edge[i].v]+edge[i].w)//进行|V|-1次操作后  有边还能进行松弛  说明
return true;//存在负环
return false;//不存在负环
}


方法二:

bool bellman_ford()
{
bool flag;
int dis[maxn];//保存最短路径
fill(dis,dis+n,INF);//初始化
dis[s] = 0;
int i;
for(i=0;i<n;i++)//共需进行|V|-1次
{
flag = false;//优化   初始化为假
for(int j=0;j<m;j++)//对每一条边
{
// if  u.d>v.d+w(u,v) , u.d = v.d+w(u,v);
if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w)//进行松弛
{
dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功
flag = true;//松弛成功变为真
}
}
if(!flag)//若每条边没有松弛
break;//跳出循环
//因为对于V个点 你最多需要进行|V|-1次外循环,如果有负环它会一直进行下去,但是只要进行到第V次的时候就说明存在负环了
if(i == n-1)//若有
return true;//返回有负环
}
return false;//不存在负环
}


Dijkstra算法:解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为正值。Dijkstra算法在运行过程中维持的关键信息是一组结点集合S。 从源结点s 到该集合中每个结点之间的最短路径都已经被找到。算法重复从结点集V-S中选择最短路径估计最小的结点u,讲u加入到 集合S,然后对所有从u发出的边进行松弛。Dijkstra 算法如下:

1 对图的建立和处理,dis
数组的初始化等等操作

2 S = ∅

3 Q = G.V

4 while Q ≠ ∅

5 u = EXTRACT-MIN(Q)

6 S = S ∪ {u}

7 for each vertex v∈ G.Adj[u]

8 relax(u,v,w)

此算法在此分为二步 : 第二大步中又分为3小步

1) 第1~3行 对dis
数组等的初始化,集合S 为∅,Q集合为G.V操作

2) 第4~8行 中,

① 第4行 进行G.V次操作

② 第5~6行 从Q中找到一个点,这个点是Q中所有的点u—>S中某点最小的最短路径的点,并将此点加入S集合

③ 第7~8行 进行松弛操作,用此点来更新与u相连的点的路径的距离。

对于邻接矩阵存储的图 来说此算法的时间复杂度为 O(|V|²),用其他的数据结构可以优化为O(|E|log|V|)的时间复杂度。

方法一:应用的数据结构:二维数组

int G[maxn][maxn];


void dijkstra()
{
bool vis[maxn];//相当于集合Q的功能, 标记该点是否访问过
int dis[maxn];//保存最短路径
for(int i=0;i<n;i++)//初始化
dis[i] = G[s][i];//s->其余各个点的距离
memset(vis,false,sizeof(vis));//初始化为假表示未访问过
dis[s] = 0;//s->s 距离为0
vis[s] = true;//s点访问过了,标记为真
for(int i=1;i<n;i++)//G.V-1次操作+上面对s的访问 = G.V次操作
{
int k = -1;
for(int j=0;j<n;j++)//从尚未访问过的点中选一个距离最小的点
if(!vis[j] && (k==-1||dis[k]>dis[j]))//未访问过 && 是距离最小的
k = j;
if(k == -1)//若图是不连通的则提前结束
break;//跳出循环
vis[k] = true;//将k点标记为访问过了
for(int j=0;j<n;j++)//松弛操作
if(!vis[j] && dis[j]>dis[k]+G[k][j])//该点为访问过 && 可以进行松弛
dis[j] = dis[k]+G[k][j];//j点的距离  大于当前点的距离+w(k,j) 则松弛成功,进行更新
}
printf("%d\n",dis[t]==INF?-1:dis[t]);//输出结果
}


方法二:用到的数据结构 :前向星 对组 优先队列

typedef pair<int, int >P; //pair 的first 保存的为最短距离, second保存的为顶点编号
struct Edge//前向星存边
{
int to, w;//to为到达的点, w为权重
int next;//记录下一个结构体的位置 ,就向链表的next功能是一样的
}edge[maxn];//存所有的边
int head[maxn];//和链表的头指针数组是一样的。只不过此处head[u]记录的为最后加入edge的且与u相连的边在edge的位置,即下标
priority_queue<P,vector<P>,greater<P> >que;//优先队列从小到大


前向星的加边函数:

void add(int u, int v, int w)
{
edge[cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u];//获得下一个结构体的位置
head[u] = cnt++;//记录头指针的下标
}


算法实现:

void dijkstra()
{
int dis[maxn];//最短路径数组
int v;//v保存从队列中取出的数的第二个数 ,也就是顶点的编号
priority_queue<P,vector<P>,greater<P> >que;//优先队列 从小到大
Edge e;//保存边的信息,为了书写方便
P p;//保存从队列取出的数值
fill(dis,dis+n,MAX);//初始化,都为无穷大
dis[s] = 0;//s—>s  距离为0
que.push(P(0,s));//放入距离 为0   点为s
while(!que.empty())
{
p = que.top();//取出队列中最短距离最小的对组
que.pop();//删除
v = p.second;//获得最短距离最小的顶点编号
if(dis[v] < p.first)//若取出的不是最短距离
continue;//则进行下一次循环
for(int i=head[v];i!=-1;i=edge[i].next)//对与此点相连的所有的点进行遍历
{
e = edge[i];//为了书写的方便。
if(dis[e.to]>dis[v]+e.w)//进行松弛
{
dis[e.to]=dis[v]+e.w;//松弛成功
que.push(P(dis[e.to],e.to));//讲找到的松弛成功的距离 和顶点放入队列
}
}
}
printf("%d\n",dis[t]==INF?-1:dis[t]);//输出结果
}


floyd-Warshall算法(动态规划):是一个很强大的算法,它可以计算任意两点之间的最短路径,其边可以为负值。

证明:

  对于0~k,我们分i到j的最短路正好经过顶点k一次和完全不经过顶点k两种情况来讨论。不仅过顶点k的情况下,d[k][i][j] = d[k-1][i][j]。通过顶点k的情况,d[k][i][j]= d[k-1][i][k]+d[k-1][k][j]。合起来就得到了d[k][i][j] = min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])。这个DP也可以用同一个数组不断进行如下的操作:d[i][j] = min(d[i][j],d[i][k]+d[k][j])的更新来实现。floyd算法的时间复杂度为O(|V|³)。 

void floyd()
{
for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
G[i][j] = min(G[i][j],G[i][k]+G[k][j]);
printf("%d\n",G[s][t]==INF?-1:G[s][t]);
}


补充一下:对于floyd判断负环是否存在只需检查是否存在d[i][i]是负数的顶点i 即可。

SPFA算法(bellman-ford算法的优化):设立一个先进先出的 队列 用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短 路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

SPFA 是这样判断负环的: 如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)

期望的时间复杂度:O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。

SPFA算法有两个优化算法 SLF 和 LLL: SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<

dist(i),则将j插入队首,否则插入队尾。 LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定 ,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。

1 对图的建立和处理,dis
数组的初始化等等操作

2 Q += s //Q 为一个队列 s为源点

3 while Q ≠ ∅//队列不为空

4 u = Q中的点//从Q中取出一个点u

5 把u点标记为为访问过的

6 for each vertex v∈ G.Adj[u]//对所有的边

7 relax(u,v,w)//进行松弛

8 if(v 未被访问过)//若v未被访问过

9 Q += v;//加入队列

此算法分为3部分 :

1) 第1~2行 建图对dis
和vis
数组等数组进行初始化。 若判断负环需要加一个flag
数组,初始化为0,某点 u 若加入Q队列一次,怎flag[u]++,若flag[u]>=n,说明u进入队列的次数大于点的个数,因此此图存在负环,返回一个布尔值。

2) 第3行当队列不为空的时候进行操作

3) 第4~9行 取出Q中的点u ,用u对所有的边进行松弛操作,若松弛成功,判断该点v是否被访问过,若未访问过加入Q队列中。

所用数据结构:前向星 双向队列

struct Edge
{
int to,w;//to 终点,w 权值
int next;//下一个
}edge[maxn];//前向星
int head[503];//头指针式的数组
int cnt;//下标
deque<int>que;//双向队列


算法实现:

bool SPFA()
{
int u, v;//u 从Q中取出的点  v找到的点
int dis[maxn];//保存最短路径
int flag[maxn];//保存某点加入队列的次数
bool vis[maxn];//标记数组
deque<int>que;//双向队列
fill(dis,dis+n+1,MAX);//初始化
memset(flag,0,sizeof(flag));//初始化
memset(vis,false,sizeof(vis));//初始化
dis[s] = 0;
que.push_back(1);//将s = 1 加入队列
while(!que.empty())//当队列不为空
{
u = que.front();//从队列中取出一个数
que.pop_front();//删除
vis[u] = false;//标记为未访问
for(i=head[u];i!=-1;i=edge[i].next)//对所有与该边相连的边进行查找
{
v = edge[i].to;//保存点 便于操作
if(dis[v]>dis[u]+edge[i].w)//进行松弛操作
{
dis[v] = dis[u]+edge[i].w;//松弛成功
if(!vis[v])//若该点未被标记
{
vis[v] = true;//标记为真
flag[v]++;//该点入队列次数++
if(flag[v]>=n)//若该点进入队列次数超过n次 说明有负环
return true;//返回有负环
//一下为SLF优化
if(!que.empty() && dis[v]<dis[que.front()])//若为队列不为空 && 队列第一个点的最短距离大于当前点的最短距离
que.push_front(v);//将该点放到队首
else//不然
que.push_back(v);//放入队尾
}
}
}
}
return false;//没有负环
}


BFS 求解最短路+路径打印:采用邻接表或前向星进行图的存储 , 则BFS的时间复杂度为开始的初始化O(V)+BFS操作O(E) = O (V+E)

所用数据结构:前向星 优先队列

struct P
{
int v, w;//v 顶点 w 最短距离
bool operator <(const P &a)const
{
return a.w < w;//按w从小到大排序
}
};
priority_queue<P>que;//优先队列   按w从小到大
struct Edge//前向星
{
int to, w;//v 顶点  w权重
int next;//下一个位置
}edge[maxn];
int head[maxn];//头指针数组


算法实现:

void BFS()
{
priority_queue<P>que;//优先队列   按w从小到大
bool vis[maxn];//标记数组, 标记是否被访问过
P p, q;
int v;
memset(vis,false,sizeof(vis));//初始化
p.to = s;//顶点为 s
p.w = 0;//距离为 0
que.push(p);//放入队列
while(!que.empty())//队列不为空
{
p = que.top();//取出队列的队首
que.pop();//删除
if(p.to == t)//若找到终点
{
printf("%d\n",p.w);//输出结果
return ;//返回
}
vis[p.to] = true;//此点标记为访问过
for(i=head[p.to];i!=-1;i=edge[i].next)//查找与该点相连的点
{
v = edge[i].to;
if(vis[v] == false)//若点未被访问过
{
q.to = v;//存入结构体
q.w = p.w+edge[i].w;//距离更新
que.push(q);//放入队列
}
}
}
printf("-1\n");//若没有到达终点  输出-1
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: