【Acwing】最短路算法

0 前言

最短路算法包含以下五种,注意区分

1

1 Dijkstra算法

该算法使用于单源最短路、无负边权的图

单源最短路就是指,从一个指定点出发到指定终点 的最短路径

该算法有两种写法,第一种是朴素版本Dijkstra,复杂度o(n^2),适合稠密图

另一个版本是采用堆优化的

适用于稀疏图,边少的时候

因为朴素版本最复杂的一步是在找最小距离的点,于是我们将dist用堆来存储,在找最小距离的点的时候,直接输出堆顶元素就好了,此时查找的复杂度是O(1)的。

于是算法的复杂度受限于更新边,总的算法复杂度为O(mlogn)

1.1 朴素版

  1. 849. Dijkstra求最短路 I

朴素版Dijkstra的思路就是两重遍历。参考这个视频的过程,
外层循环就是n次循环,每次从所有未标记的点中找到路径中的一个节点,当找完这n个点就找到了整个图的最短路径(相当于更新完毕n个点的dist数组)

内层循环则是 从当前所有未被标记的点中,选择距离最小的点。(按照视频中的,我们只需要处理出边的节点,但是这里直接遍历所有节点也能达到同样的效果,复杂度虽然没有那么完美,但是写法简单)


// dist[i]表示从起点到点i的最小距离之和,st[]是标记数组
 
int Dijkstra()
{
    memset(dist, 0x3f,sizeof dist);     //初始化距离  0x3f代表无限大

    dist[1]=0;  //第一个点到自身的距离为0

    for(int i=0;i<n;i++)      //有n个点所以要进行n次迭代
    {
        int t=-1;       //t存储当前访问的点,t=-1初始化一个不存在的点

        for(int j=1;j<=n;j++)   //这里的j代表的是从1号点开始,遍历所有未标记的点
            if(!st[j]&&(t==-1||dist[t]>dist[j])) 
                t=j;

        st[t]=true;   

        for(int j=1;j<=n;j++)           //依次更新每个点所到相邻的点路径值
            dist[j]=min(dist[j],dist[t]+g[t][j]);
    }

    if(dist[n]==0x3f3f3f3f) return -1;  //如果第n个点路径为无穷大即不存在最低路径
    return dist[n];
}

1.2 堆优化版Dijkstra算法

堆优化版本更加符合这个视频里描绘的过程

Dijkstra求最短路 II


// 稀疏图加边
void add(int x, int y, int z)
{
	e[idx] = y;    // e存当前节点的下标
	w[idx] = z;    // w存边的权重
	ne[idx] = h[x];  // ne存下一个节点下标
	h[x] = idx++;    // h[x] 表示x为起点连线的终点下标
}

int dijkstra(){
	memset(dist,0x3f,sizeof dist);  // 距离初始为无穷大
	dist[1] = 0;                    // 起点距离为0,注意dist数组存的是 起点 到 当前点 的 最短距离
	
    // 定义一个小根堆
    priority_queue<PII,vector<PII>,greater<PII>> heap;
	heap.push({0,1});    // 用pair存,第一个是dist,第二个是点的下标,因为小根堆排序的时候先first,再second
  
	while(heap.size())
	{
		PII t = heap.top();  // 每次取距离起点最小的点
		heap.pop();
		
		if(st[t.second]) continue;  // 如果该点已被访问过,跳过
		st[t.second] = true;
		
		for(int i = h[t.second]; i != -1; i = ne[i])    // 更新所有出边(朴素版是处理所有的边,但实际上只需要处理出边就好了)
		{
			int j = e[i];    // 注意h[t.second]和i存的是下标
			if(dist[j] > t.first + w[i]){
				dist[j] = t.first + w[i];  // 如果更新了节点,就要加入堆中,下次要从这些点里面找最小的距离点
			    heap.push({dist[j],j});
			}
		}
	}
	if(dist[n] == 0x3f3f3f3f) return -1;
	else return dist[n];
	
	
}


int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof h);    //稀疏图初始化时,h全部置为-1
	
	while(m--)
	{
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
	}
	
	cout<<dijkstra();
	return 0;
}

2 Bellman-ford、SPFA

Bellman-ford算法适合处理带有负权边的图,SPFA是对这个算法的优化。

对于负权图的最短路径问题,如果存在负权回路则无解,所以一般题目不会有这种负权环的存在。可以借助这两个算法去判断一个回路有没有负权回路。

一般情况下,对于这种问题的最短路,Bellmon_ford要进行n-1次迭代,就能得到最终结果(n个点,我只需要确定了n-1条边就能找出最短路径)

bellman-ford算法适合解决有边数限制的最短路问题

2.1 Bellman-ford算法

有边数限制的最短路

int bellman_ford() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for (int i = 0; i < k; i++) {    //k次循环,表示最多经过k条边
        memcpy(back, dist, sizeof dist);    // back数组保存上次迭代结束后的结果,如果不保存,可能在迭代中,数组已经发生变化了,就会导致结果错误(串联效应)
        for (int j = 0; j < m; j++) {    //遍历所有边
            int a = e[j].a, b = e[j].b, w = e[j].w;
            dist[b] = min(dist[b], back[a] + w);    // relax操作 
        }
    }
    if (dist[n] > 0x3f3f3f3f / 2) return -1;    // 为什么要大于0x3f3f3f3f/2, 因为负权边的存在,可能出现INF+或-一个很小的数,但是实际上这种情况还是不可达的状态
    else return dist[n];
}

2.2 SPFA算法

在relax操作中:dist[b] = min(dist[b], back[a] + w);,我们发现,只有back[a]变化的时候,dist[b]才会发生变化,所以我们可以用一个队列来存,只有当一个点的dist发生变化的时候,我们把这个点压入队列中,然后更新他的所有出边即可。

参考这篇题解,但是这里第四点说错了

第四点中,一般用SPFA的题目不会有负权回路,有负权回路就无解,虽然Bellman-ford算法能在负权回路的图中运行,但是结果是错的。你想想每次经过负权回路都会让你整条路径的权重减少,那么我算法就会无限走负权回路,你整条路径最终结果为-∞,显然不可能。

// 
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;    // st表示已经入队了,避免重复入队

    while (q.size())
    {
        int t = q.front();    // 取队头
        q.pop();

        st[t] = false;    // 可以再次入队了

        for (int i = h[t]; i != -1; i = ne[i])    // 更新所有出边,所以要用邻接表存储
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])  // relax操作
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])        // 如果还没入队,则入队
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return dist[n];
}

3 多源最短路 Floyd算法

多源最短路就是:每一个点到图中其他顶点的最短路

floyd算法中,d矩阵一开始存图,当算法结束后,这个d矩阵就会被更新为点与点之间的最短距离,所以你可以询问图中任意x,y两点的最短距离,即d[x][y]

// 核心代码
void floyd()
{
    for (int k = 1; k <= n; k ++ )    // 一定是k先循环
        for (int i = 1; i <= n; i ++ )    //i,j循环谁先谁后无所谓
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

posted @ 2023-07-03 16:25  wenli7363  阅读(35)  评论(0)    收藏  举报