一、最短路
单源最短路问题:求源点 s s s 到图中其余各顶点的最短路径长度。
多源最短路问题:求图上任意两个点之间的最短路径长度。
在带权图 G = ( V , E ) G=(V,E) G=(V,E) 中,每条边都有一个权值 w i w_i wi,即边的长度。两个顶点之间的路径长度为路径上所有边权之和。
如果用我们之前学习的 d f s dfs dfs 来解决单源最短路问题,效率上会很慢,时间复杂度将是 2 2 2 的幂这一级数,能解决的问题的数据规模非常小。 而 b f s bfs bfs 能解决的最短路问题只限制在边权为 1 1 1 的图上。对于边权不同的图,利用 b f s bfs bfs 求解最短路是错误的。所以我们需要更高效的算法来帮助我们解决这两个问题。
二、Dijkstra算法
1.简介
解决单源最短路径问题常用 Dijkstra 算法,用于计算一个顶点到其他所有顶点的最短路径。Dijkstra 算法的主要特点是以起点为中心,逐步向外扩展,每次都会取一个最近点继续扩展,直到取完所有点为止。注意:Dijkstra 算法要求图中不能出现负权边。
2.算法流程
我们定义带权图 G G G 所有顶点的集合为 V V V,接着我们再定义已确定从源点出发的最短路径的顶点集合为 U U U,初始集合 U U U 为空,记从源点 s s s 出发到每个顶点 v v v 的距离为 d i s t v dist_v distv,初始 d i s t s = 0 dist_s=0 dists=0 。接着执行以下操作:
- 从 V − U V-U V−U 中找出一个距离源点最近的顶点 v v v,将 v v v 加入集合 U U U。
- 用 d i s t v dist_v distv 和顶点 v v v 连出的边来更新和 v v v 相邻的、不在集合中的 U U U 顶点的 ,这一步称为松弛操作。
- 重复前两个步骤,直到 V = U V=U V=U 或找不出一个从 s s s 出发有路径到达的顶点,算法结束。
如果最后 V ≠ U V\neq U V=U,说明有顶点无法从源点到达(即图不连通);否则每个 d i s t i dist_i disti 表示从 s s s 出发到顶点 i i i 的最短距离。
Dijkstra 算法的时间复杂度为 O ( V 2 ) O(V^2) O(V2),其中 V V V 表示顶点的数量。
3.模板代码
void dijkstra(ll u)
{
memset(vis,false,sizeof(vis));
memset(dis,0x7f,sizeof(dis));
dis[u]=0;
for(ll i=1;i<=n;i++)
{
ll mi=inf;
for(ll j=1;j<=n;j++)
{
if(!vis[j] && dis[j]<mi)
{
mi=dis[j];
u=j;
}
}
if(mi==inf)
return;
vis[u]=true;
for(ll j=p[u];j!=-1;j=e[j].next)
{
ll v=e[j].v,w=e[j].w;
if(!vis[v] && dis[v]>dis[u]+w)
dis[v]=dis[u]+w;
}
}
}
4.堆优化
Dijkstra 算法的核心思想就是维护一个还没有确定最短路的点的集合,每次从这个集合中选取一个路径长度最小的点确定最短路,并更新余下其他点的路径。
如果每次都暴力枚举选取距离最短的点,那么时间复杂度为
O
(
V
2
)
O(V^2)
O(V2)。我们完全可以考虑采用堆优化的方式,用set
来维护点集,这样时间复杂度就优化到了
O
(
(
V
+
E
)
l
o
g
V
)
O((V+E)log\ V)
O((V+E)log V),对于稀疏图的优化效果非常好。
struct New
{
ll id;
ll len;
bool operator<(const New &x)const
{
if(len!=x.len)
return len<x.len;
return id<x.id;
}
};
set<New> s;
void dijkstra()
{
memset(dist,0x7f,sizeof(dist));
dist[S]=0;
s.insert((New){S,0});
while(!s.empty())
{
set<New>::iterator it=s.begin();
ll d=(*it).len,u=(*it).id;
s.erase(*it);
vis[u]=true;
for(ll i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v;
ll w=e[i].w;
if(!vis[v] && dist[v]>dist[u]+w)
{
s.erase((New){v,dist[v]});
dist[v]=dist[u]+w;
s.insert((New){v,dist[v]});
}
}
}
}
三、SPFA算法
1.简介
SPFA(Shortest Path Faster Algorithm)算法和 dijkstra 一样,是一种计算单源最短路径的算法, 通常被认为是 Bellman-ford 算法的队列优化,在代码形式上接近于宽度优先搜索 BFS,是一个在实践中非常高效的单源最短路算法。
2.算法流程
在 SPFA 算法中,使用 d i d_i di 表示从源点到顶点 i i i 的最短路,额外用一个队列 来保存即将进行拓展的顶点列表,并用 i n q i inq_i inqi 来标识顶点 i i i 是不是在队列中。
- 初始队列中仅包含源点,且源点 s s s 的 d s = 0 d_s=0 ds=0。
- 取出队列头顶点
u
u
u ,扫描从顶点
u
u
u 出发的每条边,设每条边的另一端
v
v
v 为 ,边
<
u
,
v
,
w
>
<u,v,w>
<u,v,w> 权值为
w
w
w,若
d
u
+
w
<
d
v
d_u+w<d_v
du+w<dv,则:
- 将 d v d_v dv 修改为 d u + w d_u+w du+w
- 若 v v v 不在队列中,则将 v v v 入队
- 重复上述步骤直到队列为空
最终的 d d d 数组就是从源点出发到每个顶点的最短路距离。如果一个顶点从没有入队过,则说明没有从源点到该顶点的路径。
很显然,SPFA 的空间复杂度为 。如果顶点的平均入队次数为 k k k ,则 SPFA 的时间复杂度为 O ( K E ) O(KE) O(KE),对于较为随机的稀疏图,根据经验 k k k 一般不超过 4 4 4。
3.模板代码
void spfa(int start)
{
memset(inq,false,sizeof(inq));
memset(dis,0x7f,sizeof(dis));
dis[start]=0;
queue<int> s;
s.push(start);
inq[start]=vis[start]=true;
while(!s.empty())
{
int u=s.front();
s.pop();
inq[u]=false;
for(int i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v,w=e[i].w;
if(dis[u]+w<dis[v])
{
dis[v]=dis[u]+w;
if(!inq[v])
{
inq[v]=vis[v]=true;
s.push(v);
}
}
}
}
}
4.SPFA判负环
如果图上出现负环,那么 SPFA 算法将永远不会终止,所以需要提前判断是否出现负环,以此来及时跳出循环,以免出现 Run time error。
实际上只需要做一点点小小的修改就可以完成对负环的判断了。只要一个点入队的次数大于顶点总数 n n n ,则表示图中包含负环。
bool spfa(ll u)
{
memset(inq,false,sizeof(inq));
memset(cnt,0,sizeof(cnt));
memset(dis,0x7f,sizeof(dis));
inq[u]=vis[u]=true;
dis[u]=0;
cnt[u]=1;
q.push(u);
while(!q.empty())
{
u=q.front();
q.pop();
inq[u]=false;
for(ll i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v,w=e[i].w;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
if(!inq[v])
{
q.push(v);
vis[v]=inq[v]=true;
++cnt[v];
if(cnt[v]>n)
return true;
}
}
}
}
return false;
}
5.优化
SPFA 算法有两个优化策略 SLF 和 LLL:
- SLF:Small Label First 策略,设要加入的顶点是 j j j,队首元素为 i i i,若 d [ j ] < d [ i ] d[j]<d[i] d[j]<d[i] ,则将 j j j 插入队首,否则插入队尾;
- LLL:Large Label Last 策略,设队首元素为 i i i,队列中所有最短距离值的平均值为 x x x,若 d [ i ] > x d[i]>x d[i]>x 则将 i i i 插入到队尾,查找下一元素,直到找到某一顶点 i i i 使得 d [ i ] ≤ x d[i]\leq x d[i]≤x,则将 i i i 出队进行松弛操作。
- SLF 可使速度提高 15 ∼ 20 % 15\sim20\% 15∼20%;SLF + LLL 可提高约 50 % 50\% 50%。
在解决算法题目时,一般来说不带优化的 SPFA 就足以解决问题;而一些题目会故意 制造出让 SPFA 效率低下的数据,即使你使用这两个优化也无法避免“被 卡”。
对于稀疏图而言,SPFA 相比堆优化的 Dijkstra 有很大的效率提升,但是对于稠密图而言,SPFA 最坏为 O ( V E ) O(VE) O(VE),远差于堆优化 Dijkstra 的 O ( ( V + E ) l o g V ) O((V+E)logV) O((V+E)logV)。
四、Floyd算法
1.简介
Floyd 算法是一种利用动态规划的思想、计算给定的带权图中任意两个顶点 之间最短路径的算法。相比于重复执行多次单源最短路算法,Floyd 具有高效、代码简短的优势,在解决图论最短路题目时比较常用。注意 Floyd 算法虽然能处理负边权,但是依然无法处理负环。Floyd算法对于稠密图会有比较大的优势。
2.算法流程
我们用 d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j] 表示 i i i 到 j j j 能经过 1 ∼ k 1\sim k 1∼k 的点的最短路。那么实际上 d p [ 0 ] [ i ] [ j ] dp[0][i][j] dp[0][i][j] 就是原图,如果 i , j i,j i,j 之间存在边,那么 i , j i,j i,j 之间不经过任何点的最短路就是边长,否则, i , j i,j i,j 之间的最短路为无穷大。
那么对于 i , j i,j i,j 之间经过 1 ∼ k 1\sim k 1∼k 的最短路 d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j] 可以通过经过 1 ∼ k − 1 1\sim k-1 1∼k−1 的最短路转移过来。
- 如果不经过第 k k k 个点,那么就是 d p [ k − 1 ] [ i ] [ j ] dp[k-1][i][j] dp[k−1][i][j] 。
- 如果经过第 k k k 个点,那么就是 d p [ k − 1 ] [ i ] [ k ] + d p [ k − 1 ] [ k ] [ j ] dp[k-1][i][k]+dp[k-1][k][j] dp[k−1][i][k]+dp[k−1][k][j]。
所以就有转移:
d
p
[
k
]
[
i
]
[
j
]
=
m
i
n
(
d
p
[
k
−
1
]
[
i
]
[
j
]
,
d
p
[
k
−
1
]
[
i
]
[
k
]
+
d
p
[
k
−
1
]
[
k
]
[
j
]
)
dp[k][i][j]=min(dp[k−1][i][j],dp[k−1][i][k]+ dp[k−1][k][j])
dp[k][i][j]=min(dp[k−1][i][j],dp[k−1][i][k]+dp[k−1][k][j])
我们再仔细分析可以发现,
d
p
[
k
]
dp[k]
dp[k] 只能由
d
p
[
k
−
1
]
dp[k-1]
dp[k−1] 转移过来,如果你不想思考的话,这里显然也可以采用滚动数组来优化。
继续分析状态转移方程,不难发现
d
p
[
k
−
1
]
[
i
]
[
k
]
=
=
d
p
[
k
]
[
i
]
[
k
]
dp[k-1][i][k]==dp[k][i][k]
dp[k−1][i][k]==dp[k][i][k]。因为
i
i
i 到
k
k
k 的最短路中间肯定不会经过
k
k
k 。同理,
d
p
[
k
−
1
]
[
k
]
[
j
]
=
d
p
[
k
]
[
k
]
[
j
]
dp[k-1][k][j]=dp[k][k][j]
dp[k−1][k][j]=dp[k][k][j]。那么转移实际上变成了:
d
p
[
k
]
[
i
]
[
j
]
=
m
i
n
(
d
p
[
k
−
1
]
[
i
]
[
j
]
,
d
p
[
k
]
[
i
]
[
k
]
+
d
p
[
k
]
[
k
]
[
j
]
)
dp[k][i][j]=min(dp[k-1][i][j],dp[k][i][k]+dp[k][k][j])
dp[k][i][j]=min(dp[k−1][i][j],dp[k][i][k]+dp[k][k][j])
这时候,我们尝试把
k
k
k 这一维去掉,就用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 来表示
i
,
j
i,j
i,j 之间的最短路,那么转移变成了:
∀
1
≤
k
≤
n
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
j
]
,
d
p
[
i
]
[
k
]
+
d
p
[
k
]
[
j
]
)
\forall 1\leq k\leq n\ dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
∀1≤k≤n dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
所以我们就成功地优化了算法的空间复杂度。最终空间复杂度为
O
(
n
2
)
O(n^2)
O(n2),时间复杂度为
O
(
n
3
)
O(n^3)
O(n3)。
3.模板代码
memset(dp,0x3f,sizeof(dp));
cin>>n>>m;
for(ll i=1;i<=n;i++)
dp[i][i]=0;
for(ll i=1;i<=m;i++)
{
ll u,v,w;
cin>>u>>v>>w;
dp[u][v]=dp[v][u]=w;
}
for(ll k=1;k<=n;k++)
for(ll i=1;i<=n;i++)
for(ll j=1;j<=n;j++)
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);