首页 > 技术文章 > 最短路

zhpisnotphz 2020-05-07 01:28 原文

Floyd 算法

Floyd 算法,号称五行算法,确实代码简洁,但是也有不足,就是跑得慢
Floyd算法时间复杂度为O(n^3)
基本思路也很简洁,采用的是最基本的动归:
有dp数组:dp[k][i][j]表示:使用前k号点作为中间媒介时,点i到点j之间的最短路径长度
则我们能够得到状态转移方程:
dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j])
类似01背包,这里可以在空间上做一定的优化:

我们可以注意到比如第i个阶段第j个状态它是从第i-1个阶段第j个状态前面转移过来的,所以我们完全只用一个一维的dp数组就够了,即我们只需要保存上一个阶段的所有状态,当前阶段的状态可以从这个数组中转移并储存,而不影响别的状态的转移(比如我们计算得到了当前阶段的第j个状态的值,我们覆盖掉保存的上一个阶段第j个状态的值,这不会影响后面状态的转移,因为后面的状态不会再用到这个值了)
但是我们要注意一点,前面已经说了,当前阶段的状态是从上一个阶段转移过来的,如果我们用一维dp数组,我们还是按顺序转移,即如下所示的转移过程:

那么有:
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
感性的直观认识即为:

对于第1个中间点k1,计算以k1作为中间点时,所有点两两之间的最小距离;对于第2个中间点k2,计算以k2作为中间点时,所有点两两之间的最小距离,如果这个距离比之前的小,就更新d[i][j]的值……如此遍历所有中间点k后,所有d[i][j]都更新到最小值。
代码为:

 for(k=1;k<=n;k++)
        for(i=1;i<=n;i++)
            for(j=1;j<=n;j++)
                if(e[i][j]>e[i][k]+e[k][j])
                     e[i][j]=e[i][k]+e[k][j];

Dijstra 算法

Dijstra的内核是贪心,属于动归的一种子集
每次在剩余节点中找到离起点最近的节点放到队列中,并且用来更新剩下的节点的距离,再将它标记上,表示已经找到它的最短路径,不再更新它了
这是基于:到一个节点的最短路径必然会经过比它离起点更近的节点,而如果一个节点的当前距离值比任何剩余节点都小,那么当前的距离值一定是最小的。
为了做到这一点,我们采用优先队列来实现

#include<iostream>
#include<queue>
#include<algorithm>
#define MAX 200
using namespace std;
struct Edge
{
	int to; int next; int w;
}edges[MAX];
int head[MAX];

struct Node
{
	int now; int weight;
};
//降序是相反
bool operator < (Node a, Node b) {
	return a.weight > b.weight;
}

priority_queue<Node> Q;
int cnt = 0, d[MAX];

void init() {
	memset(head, -1, sizeof(head));
	while (!Q.empty())Q.pop();
	memset(d, (int)(1e9+7), sizeof(d));
	cnt = 0;
}
void add(int from, int to, int weight) {
	edges[cnt].w = weight;
	edges[cnt].to = to;
	edges[cnt].next = head[from];
	head[from] = cnt++;
}
int main(){
	int T; cin >> T;
	while (T--) {
		init();
		int A, B, n; cin >> A >> B >> n;
		for (int i = 0; i < n; i++) {
			int a, b, w; cin >> a >> b >> w;
			add(a, b, w);
		}
		Q.push({ A,0 });
		while (!Q.empty()) {
			Node cur = Q.top();
			Q.pop();
			if (d[cur.now] < cur.weight)continue;
			for (int i = head[cur.now]; ~i; i = edges[i].next) {
				int to = edges[i].to;
				if (d[to] > cur.weight + edges[i].w)
					d[to] = cur.weight + edges[i].w, Q.push({ to,d[to] });
			}
		}
		cout << d[B] << endl;
	}
}

Spfa 算法

dijstra的局限性在于不能处理负权,因为采用的是贪心的方法
所以Bellman-Ford采用了对所有节点进行一次松弛操作
spfa是bellman-ford的队列优化版本,不需要将所有边拿来松弛,只有那些已经确定了最短路径的点所连出去的边才是有效的

bool inq[MAX_N];
void SPFA(int s)
{
	//初始化距离d,队列,inq队列标志 
	queue<int> que;
	memset(d, 0x3f, sizeof(d));
	d[s] = 0;
	memset(inq, 0, sizeof(inq));
	que.push(s);
	while(que.size())
	{
		int u = que.front();	que.pop();
		//清除标志 
		inq[u] = false;			
		//对每一条邻边松弛操作 
		for(int i = first[u]; i != -1; i = next[i])		 
			if(d[e[i].u] + e[i].w < d[e[i].v])
			{
				d[e[i].v] = d[e[i].u] + e[i].w;
				if(!inq[e[i].v])
				{
					inq[e[i].v] = true;
					que.push(e[i].v);
				}
			}
	}
}

和dijstra的最大区别在于,spfa是对所有已知点连出去的边进行了松弛
即,
该算法能够保证每更新一次都能确定一个节点的最短路,但与dijkstra不同的是,并不知道是那个节点的最短路被确定了,只是知道比上次多确定一个,这样进行n-1次更新后所有节点的最短路都确定了(源点的距离本来就是确定的)

Bellman-Ford判负环:

如果循环执行了n-1次,那就说明有负环,因为没有负环的话,对每个点都松弛了,
一定能够有最小值

bool find_negative_loop()
{
	memset(d, 0, sizeof(d));
	for(int i = 0; i < n; i++)
		for(int j = 0; j < m; j++)
		{
			int u = e[j].u, v = e[j].v, w = e[j].w;
			if(d[u] + w < d[v])
			{
				d[v] = d[u] + w;
				if(i == n-1)
				return true;
			}
		}
	return false;
} 

推荐阅读