最短路问题(整理版)

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

最短路问题(short-path problem)
若网络中的每条边都有一个权值值(长度、成本、时间等),则找出两节点(通常是源节点与结束点)之间总权和最小的路径就是最短路问题。

最短路问题是网络理论解决的典型问题之一,可用来解决管路铺设、线路安装、厂区布局和设备更新等实际问题。

最短路问题,我们通常归属为三类:单源最短路径问题(确定起点或确定终点的最短路径问题)、确定起点终点的最短路径问题(两节点之间的最短路径)
1、Dijkstra算法:
用邻接矩阵a表示带权有向图,d为从v0出发到图上其余各顶点可能达到的最短路径长度值,以v0为起点做一次dijkstra,便可以求出从结点v0到其他结点的最短路径长度
代码:
procedure dijkstra(v0:longint);//v0为起点做一次dijkstra
begin//a数组是邻接矩阵,a[i,j]表示i到j的距离,无边就为maxlongint
for i:=1 to n do d[i]:=a[v0,i];//初始化d数组(用于记录从v0到结点i的最短路径), fillchar(visit,sizeof(visit),false);//每个结点都未被连接到路径里
visit[v0]:=true;//已经连接v0结点
for i:=1 to n-1 do//剩下n-1个节点未加入路径里;
begin
min:=maxlongint;//初始化min
for j:=1 to n do//找从v0开始到目前为止,哪个结点作为下一个连接起点(*可优化) if (not visit[j]) and (min>d[j]) then//结点k要未被连接进去且最小
begin min:=d[j];k:=j;end;
visit[k]:=true;//连接进去
for j:=1 to n do//刷新数组d,通过k来更新到达未连接进去的节点最小值,
if (not visit[j]) and (d[j]>d[k]+a[k,j]) then d[j]:=a[k,j]+d[k];
end;
writeln(d[n]);//结点v0到结点n的最短路。

思考:在实现步骤时,效率较低需要O(n),使总复杂度达到O(n^2)。

对此可以考虑用堆这种数据结构进行优化,使此步骤复杂度降为O(log(n))(总复杂度降为O(n log(n))。

实现:1. 将与源点相连的点加入堆(小根堆),并调整堆。

2. 选出堆顶元素u(即代价最小的元素),从堆中删除,并对堆进行调整。

3. 处理与u相邻(即下一个)未被访问过的,满足三角不等式的顶点
1):若该点在堆里,更新距离,并调整该元素在堆中的位置。

2):若该点不在堆里,加入堆,更新堆。

4. 若取到的u为终点,结束算法;否则重复步骤2、3。

**优化代码:(DIJKSTRA+HEAP)
program SSSP;{single source shortest path}
{假设一个图的最大节点数为1000,所有运算在integer范围内}
{程序目标:给定有向图的邻接表,求出节点1到节点n的最短路径长度}
const maxn=1000;{最大节点数}
var
n:integer;{节点个数}
list:array[1..maxn,1..maxn] of integer;{邻接矩阵,表示边的长度}
count:integer;{堆内元素个数计数器}
heap:array[1..maxn] of integer;{heap[i]表示堆内的第i的元素的节点编号} pos:array[1..maxn] of integer;{表示编号为i的元素在堆内的位置}
key:array[1..maxn] of integer;{表示节点1到节点i的最短距离}
exist:array[1..maxn] of boolean;{表示节点i是否存在于堆中}
i,j,now:integer;
procedure swap(var i,j:integer);{交换整数i和j}//省略
procedure heapify(p:integer);{向下调整堆的过程}
var best:integer;
begin
best:=p;//下面两个if是分别判断根和左、右孩子最短距离的大小
if (p*2<=count) and (key[heap[p*2]]<key[heap[best]]) then best:=p*2;
if (p*2+1<=count) and (key[heap[p*2+1]]<key[heap[best]]) then best:=p*2+1; if best<>p then//若根有所变动,即跟比左右孩子都大(最短距离)
begin
swap(pos[heap[p]],pos[heap[best]]);//互换节点heap[p]、heap[best]在堆的位置swap(heap[p],heap[best]);//互换堆中元素p、best
heapify(best);//继续调整互换后的元素best
end;
end;
procedure modify(id,new_key:integer);
{判断new_key与key[id]大小,并修改key[id]大小}
var p:integer;
begin
if (new_key<key[id]) then
begin//修改
key[id]:=new_key;//更新最短距离
p:=pos[id];//结点id在堆中的位置
while (p>1) and (key[heap[p]]<key[heap[p div 2]{父}]) do//向上调整
begin
swap(pos[heap[p]],pos[heap[p div 2]]);
swap(heap[p],heap[p div 2]);
p:=p div 2;//更上一层
end;
end;
end;
procedure extract(抽出)(var id,dis:integer);
{读取堆中最小元素的节点编号和节点1到该节点的距离}
begin
id:=heap[1];//堆顶
dis:=key[id];
dec(count);//出堆
if (count>0) then//堆里还有元素
begin
swap(pos[heap[1]],pos[heap[count+1]]);// 堆顶的元素和第count+1个元素换位置swap(heap[1],heap[count+1]);//把堆顶的元素扔到count后面去,
heapify(1);//此时堆顶不一定是最小的~扔到下面去,把最小的搞上来。

