23.最短路问题

一、最短路

单源最短路问题:求源点 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 VU 中找出一个距离源点最近的顶点 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\% 1520%;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 1k 的点的最短路。那么实际上 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 1k 的最短路 d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j] 可以通过经过 1 ∼ k − 1 1\sim k-1 1k1 的最短路转移过来。

  • 如果不经过第 k k k 个点,那么就是 d p [ k − 1 ] [ i ] [ j ] dp[k-1][i][j] dp[k1][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[k1][i][k]+dp[k1][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[k1][i][j],dp[k1][i][k]+dp[k1][k][j])
我们再仔细分析可以发现, d p [ k ] dp[k] dp[k] 只能由 d p [ k − 1 ] dp[k-1] dp[k1] 转移过来,如果你不想思考的话,这里显然也可以采用滚动数组来优化。

继续分析状态转移方程,不难发现 d p [ k − 1 ] [ i ] [ k ] = = d p [ k ] [ i ] [ k ] dp[k-1][i][k]==dp[k][i][k] dp[k1][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[k1][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[k1][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]) ∀1kn 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]);

五、作业

1.dijkstra算法

P3371 【模板】单源最短路径(弱化版)

P4779 【模板】单源最短路径(标准版)

P1346 电车

2.SPFA算法

P1807 最长路

P3385 【模板】负环

3.Floyd算法

B3647 【模板】Floyd 算法

P1476 休息中的小呆

P2419 [USACO08JAN] Cow Contest S

P6464 [传智杯 #2 决赛] 传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值