图论之最短路径(Bellman-Ford算法、Dijkstra算法、SPFA算法、Floyd-Warshall算法实现)

前言:

前几天考研复习了图论算法,现在抽点时间,写下博客记录下。


最短路径的定义:

给定图G(V,E),求一条从起点到终点的路径,使得这条路径上经过的所有边的边权之和最小。

常用的最短路算法有:Bellman-Ford算法、Dijkstra算法、SPFA算法、Floyd-Warshall算法。


Bellman-Ford算法:

Bellman-Ford算法以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。此算法在计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。Bellman-Ford算法 简单地对所有边进行松弛操作,共 V-1 次,其中 V 是图的点的数量。在重复地计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。Bellman-Ford算法适用于边权为负数的情况。

Bellman-Ford 的时间复杂度为: O ( V ∗ E ) O(V*E) O(VE)(V为图的顶点数、E为图的边数)。

代码如下:

//edges为无向图,边(起点、终点、权值),dist为源点s出发的最短路径值数组
void bellman_ford(int s)
{
    memset(dist, 0x3f, sizeof(dist));
    dist[s] = 0;
    //由于最短路不会经过同一顶点两次,所以最短路最多有n-1条边,因此有些时候可以利用这个性质检查图是否存在负圈
    for (int j = 0; j < n - 1; ++j)
    {
        for (int i = 0; i < edges.size(); ++i)
        {
            int a = edges[i][0], b = edges[i][1], w = edges[i][2];
            //更新边ab,终点b的最短路径值
            if (dist[a] != 0x3f3f3f3f && dist[b] > dist[a] + w)
            {
                dist[b] = dist[a] + w;
            }
        }
    }
}

Dijkstra算法:

Dijkstra算法基于贪心,贪心算法中最重要的一部分就是贪心策略,在这个算法中,贪心策略就是,去寻找点i,满足min(d[i]) i∈ B,满足这个条件的点 i,必定是无法被继续松弛的点,如果说要松弛点 i,那么必定通过 A 中或者 B 中的点进行更新,若通过 B 中的点j进行更新那么松弛之后的路径为d[j] + a[j][i]。由于 d[i] 已经是最小了,因此d[j]+a[j][i]>d[i] 因此不可能是通过 B 中的点进行松弛,若通过 A 中的点 m 进行松弛,由于 m 是点集A中的点,因此点 m 一定松弛过点i,重复的松弛没有意义的。因此,我们找到的点i,现在的d[i],一定是从源点到点 i 路径最小的点了。

Dijkstra+heap 优化的时间复杂度为: O ( ( V + E ) l o g V ) O((V+E)logV) O((V+E)logV)

void dijkstra(int s)
{
    memset(dist, 0x3f, sizeof(dist));
    dist[s] = 0;
    //pair的first表示最短路径,second表示源点s
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q; //小根堆
    q.push(make_pair(0, s));
    while (!q.empty())
    {
        pair<int, int> p = q.top();
        q.pop();
        int v = p.second;
        if (dist[v] < p.first)continue; //取出的最小值不是最短距离,丢弃该值
        for (const auto &edge : adjacent[v]){ //遍历顶点v的邻接点,进而更新顶点v的邻节点的最短距离
            //更新顶点v的邻接点的最短距离,并添加到q中,first为顶点v的邻接点,second为边v first的权值
            if (dist[edge.first] > dist[v] + edge.second)
            {
                dist[edge.first] = dist[v] + edge.second;
                q.push(make_pair(dist[edge.first], edge.first));
            }
        }
    }
}

SPFA算法:

SPFA即 Bellman-ford队列优化算法,代码实现与 Dijkstra 类似,二者都是用于求单源最短路径的算法。区别在于SPFA可以检测负权环:

利用 spfa 算法判断负环有两种方法:

  • 1) spfa 的 dfs 形式,判断条件是存在一点在一条路径上出现多次。
  • 2) spfa 的 bfs 形式,判断条件是存在一点入队次数大于总顶点数。

总的应用场景上:

  • 如果是稠密图,Dijkstra+heap 比 SPFA 快,稀疏图则是 SPFA 比较快
  • 如果求最短路径,则 SPFA+BFS 比较快,如果是负环检测,则是 SPFA+DFS 比较快。

SPFA+BFS 时间复杂度为 O ( V ∗ E ) O(V*E) O(VE)

//从源点s出发建立最短路径
void SPFA(int s) {
	queue<int> q;
	bool inq[maxn] = {false};
	for(int i = 1; i <= N; i++) dis[i] = 2147483647;
	dis[s] = 0;
	q.push(s); inq[s] = true;
	while(!q.empty()) {
		int x = q.front(); q.pop();
		inq[x] = false;
		for(int i = front[x]; i !=0 ; i = e[i].next) {
			int k = e[i].v;
			if(dis[k] > dis[x] + e[i].w) {
				dis[k] = dis[x] + e[i].w;
				if(!inq[k]) {
					inq[k] = true;
					q.push(k);
				}
			}
		}
	}
}

Floyd-Warshall算法:

Floyd-Warshall算法是求任意两点之间的最短路。此算法基于的事实是:如果存在顶点 k,使得以 k 作为中介点的顶点 i、j 的当前距离是缩短,即dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])

Floyd-Warshall算法的时间复杂度时间复杂度为 O ( n ∗ n ∗ n ) O(n*n*n) O(nnn)

void Floyd_Warshall(vector<vector<int>>& edges)
{
    //1、建立并初始化dp数组,若(i,j)之间存在边,dp[i][j]则为边ij的权值,否则为0x3f3f3f3f
    int dp[n][n];
    memset(dp, 0x3f, sizeof(dp));
    for (const auto &edge : edges)
    {
        dp[edge[0]][edge[1]] = dp[edge[1]][edge[0]] = edge[2];
    }
    //2、开始进行floyd算法求dp数组
    for (int k = 0; k < n; ++k)
    {
        for (int i = 0; i < n; ++i)
        {
            for (int j = 0; j < n; ++j)
            {
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
            }
        }
    }
}

你可能感兴趣的:(数据结构)