end;
end;
begin
readln(n);
init;//读入邻接矩阵,没有连线的为maxint;省略
for i:=1 to n do{初始化}
begin
exist[i]:=true;//所有元素入堆
pos[i]:=i; heap[i]:=i; key[i]:=maxint;
end;
count:=n; key[1]:=0;
{dijkstra算法的主要操作}
while (count>0) do
begin
extract(i,now); exist[i]:=false;
if now=maxint then break;//无路可走了,over
for j:=1 to n do
if (exist[j]) then modify(j,now+list[i,j]);
end;
{输出}
if key[n]=maxint then writeln('Not Connected!'){节点1和节点n不连通}
else writeln(key[n]);{连通}
end.
SPFA+(前向星、循环队列、邻接表、链表)
2、SPFA算法(Shortest Path Faster Algorithm):
spfa算法是西南交通大学段凡丁于1994年发表的.
简介:给定的图存在负权边时,dijkstra等便无用武之地,spfa算法便派上用场了。

若有向加权图G不存在负权回路,即最短路径一定存在,用数组d记录每个结点的最短路径估计值,用邻接表来存储图G。

采取方法是动态逼近法:设立一个队列用来保存待优化的结点,优化时每次取出队首结点u,且用到达u点当前的最短路径估计值d[u]对点u所指向的结点v进行松弛操作,若v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾(已在队列里就不用放到队尾)。

这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

若某点入队的次数超过n次,则存在负环,spfa无法处理这样的图。

定理: 只要最短路径存在,spfa算法必定能求出最小值。

证明:松弛操作原理:“三角形两边之和大于第三边”,在信息学中称三角不等式。

每次将点放入队尾,都是经松弛操作达到的。

所谓对i,j进行松弛,即if (d[j]>d[i]+w[i,j])then d[j]:=d[i]+w[i,j],每次优化都可能将某个点v的最短路径估计值d[v]变小,d数组将会越来越小。

由于图中不存在负权回路,所以每个结点都有最短路径值,算法不会无限执行下去,随着d值的逐渐变小,到最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。

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

* 实现方法:建立一个队列,初始时队列里只有起始点,用d数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。

然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。

重复执行直到队列为空
求路径方案:若这幅图是地图的模型,除了算出最短路径
长度,还要知道路径方案。

设path数组,path[i]表示从起点
到i的最短路径中,结点i之前一个结点的编号,我们只需在
结点u对结点v进行松弛时,标记下path[v]=u即可。

spfa算法采用邻接表来存储图,方法是动态优化逼近法。

算法中用队列queue用来保存待优化的结点,优化时从队首取
出一个点w,并且用w点的当前路径d[w]去调整相连各点的路
径值d[j],若有调整,即d[j]的值改小,就将j点放入queue队列以待进一步优化。

重复执行,直至队空。

此时d数组便保存了从起点到各点的最短路径值。

实例:设有向图g={v,e},e={<v0,v1>,<v0,v4>,<v1,v2>,<v1,v4>,<v2,v3>,<v3,v4>,<v4,v2>}={2,10,3,7,4,5,6},v={v0,v1,v2,v3,v4},见上图:
算法执行时各步的queue队的值和d数组的值由下表所示。

分析:源点v0到v1、v2、v3、v4的最短路径分别为2、5、9、9,结果显然是正确的。

spfa 在形式上和宽度优先搜索非常类似,不同的是宽度优先搜索中一个点出了队就不可能重新入队,但是spfa中一个点可能在出队之后再次被一个点改进而入队,再次用来改进其它的点,这样反复迭代下去。

