单源最短路问题Word下载.docx
- 文档编号:19485377
- 上传时间:2023-01-06
- 格式:DOCX
- 页数:14
- 大小:456.26KB
单源最短路问题Word下载.docx
《单源最短路问题Word下载.docx》由会员分享,可在线阅读,更多相关《单源最短路问题Word下载.docx(14页珍藏版)》请在冰豆网上搜索。
i<
n;
i++)d[i]=INF;
d[start]=0;
//计算n次
i++){
intx,min=INF;
//在所有未标号的结点中,选择一个d值最小的点x。
for(inta=0;
a<
a++)if(!
visited[a]&
&
d[a]<
min)min=d[x=a];
visited[x]=true;
//标记这个点x。
//对于从x出发的所有边(x,y),更新一下d[y]。
for(inty=0;
y<
y++)if(d[y]>
d[x]+G[x][y]){
d[y]=d[x]+G[x][y];
prev[y]=x;
//y这个最短路径是从x走到y。
}
(2)使用优先队列的Dijkstra算法*[邻接表]
朴素的Dijkstra算法在选d值最小的点时要浪费很多时间,所以可以用优先队列(最小堆)来优化。
O[(n+m)logm],最差情况(密集图)为O(n2logm)
//需要的头文件:
<
queue>
、<
vector>
utility>
typedefpair<
int,int>
pii;
//将终点和最短路径长度“捆绑”的类型
//定义一个优先队列,d值最小的先出列
priority_queue<
pii,vector<
pii>
greater<
>
q;
intd[N],prev[N];
q.push(make_pair(d[start],start));
while(!
q.empty()){
//在所有未标号的结点中,选择一个d值最小的点x。
piiu=q.top();
q.pop();
intx=u.second;
if(u.first!
=d[x])continue;
//已经计算完
for(edge*e=adj[x];
e!
=NULL;
e=e->
next){
int&
v=e->
v,&
w=e->
w;
if(d[v]>
d[x]+w){
d[v]=d[x]+w;
//松弛
prev[v]=x;
q.push(make_pair(d[v],v));
}}}}
(3)Bellman-Ford算法[边目录]
Bellman-Ford算法是迭代法,它不停地调整图中的顶点值(源点到该点的最短路径值),直到没有点的值调整了为止。
该算法除了能计算最短路,还可以检查负环(一个每条边的权都小于0的环)。
如果图中有负环,那么这个图不存在最短路。
boolFord(intstart){//有负环则返回false
for(inti=0;
//初始化
d[start]=0;
for(intk=0;
k<
n-1;
k++)//迭代n次
m;
i++){//检查每条边
x=u[i],&
y=v[i];
if(d[x]<
INF)d[y]=min(d[y],d[x]+w[i]);
//下面的代码用于检查负环——如果全部松弛之后还能松弛,说明一定有负环
i++){//再次检查每条边
int&
if(d[y]>
d[x]+w[i])returnfalse;
returntrue;
Dijkstra算法是处理单源最短路径的有效算法,但它局限于边的权值非负的情况,若图中出现权值为负的边,Dijkstra算法就会失效,求出的最短路径就可能是错的。
这时候,就需要使用其他的算法来求解最短路径,Bellman-Ford算法就是其中最常用的一个。
该算法由美国数学家理查德•贝尔曼(Richard
Bellman,
动态规划的提出者)和小莱斯特•福特(Lester
Ford)发明。
适用条件&
范围:
单源最短路径(从源点s到其它所有顶点v);
有向图&
无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图);
边权可正可负(如有负权回路输出错误提示);
差分约束系统;
Bellman-Ford算法的流程如下:
给定图G(V,
E)(其中V、E分别为图G的顶点集与边集),源点s,数组Distant[i]记录从源点s到顶点i的路径长度,初始化数组Distant[n]为,
Distant[s]为0;
以下操作循环执行至多n-1次,n为顶点数:
对于每一条边e(u,
v),如果Distant[u]
+
w(u,
v)
Distant[v],则另Distant[v]
=
Distant[u]+w(u,
v)。
v)为边e(u,v)的权值;
若上述操作没有对Distant进行更新,说明最短路径已经查找完毕,或者部分点不可达,跳出循环。
否则执行下次循环;
为了检测图中是否存在负环路,即权值之和小于0的环路。
v),如果存在Distant[u]
Distant[v]的边,则图中存在负环路,即是说改图无法求出单源最短路径。
否则数组Distant[n]中记录的就是源点s到各顶点的最短路径长度。
可知,Bellman-Ford算法寻找单源最短路径的时间复杂度为O(V*E).
首先介绍一下松弛计算。
如下图:
松弛计算之前,点B的值是8,但是点A的值加上边上的权重2,得到5,比点B的值(8)小,所以,点B的值减小为5。
这个过程的意义是,找到了一条通向B点更短的路线,且该路线是先经过点A,然后通过权重为2的边,到达点B。
当然,如果出现一下情况二则不会修改点B的值,因为3+4>
6。
Bellman-Ford算法可以大致分为三个部分
第一,初始化所有点。
每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的值设为无穷大(表示不可达)。
第二,进行循环,循环下标为从1到n-1(n等于图中点的个数)。
在循环内部,遍历所有的边,进行松弛计算。
第三,遍历途中所有的边(edge(u,v)),判断是否存在这样情况:
d(v)>
d(u)+w(u,v)
则返回false,表示途中存在从源点可达的权为负的回路。
之所以需要第三部分的原因,是因为,如果存在从源点可达的权为负的回路。
则应为无法收敛而导致不能求出最短路径。
考虑如下的图:
经过第一次遍历后,点B的值变为5,点C的值变为8,这时,注意权重为-10的边,这条边的存在,导致点A的值变为-2。
(8+-10=-2)第二次遍历后,点B的值变为3,点C变为6,点A变为-4。
正是因为有一条负边在回路中,导致每次遍历后,各个点的值不断变小。
在回过来看一下bellman-ford算法的第三部分,遍历所有边,检查是否存在d(v)>
d(u)+w(u,v)。
因为第二部分循环的次数是定长的,所以如果存在无法收敛的情况,则肯定能够在第三部分中检查出来。
比如图三,此时,点A的值为-2,点B的值为5,边AB的权重为5,5>
-2+5.检查出来这条边没有收敛。
所以,Bellman-Ford算法可以解决图中有权为负数的边的单源最短路径问。
(4)SPFA!
[邻接表]
SPFA是使用队列实现的Bellman-Ford算法。
操作步骤如下:
①初始队列和标记数组。
②源点入队。
③对队首点出发的所有边进行松弛操作(即更新最小值)。
④将不在队列中的尾结点入队。
⑤队首点更新完其所有的边后出队。
queue<
int>
boolinqueue[N];
//是否在队列中
intcnt[N];
//检查负环时使用:
结点进队次数。
如果超过n说明有负环。
boolSPFA(intstart){//有负环则返回false
//初始队列和标记数组
memset(cnt,0,sizeof(cnt));
q.push(start);
//源点入队
cnt[start]++;
intx=q.front();
inqueue[x]=false;
//对队首点出发的所有边进行松弛操作(即更新最小值)
e!
e=e->
w
if(d[v]>
d[x]+w){
d[v]=d[x]+w;
if(!
inqueue[v]){//将不在队列中的尾结点入队
inqueue[v]=true;
q.push(v);
if(++cnt[v]>
n)returnfalse;
}}}}//有负环
}
求单源最短路的SPFA算法的全称是:
ShortestPathFasterAlgorithm,是Bellman-Ford算法0(VE)的一个优化,期望的时间复杂度O(2E),E为图的边数,所以SPFA用在稀疏图上的效果会更加明显。
SPFA对Bellman-Ford算法优化的关键之处在于意识到:
只有那些在前一遍松弛中改变了距离估计值的点,才可能引起他们的邻接点的距离估计值的改变。
很多时候,给定的图存在负权边,这时类似Dijkstra算法(0(V^2),在稠密图上有优势)便没有了用武之地,而bellman_ford的复杂度又过高,SPFA算法便派上用场了。
算法核心思想:
理解这个算法,最好先看看Bellman-Ford,因为他是对Bellman-Ford的一个优化,SPFA算法采用了一个队列,优化算法,用数组dict[]记录每个结点的最短路径估计值,而且用邻接表来存储图G。
我们采取的方法是动态逼近法:
设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。
这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
需要注意的是:
仅当图不存在负权回路时,SPFA能正常工作。
如果图存在负权回路,由于负权回路上的顶点无法收敛,总有顶点在入队和出队往返,队列无法为空,这种情况下SPFA无法正常结束。
1记录每个结点进队次数,超过|V|次表示有负权
2记录这个结点在路径中处于的位置,ord[i],每次更新的时候ord[i]=ord[x]+1,若超过|V|则表示有负圈....
下面举一个实例来说明SFFA算法是怎样进行的:
设有一个有向图G={V,E},其中,V
{V0,V1,V2,V3,V4},E={<
V0,V1>
<
V0,V4>
V1,V2>
V1,V4>
V2,V3>
V3,V4>
V4,V2>
}=
{2,10,3,7,4,5,6},见下图:
算法执行时各步的Queue队的值和D数组的值由下表所示:
算法执行到第五步后,队Queue空,算法结束。
源点V0到V1的最短路径为2,到V2的最短路径为5,到V3的最短路径为9,到V4的最短路径为9,结果显然是正确的。
13.7 每两点间最短路问题(APSP问题)!
此类问题使用Floyd-Warshall算法解决。
它是动态规划算法。
通过Floyd计算图G=(V,E)中各个顶点的最短路径时,需要引入一个矩阵S,矩阵S中的元素a[i][j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。
假设图G中顶点个数为N,则需要对矩阵S进行N次更新。
初始时,矩阵S中顶点a[i][j]的距离为顶点i到顶点j的权值;
如果i和j不相邻,则a[i][j]=∞。
接下来开始,对矩阵S进行N次更新。
第1次更新时,如果"
a[i][j]的距"
a[i][0]+a[0][j]"
(a[i][0]+a[0][j]表示"
i与j之间经过第1个顶点的距离"
),则更新a[i][j]为"
。
同理,第k次更新时,如果"
a[i][j]的距离"
"
a[i][k]+a[k][j]"
,则更新a[i][j]为"
更新N次之后,操作完成!
O(n3)
intf[N][N],prev[N][N];
//追踪prev可以得到最短路。
intlen=INF;
//最小环的长度
voidFloyd(){
i++)//初始化——可以在读图时完成
for(intj=0;
j<
j++)f[i][j]=G[i][j];
memset(prev,-1,sizeof(prev));
/*len=INF;
*/
k<
k++){//计算。
注意,k在最外面。
/*如果求最小环,请将下面的代码插入到这里。
i++)
for(intj=0;
j++)
if(f[i][k]+f[k][j]<
f[i][j]){
f[i][j]=f[i][k]+f[k][j];
prev[i][j]=k;
}}}
//cout<
f[i][j]<
endl;
//通过递归调用追踪prev,就可以得到最短路径上的结点。
Floyd算法可以用来求最小环(无向图!
)。
将以下代码插入到上面的标记处即可。
for(inti=0;
k;
for(intj=i+1;
len=min(len,G[i][j]+f[i][k]+f[k][j]);
//G是无向图!
len为最小环的和
13.5 最小生成树(MST)
已知n个城市,并且已知它们之间的距离。
问怎样修路才能保证道路总长最短,并且每个城市都被连接?
(1)Prim算法[邻接矩阵]
Prim算法是贪心算法,贪心策略为:
找到目前情况下能连上的权值最小的边的另一端点,加入之,直到所有的顶点加入完毕。
是用来求加权连通图的最小生成树的算法。
Prim适用于稠密图。
朴素Prim的时间复杂度是O(n2),因为在寻找离生成树最近的未加入顶点时浪费了很多时间。
所以,可以用堆进行优化。
堆优化后的Prim算法的时间复杂度为O(mlogn)。
堆优化Prim的代码比较复杂,并查集优化的Kruskal算法与它相比,要好很多。
intminEdge[N],cloest[N];
//与点N连接的最小边
intPrim(intstart=0){//start的出度不能为0!
intans=0,k=0,min;
//加入第一个点
i<
i++){
minEdge[i]=G[start][i];
cloest[i]=start;
minEdge[start]=0;
min=INF;
//寻找离生成树最近的未加入顶点k
j<
j++)
if(minEdge[j]!
=0&
minEdge[j]<
min)min=minEdge[k=j];
//把找到的边加入到MST中
ans+=minEdge[k];
minEdge[k]=0;
//加入完毕。
以后不用再处理这个点。
//重新计算最短边
if(G[k][j]<
minEdge[j]){
minEdge[j]=G[k][j];
cloest[j]=k;
returnans;
基本思想
对于图G而言,V是所有顶点的集合;
现在,设置两个新的集合U和T,其中U用于存放G的最小生成树中的顶点,T存放G的最小生成树中的边。
从所有uЄU,vЄ(V-U)(V-U表示出去U的所有顶点)的边中选取权值最小的边(u,v),将顶点v加入集合U中,将边(u,v)加入集合T中,如此不断重复,直到U=V为止,最小生成树构造完毕,这时集合T中包含了最小生成树中的所有边。
(2)Kruskal算法!
[边目录]
在含有n个顶点的连通图中选择n-1条边,构成一棵极小连通子图,并使该连通子图中n-1条边上权值之和达到最小,则称其为连通网的最小生成树。
Kruskal算法是贪心算法,贪心策略为:
选目前情况下能连上的权值最小的边,若与以生成的树不够成环,加入之,直到n-1条边加入完毕。
时间复杂度为O(nlogm),最差情况为O(mlogn)。
相比于Prim,这个算法更常用。
intparent[N],rank[M];
//p代表并查集,r是边的序号
intcomp(constinti,constintj){returnw[i]<
w[j];
}//排序时使用
intfind(intx){//带路径压缩的查找函数
returnparent[x]==x?
x:
parent[x]=find(parent[x]);
intKruskal(){
intans=0;
i++)parent[i]=i;
//初始化并查集
i++)rank[i]=i;
//边的序号(下面要按照边的权值大小来排序)
sort(rank,rank+m,comp);
//按照边的权值大小排序
inte=rank[i];
intx=find(u[e]),y=find(v[e])//找出当前边的两个端点所在集合的编号
if(x!
=y){//如果不在同一集合,合并
ans+=w[e];
parent[x]=y;
}}
基本思想:
按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路。
具体做法:
首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。
根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问题:
问题一
对图的所有边按照权值大小进行排序。
问题二
将边添加到最小生成树中时,怎么样判断是否形成了回路。
问题一很好解决,采用排序算法进行排序即可。
问题二,处理方式是:
记录顶点在"
最小生成树"
中的终点,顶点的终点是"
在最小生成树中与它连通的最大顶点"
(关于这一点,后面会通过图片给出说明)。
然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。
如右图所示:
在将<
E,F>
<
C,D>
D,E>
加入到最小生成树R中之后,这几条边的顶点就都有了终点:
C终点是F,D的终点是F,
E的终点是F,
F的终点是F。
关于终点,就是将所有顶点按照从小到大的顺序排列好之后;
某个顶点的终点就是"
与它连通的最大顶点"
因此,接下来,虽然<
C,E>
是权值最小的边。
但是C和E的重点都是F,即它们的终点相同,因此,将<
加入最小生成树的话,会形成回路。
这就是判断回路的方式。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 单源最 短路 问题