-
SPFA算法
编辑
-
简 称
- SPFA
-
全 称
- Shortest Path Faster Algorithm 提出者
- 段凡丁
目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Procedure SPFA;
Begin
initialize-single-source(G,s);
initialize-queue(Q);
enqueue(Q,s);
while
not empty(Q)
do
begin
u:=dequeue(Q);
for
each v∈adj[u]
do
begin
tmp:=d[v];
relax(u,v);
if
(tmp<>d[v]) and (not v in Q) then
enqueue(Q,v);
end;
end;
End;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
ProcedureSPFA;
Begin
initialize-
single
-source(G,s);
initialize-queue(Q);
enqueue(Q,s);
while
not
empty(Q)
do
begin
u:=dequeue(Q);
for
each v∈adj[u]
do
begin
tmp:=d[v];
relax(u,v);
if
(tmp<>d[v])
and
(
not
v
in
Q)
then
enqueue(Q,v);
end
;
end
;
End
;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
#include<iostream>
#include<vector>
#include<list>
using
namespace
std;
struct
Edge
{
int
to,len;
};
bool
spfa(
const
int
&beg,
//出发点
const
vector<list<Edge> > &adjlist,
//邻接表,通过传引用避免拷贝
vector<
int
> &dist,
//出发点到各点的最短路径长度
vector<
int
> &path)
//路径上到达该点的前一个点
//没有负权回路返回0
//福利:这个函数没有调用任何全局变量,可以直接复制!
{
const
int
INF=0x7FFFFFFF,NODE=adjlist.size();
//用邻接表的大小传递顶点个数,减少参数传递
dist.assign(NODE,INF);
//初始化距离为无穷大
path.assign(NODE,-1);
//初始化路径为未知
list<
int
> que(1,beg);
//处理队列
vector<
int
> cnt(NODE,0);
//记录各点入队次数,用于判断负权回路
vector<
bool
> flag(NODE,0);
//标志数组,判断是否在队列中
dist[beg]=0;
//出发点到自身路径长度为0
cnt[beg]=flag[beg]=1;
//入队并开始计数
while
(!que.empty())
{
const
int
now=que.front();
que.pop_front();
flag[now]=0;
//将当前处理的点出队
for
(list<Edge>::const_iterator
//用常量迭代器遍历邻接表
i=adjlist[now].begin(); i!=adjlist[now].end(); ++i)
if
(dist[i->to]>dist[now]+i->len)
//不满足三角不等式
{
dist[i->to]=dist[now]+i->len;
//更新
path[i->to]=now;
//记录路径
if
(!flag[i->to])
//若未在处理队列中
{
if
(NODE==++cnt[i->to])
return
1;
//计数后出现负权回路
if
(!que.empty()&&dist[i->to]<dist[que.front()])
//队列非空且优于队首(SLF)
que.push_front(i->to);
//放在队首
else
que.push_back(i->to);
//否则放在队尾
flag[i->to]=1;
//入队
}
}
}
return
0;
}
int
main()
{
int
n_num,e_num,beg;
//含义见下
cout<<
"输入点数、边数、出发点:"
;
cin>>n_num>>e_num>>beg;
vector<list<Edge> > adjlist(n_num,list<Edge>());
//默认初始化邻接表
for
(
int
i=0,p; i!=e_num; ++i)
{
Edge tmp;
cout<<
"输入第"
<<i+1<<
"条边的起点、终点、长度:"
;
cin>>p>>tmp.to>>tmp.len;
adjlist[p].push_back(tmp);
}
vector<
int
> dist,path;
//用于接收最短路径长度及路径各点
if
(spfa(beg,adjlist,dist,path))cout<<
"图中存在负权回路\n"
;
else
for
(
int
i=0; i!=n_num; ++i)
{
cout<<beg<<
"到"
<<i<<
"的最短距离为"
<<dist[i]<<
",反向打印路径:"
;
for
(
int
w=i; path[w]>=0; w=path[w])cout<<w<<
"<-"
;
cout<<beg<<
'\n'
;
}
}
//这段c++代码在某些情况下会runtime error
//楼上是不是没注意点是从0开始的
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
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,m:
array
[
1..
maxp]
of
integer
;
{d:队列,m:入队次数标记}
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:
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(m[b[now,i]]);
if
m[b[now,i]]=p
then
begin
writeln
(
'no way'
);halt;
end
;
{同一节点入队次数超过p,存在负环}
inc(tail);
d[tail]:=b[now,i];
v[b[now,i]]:=
true
;
end
;
end
;
v[now]:=
false
;
{释放结点,一定要释放掉,因为这节点有可能下次用来松弛其它节点}
inc(head);
{出队}
end
;
end
;
procedure
print;
begin
writeln
(dist[t]);
end
;
begin
init;
spfa(s);
print;
end
.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
#include<cstdio>
#include<iostream>
#include<cstring>
using
namespace
std;
struct
node
{
int
x,y,d,next;
};
int
num=0;
node v[11100];
int
first[105];
int
n;
int
q[105];
int
f[105];
bool
p[105];
int
start=1,end=2;
char
map[105][5055];
void
connect(
int
x,
int
y,
int
d)
{
num++;
v[num].x=x;v[num].y=y;v[num].d=d;
v[num].next=first[x];first[x]=num;
return
;
}
int
main()
{
int
temp;
int
ans=0;
memset
(first,0,
sizeof
(first));
memset
(q,0,
sizeof
(q)); q[1]=1;
memset
(f,63,
sizeof
(f)); temp=f[1];f[1]=0;
memset
(p,
false
,
sizeof
(p)); p[1]=
true
;
scanf
(
"%d\n"
,&n);
for
(
int
i=1;i<n;i++)
{
int
x=(i+1),y=1,d=0;
gets
(map[i]);
for
(
int
j=0;j<
strlen
(map[i]);j++)
{
if
(map[i][j]!=
' '
&& map[i][j]!=
'x'
) {d*=10;d+=(map[i][j]-
'0'
);}
if
(map[i][j]==
' '
|| j==
strlen
(map[i])-1)
{
if
(map[i][j-1]!=
'x'
&& map[i][j]!=
'x'
)
{
connect(y,x,d);
connect(x,y,d);
}
d=0;y++;
}
}
}
while
(start!=end)
{
int
x=q[start];
for
(
int
i=first[x];i!=0;i=v[i].next)
{
int
y=v[i].y;
if
(f[y]>f[x]+v[i].d)
{
f[y]=f[x]+v[i].d;
if
(p[y]==
false
)
{
q[end]=y;
p[y]=
true
;
end++;
if
(end>n) end=1;
}
}
}
p[x]=
false
;
start++;
if
(start>n) start=1;
}
for
(
int
i=1;i<=n;i++)
{
if
(f[i]!=temp && f[i]>ans) ans=f[i];
}
printf
(
"%d"
,ans);
return
0;
}
粗略讲讲SPFA算法的原理,SPFA算法是1994年西安交通大学段凡丁提出 是一种求单源最短路的算法 算法中需要用到的主要变量 int n; //表示n个点,从1到n标号 int s,t; //s为源点,t为终点 int d[N]; //d[i]表示源点s到点i的最短路 int p[N]; //记录路径(或者说记录前驱) queue <int> q; //一个队列,用STL实现,当然可有手打队列,无所谓 bool vis[N]; //vis[i]=1表示点i在队列中 vis[i]=0表示不在队列中
几乎所有的最短路算法其步骤都可以分为两步 1.初始化 2.松弛操作
初始化: d数组全部赋值为INF(无穷大);p数组全部赋值为s(即源点),或者赋值为-1,表示还没有知道前驱 然后d[s]=0; 表示源点不用求最短路径,或者说最短路就是0。将源点入队; (另外记住在整个算法中有顶点入队了要记得标记vis数组,有顶点出队了记得消除那个标记) 队列+松弛操作 读取队头顶点u,并将队头顶点u出队(记得消除标记);将与点u相连的所有点v进行松弛操作,如果能更新估计值(即令d[v]变小),那么就更新,另外,如果点v没有在队列中,那么要将点v入队(记得标记),如果已经在队列中了,那么就不用入队 以此循环,直到队空为止就完成了单源最短路的求解
SPFA可以处理负权边 定理: 只要最短路径存在,上述SPFA算法必定能求出最小值。 证明: 每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。(证毕) 期望的时间复杂度O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。
判断有无负环: 如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
SPFA的两种写法,bfs和dfs,bfs判别负环不稳定,相当于限深度搜索,但是设置得好的话还是没问题的,dfs的话判断负环很快
int spfa_bfs(int s) { queue <int> q; memset(d,0x3f,sizeof(d)); d[s]=0; memset(c,0,sizeof(c)); memset(vis,0,sizeof(vis)); q.push(s); vis[s]=1; c[s]=1; //顶点入队vis要做标记,另外要统计顶点的入队次数 int OK=1; while(!q.empty()) { int x; x=q.front(); q.pop(); vis[x]=0; //队头元素出队,并且消除标记 for(int k=f[x]; k!=0; k=nnext[k]) //遍历顶点x的邻接表 { int y=v[k]; if( d[x]+w[k] < d[y]) { d[y]=d[x]+w[k]; //松弛 if(!vis[y]) //顶点y不在队内 { vis[y]=1; //标记 c[y]++; //统计次数 q.push(y); //入队 if(c[y]>NN) //超过入队次数上限,说明有负环 return OK=0; } } } } return OK; }
int spfa_dfs(int u) { vis[u]=1; for(int k=f[u]; k!=0; k=e[k].next) { int v=e[k].v,w=e[k].w; if( d[u]+w < d[v] ) { d[v]=d[u]+w; if(!vis[v]) { if(spfa_dfs(v)) return 1; } else return 1; } } vis[u]=0; return 0; } |