标准spfa代码:(以求结点t到结点s的最短路为例,稍加修改即为单源最短路)
const
maxp=10000; {最大结点数}
var
p,c,s,t:longint; {p结点数;c边数;s起点;t终点}
a,b:array[1..maxp,0..maxp] of longint;
{a[x,y]:x,y之间边的权;b[x,c]:与x相连的第c个边的另一个结点y}
d:array[1..maxp] of integer; {队列}
v:array[1..maxp] of boolean; {是否在队的标记}
dist:array[1..maxp] of longint; {到起点的最短路}
head,tail:longint; {队首/队尾指针}
procedure init;
var i,x,y,z:longint;
begin
read(p,c);
for i := 1 to c do
begin
readln(x,y,z); {x,y:一条边的两个结点;z:这条边的权值}
inc(b[x,0]); b[x,b[x,0]]:=y; a[x,y]:=z; {b[x,0]:以x为一个结点的边的条数} inc(b[y,0]); b[y,b[y,0]]:=x; a[y,x]:=z;
end;
readln(s,t); {读入起点与终点}
end;
procedure spfa(s:longint); {spfa}
var i,j,now,sum:longint;
begin
fillchar(d,sizeof(d),0);
fillchar(v,sizeof(v),false);
for j:=1 to p do dist[j]:=maxlongint;
dist[s]:=0; v[s]:=true; d[1]:=s;{队列的初始状态,s为起点}
head:=1; tail:=1;
while head<=tail do {队列不空}
begin
now:=d[head]; {取队首元素}
for i:=1 to b[now,0] do
if dist[b[now,i]]>dist[now]+a[now,b[now,i]] then
begin
dist[b[now,i]]:=dist[now]+a[now,b[now,i]]; {修改最短路} if not v[b[now,i]] then {扩展结点入队}
begin
inc(tail);d[tail]:=b[now,i];
v[b[now,i]]:=true;
end;
end;
v[now]:=false; {释放结点,此节点有可能下次用来松弛其它节点}
inc(head); {出队}
end;
writeln(dist[t]);
end;
begin
init;
spfa(s);
end.
**SPFA+前向星优化
前向星:星形(star)表示法的思想与邻接表表示法有一定的相似之处。

对每个节点,也是记录从该节点出发的所有弧,但它不是采用单向链表而是采用一个单一的数组表示,而是在该数组中首先存放从节点1出发的所有弧,然后接着存放从节点2出发的所有孤,依此类推,最后存放从节点n出发的所有孤。

对每条弧,要依次存放其起点、终点、权的数值等有关信息。

这实际上相当于对所有弧给出了一个顺序和编号,只是从同一节点出发的弧的顺序可以任意排列。

此外,为了能够快速检索从每个节点出发的所有弧,我们一般还用一个数
组记录每个节点出发的弧的起始地址(即弧的编号)。

在这种表示法中,可以快速检索从每个节点出发的所有弧,这种星形表示法称为前向星形(forward star)表示法。

例弧(1,2),(l,3),(2,4),(3,2),(4,3),(4,5),(5,3)和(5,4)上的权分别为8、9、6、4、0、3、6、7。

此时该网络图可以用前向星形表示法表示如下:节点对应的出弧的起始地址编号数组(记为point)和记录弧信息的数组:
在数组point中,其元素个数比图的节点数多1(即n+1),且一定有point[0]=1,point[n+1]=m+1。

对于节点i,其对应的出弧存放在弧信息数组的位置区间为[point[i],point[i+1]-1],如果point[i]=point[i+1],则节点i没有出弧。

在前向星形表示法中,弧被编号后有序存放,并增加一个数组(point)记录每个节点出发的弧的起始编号。

