图论讲义P2
本讲义上接 浙江科技学院ACM新生培训图论讲义1 图论讲义P1
5 最短路问题
最短路问题是指:在一张图中的任意两个点,从一个点出发,经过任意边,到达另一个点,如何决策使得经过的边的边权和最小(或者是最大,本质上是一类问题)。
如果一张图有负环,则不存在最短路径,同样的,如果一张图有正环,那么就不存在最长路径。
5.a.迪彻斯特初步
对于单源最短路问题,最常用的方法是迪彻斯特,他要求图中不存在负数权值的边。
迪彻斯特的算法思想类似于广度优先搜索,通过已经确定最短路的点来更新最短路未确定的节点(或者已经初步确定最短路,但是之后可能还会更新)。
单源最短路指的是起点就只有一个点 S。
我们以
D
i
s
i
Dis_i
Disi来表示点
i
i
i 的最短路径(以S作为起点)。在求解过程中
D
i
s
i
Dis_i
Disi 可以被不断更新,一开始 所有点的最短路径都是无穷(INF),起点的最短路径初始化为0
D
i
s
s
=
=
0
Dis_s==0
Diss==0
一开始,只有源点(起点)S的最短路
D
i
s
Dis
Dis 是被确定的,我们使用源点去更新与源点相连的点的
D
i
s
Dis
Dis,即松弛操作,更新完成后有部分点的
D
i
s
≠
∞
Dis \ne ∞
Dis=∞ 。
我们再将除了起点S之外
D
i
s
Dis
Dis最小的点作为源点,来更新其他的点。(除了起点S之外
D
i
s
Dis
Dis最小的点的最短路
D
i
s
Dis
Dis此时就是最终答案了(已经确定),由于该点的
D
i
s
Dis
Dis是最小的,它的
D
i
s
Dis
Dis是无法被其他点更新,导致该点的
D
i
s
Dis
Dis进一步变小)。
完成更新之后,我们再取没有作为过源点的并且
D
i
s
Dis
Dis最小的点,作为源点,进行更新操作。
更新操作(松弛):与当前源点直接有边相连的点
x
x
x,从起点S出发经过当前点到达
x
x
x后的最短路,如果比
D
i
s
x
Dis_x
Disx(
x
x
x原先的最短路)小,那么将
D
i
s
x
Dis_x
Disx更新。
重复操作,直到没有点的最短路被更新。
(图片来源网络,侵删)
我们可以结合图像来进一步理解算法。
假如需要求a点到其他点的最短路。
此时
D
i
s
a
=
0
Dis_a = 0
Disa=0
第一次我们使用源点更新完其他点后
D
i
s
b
=
2
:
D
i
s
f
=
9
:
D
i
s
d
=
6
:
D
i
s
a
=
0
:
D
i
s
o
t
h
e
r
s
=
∞
Dis_b=2:Dis_f=9:Dis_d=6 :Dis_a=0:Dis_{others}=∞
Disb=2:Disf=9:Disd=6:Disa=0:Disothers=∞
除了a之外,b的最短路
D
i
s
Dis
Dis 是最短的,此时,b的最短路已经是最终答案了,无论用除了b之外的任意一个点,都无法更新b的最短路
D
i
s
b
Dis_b
Disb使其变得更小。
接下来用b作为源点更新其他点,同样会产生一个除了b之外最短路
D
i
s
Dis
Dis最短的点,这个点的最短路也同理被确定。
最终
D
i
s
a
=
0
:
D
i
s
b
=
2
:
D
i
s
d
=
3
:
D
i
s
e
=
5
:
D
i
s
f
=
9
:
D
i
s
g
=
12
:
D
i
s
c
=
13
:
D
i
s
h
=
18
Dis_a=0:Dis_b=2:Dis_d=3:Dis_e=5:Dis_f=9:Dis_g=12:Dis_c=13:Dis_h=18
Disa=0:Disb=2:Disd=3:Dise=5:Disf=9:Disg=12:Disc=13:Dish=18
//迪彻斯特最短路参考代码 MGraph 为邻接矩阵 其中 arcs 为存邻接矩阵边信息的二维数组
//mindis存最短路Dis
void Dij_ShortestPath(MGraph G,closedis &mindis)
{
for(int i=0;i<G.vexnum;++i) // 最开始,所有点的dis初始化为INF
{
mindis[i].dis=0x7fffffff;
}
mindis[0].dis=0; // 起点S的最短路为0
int st=0; // 源点,初始源点就是起点
for(int round=1;round<G.vexnum;round++) // 最多进行v(点数)论松弛操作
{
int tmp=0;int distmp=0x7fffffff;
vis[st]=1;
for(int vtx=0;vtx<G.vexnum;vtx++)
{
if(mindis[st].dis+G.arcs[st][vtx]<mindis[vtx].dis) // 更新操作:与当前源点直接相连的点vtx,从起点S
//出发经过当前点到达vtx后的最短路,如果比Dis_vtx(vtx原先的最短路)小,那么将Dis_vtx更新。
{
mindis[vtx]=mindis[st];
mindis[vtx].dis+=G.arcs[st][vtx];
}
if(vis[vtx]==0&&mindis[vtx].dis<distmp) // 找出下一个源点
{
tmp=vtx;
distmp=mindis[vtx].dis;
}
}
st=tmp;
}
}
5.b.迪彻斯特实现
通过5.a的学习,我们已经了解迪彻斯特算法的具体过程,但是观察5.a中的代码,可以发现,每次通过遍历所有点,来找出
D
i
s
Dis
Dis最小的点,显然时间复杂度
O
(
n
2
)
O(n^2)
O(n2)。当
n
≥
10000
n\geq 10000
n≥10000时,就很难在规定时间内计算出答案,在算法竞赛中,程序的运行效率也是非常重要的。
有没有什么方法可以控制时间复杂度在一个可以接受的范围之内呢?
每次松弛操作,我们只更新了有限个点,而且每次我们只需要知道哪个点的
D
i
s
Dis
Dis最小。利用优先队列来维护Dis,每次松弛操作取堆顶元素(最小值),并将更新后的
D
i
s
Dis
Dis加入堆。这样就可以将原来
O
(
n
2
)
O(n^2)
O(n2) 的时间复杂度降低到
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)(具体时间复杂度为O(vlogv+e))。
例题:最短路
本题的点数数据规模为1e5,需要求解给定起点到其他所有点的最短路。
/*
OriginProblem:Luogu P4479
Author: 洛谷用户118873
LastModify:2022/1/14
*/
#include<iostream>
#include<bits/stdc++.h>
#include<queue>
#define ll long long
using namespace std;
const int maxn=1e6+2;
priority_queue<pair<ll,int> >que;//定义优先队列,其中用pair来存距离Dis和编号。
// 也可以自定义结构体来存储编号和Dis。
ll w[maxn];// 链式前向心
int to[maxn];
int head[maxn];
int vis[maxn];
ll dit[maxn];
int nxt[maxn];
int fr,ed;
int tot=0;
int n,m,s;// 点数,边数,起点编号
int addedge(int f,int t,int we)// 链式前向心加边。
{
tot++;
to[tot]=t;
w[tot]=we;
nxt[tot]=head[f];
head[f]=tot;
return 0;
}
void Dij_ShortestPath(int p)
{
for(round = 1;round <= 2*n;round ++)
{
vis[p]=1;
for(int i=head[p];i!=0;i=nxt[i])// 遍历,访问与当前点相连的所有点
{
if(vis[to[i]]!=1&&dit[to[i]]>dit[p]+w[i])// 更新操作(松弛)
{
dit[to[i]]=dit[p]+w[i];
que.push(make_pair(-dit[to[i]],to[i]));//将更新后的Dis 和点的编号加入优先队列中,其中优先队列
// 默认的排序方式时 less<> 也就是堆顶最大值,我们需要的是最小值,故取相反数存入
}
}
if(que.empty())break;// 如果没有点被更新(加入队列)则退出循环
while(1){
p=que.top().second;
que.pop();
if(que.empty()||vis[p]!=1)
break;
}// 找出下一个源点,这个源点不仅要满足是Dis最小的点,还要满足之前没有作为源点取更新其他的点
}
}
int main()
{
cin>>n>>m>>s;
for(int i=1;i<=n;i++)// 初始化Dis
dit[i]=1e10;
dit[s]=0;
for(int i=1;i<=m;i++)
{
int in1,in2,in3;
cin>>in1>>in2>>in3;
addedge(in1,in2,in3);
}
Dij_ShortestPath(s);
for(int i=1;i<=n;i++)
{
cout<<dit[i]<<" ";// 按照题目要求,输出起点到所有点的最短路
}
}
5.c.弗洛伊德
弗洛伊德算法的思想类似于动态规划。
弗洛伊德求解的是多源最短路,也就是求解任意两个点之间的最短路,由于我们要表示任意两个点之间的最短路,只能使用邻接矩阵(二维数组)来记录任意两个点之间的最短路信息。
假设矩阵G
G
i
,
j
G_{i,j}
Gi,j表示 原图的邻接矩阵,F
F
i
,
j
F_{i,j}
Fi,j表示i到j的最短路。
最初,
F
i
,
j
F_{i,j}
Fi,j矩阵中,除了能直接到达的,其余最短路皆为INF。
每次枚举两个点S,T以及一个中转点K,看看是否满足以K作为桥梁,即S->K->T,所经过的最短路比原来S->T的最短路要小。如果满足,即更新S到T的最短路,其中我们要枚举3个点,所以时间复杂度为
O
(
n
3
)
O(n^3)
O(n3)。
转移方程为
F
i
,
j
=
m
i
n
(
F
i
,
k
+
F
k
,
j
,
F
i
,
j
)
F_{i,j} = min(F_{i,k}+F_{k,j},F_{i,j})
Fi,j=min(Fi,k+Fk,j,Fi,j)
//弗洛伊德核心代码
for(int k = 1;k <= n;k ++)
{
for(int i = 1;i <= n;++i)
{
for(int j = 1;j <= n;j++)
{
F[i][j] = min(F[i][k] + F[k][j],F[i][j]);
}
}
}
大家可以根据这张图来画一下矩阵F来理解弗洛伊德的过程。
5.d.贝尔曼福德
贝尔曼福德算法是最简单最暴力的最短路算法,其松弛操作与迪彻斯特算法相似,每次枚举所有的边,用边弧尾节点对弧头节点进行松弛(更新)操作。一共需要枚举v - 1轮。
一开始所有点的
D
i
s
Dis
Dis为INF,起点的
D
i
s
Dis
Dis为0。(
D
i
s
Dis
Dis的含义与5.a中一样)
然后进行v - 1轮松弛操作,每轮枚举所有的边,对每一条边的操作是:
弧尾节点S,弧头节点T,S的最短路
D
i
s
Dis
Dis加上S,T之间的边,是否小于T的最短路
D
i
s
Dis
Dis,如果小于则更新T的最短路。
基于迪彻斯特的思想,每一轮松弛操作,都有一个点的最短路被确定,所以只要v - 1轮。
如果v - 1轮之后,还有点可以被更新,说明图存在负环。
贝尔曼福德算法支持负权边。
由于贝尔曼福德的时间复杂度相对较高,一般不适用这个算法。
贝尔曼福德算法也仅仅在解决费用流等的数据规模相对较小的问题中用的比较多。
for (int round = 1;round <= v - 1;round++)// 贝尔曼福德
{
for(int i = 1;i <= e;i++)
{
Dis[Edge[i].T] = min(Edge[i].S+Edge[i].val,Edge[i].T);// 松弛操作,其中val是边权,S是弧尾节点,T是弧头节点
}
}
for(int i = 1;i <= e;i++)
{
if(Dis[Edge[i].T]>Dis[Edge[i].S]+Edge[i].val)return -1;// 存在负环 -1退出
}