当点数多或两点间有多条弧时,一般的数据结构不能用时才考虑用前向星,
前向星的缺点:不能用起点直接终点定位,如果程序必须要定位的话,我们也不能示弱,最好不要用朴素的方法定位,这样很花时间的,用二分的思想进行查找
前向星的用途:最常用的是来优化spfa,最基本的前向星优化的spfa(有向图)代码:type rec=record
x,y,v:longint;
end;
var
a:array[1..1000] of rec;
vis:array[1..2000] of boolean; //是否在队列里
q,d,f:array[1..2001] of longint;
n,m,i,s,t,k,head,tail:longint; //s、t分别为起、终点
procedure qsort(l,r:longint); //按起点排序
procedure spfa(s:longint);
begin
fillchar(vis,sizeof(vis),false);
for i:=1 to n do d[i]:=maxlongint; //初始化
d[s]:=0;//记录s到其他点的最短路径值
head:=0; tail:=1;
q[1]:=s; vis[s]:=true;//s起点入队
repeat
inc(head); k:=q[head];
for i:=f[k] to f[k+1]-1 do //枚举起点为结点q[head]的所有边
if d[k]+a[i].v<d[a[i].y] then
begin
d[a[i].y]:=d[k]+a[i].v;
if not vis[a[i].y] then
begin
inc(tail); q[tail]:=a[i].y;
vis[a[i].y]:=true;
end;
end;
vis[k]:=false;
until head=tail;
end;
begin
readln(n,m);
for i:=1 to m do readln(a[i].x,a[i].y,a[i].v); //初始化前向星
qsort(1,m);
for i:=1 to m do
if f[a[i].x]=0 then f[a[i].x]:=i;
f[n+1]:=m+1;
for i:=n downto 1 do
if f[i]=0 then f[i]:=f[i+1];
readln(s,t);
spfa(s);
writeln(d[t]);
end.
spfa+前向星例题:
1、Vijos的P1082丛林探险便可以用前向星+SPFA快速解决
2、sweet butter 香甜的黄油
描述:农夫john发现做出全威斯康辛州最甜的黄油的方法:糖。

把糖放在一片牧场上,他知道n(1<=n<=500)只奶牛会过来舔它,这样就能做出能卖好价钱的超甜黄油。

当然,他将付出额外的费用在奶牛上。

农夫john很狡猾。

像以前的巴甫洛夫,他知道他可以训练这些奶牛,让它们在听到铃声时去一个特定的牧场。

他打算将糖放在那里然后下午发出铃声,以至他可以在晚上挤奶。

农夫john知道每只奶牛都在各自喜欢的牧场(一个牧场不一定只有一头牛)。

给出各头牛在的牧场和牧场间的路线,找出使所有牛到达的路程和最短的牧场(他将把糖放在那)
input format :(file butter.in)
第一行: 三个数:奶牛数n,牧场数p(2<=p<=800),牧场间道路数c(1<=c<=1450)
第二行到第n+1行: 1到n头奶牛所在的牧场号
第n+2行到第n+c+1行:每行有三个数:相连的牧场a、b,两牧场间距离d(1<=d<=255),当然,连接是双向的
output format :(file butter.out)
一行输出奶牛必须行走的最小的距离和
sample input
3 4 5
2
3
4
1 2 1
1 3 5
2 3 7
2 4 3
3 4 5
3、Aim Netbar(netbar.pas/c/cpp)
【问题背景】
绵阳中学以人数众多而闻名。

三个年级共有10000 多人,学生多了附近的网吧也多。

Mzoiers 都热衷于Dota,可学校的机子配置相当差(评测服务器除外),根本不能玩Dota,那就只有去网吧。

星期天到星期五都是晚上10:20才下晚自习,几乎没时间玩。

然而星期六下午放假是绝好的时间,但是学校人多啊,一放学去网吧的人就开始狂奔,竞争之激烈,抢到机子的难度非常之大。

往往在我们到达网吧之前都坐满了。

学校到网吧的路是错综复杂的,以致于到一个自己想去的网吧都有非常多的路线可以选择,而路线的长度又不相同,这样就决定了要花费的时间,因此想要尽快到达,选择一条最佳的线路是很有必要的。

【问题描述】
为了简化问题,我们把学校与周边的网吧看做图中的顶点,学校与网吧,网吧与网吧之间的路线看做边,每个边都有一个权,表示我们走完这条路的时间,由于放学人流量大,如果反向走会有危险,因此这是一个有向图。

我的的学校在 S点,想要去的网吧在 T点。

你的任务就是选择一条最佳路线,使得从学校到网吧的时间最短,你只需输出最短到达时间。

【输入文件】
netbar.in 中共有M+2 行数据
第一行两个整数 N,M,表示点数和边数。

然后M行每行3 个正整数(u,v,t),表示有一条可由u 到v耗时为 t的边。

最后一行两个正整数S、T。

【输出文件】
netbar.out 中,只有一行,一个整数表示最短时间。

如果 S、T之间不存在通路则输
出“No Solution!”(双引号不输出,“!”为西文标点)。

【输入样例】
44
1 23
2 4 10
1 35
3 45
14
【输出样例】
10
【数据规模】
对于30%的数据保证有 1<N<=1000,1<=M<=1000;
对于全部的数据保证有 1<N<=10000,1<=M<=100000。

相关文档
最新文档