最短路
作者:互联网
前言
最短路算法是图论里最重要且最基础的算法之一(但是他也有很难的题)。
最短路算法的主要难点就是在于建模,建模建好了直接跑模板就行。
Dijkstra————单源最短路
朴素版本
先讲Dijkstra算法的朴素版本。
Dijkstra算法的思路是:
- 先把所有的
dist[i]
[1] 初始化成\(+\infty\) [2],然后把起始点的dist
设为\(0\)。 - 然后找到一个不在
S
[3]里且距离S最近的一个点。 - 把此点加入
S
。 - 更新此点的所有不在
S
里的邻接点,这个操作就叫松弛。 - 循环1~4操作直到。
还有一个点:我们可以保证每次松弛至少可以确定一个点是最短路,所以最多只需要循环n次就可以找出所有点的最短路了。
我们可以看出:Dijkstra是一个基于贪心思想的算法,因为我每次都选的是最短的一条边,更新了也肯定是最短的。然后他的邻接点如果不是最短路那么也会被其他的点来松弛[4]一遍。
代码
所以我们就可以开始写代码了。
int Dijkstra()
{
memset(dist, 0x3f,sizeof dist); //0x3f是一个较大的值,所以我们用它来初始化。
dist[1]=0; //假设从1开始
for(int i=1;i<=n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
if(!st[j]&&(t==-1||dist[t]>dist[j])) //找到一个距离S最短的点(用了一个bool数组st来表示是不是在S里面)
t=j;
st[t]=true; //可以保证t这个点一定是起始点到t的最短路了,把t加入S
for(int j = 1; j <= n; j++)
dist[j] = min(dist[j], dist[t] + g[t][j]); //松弛t的邻接点
}
}
时间复杂度
\(O(n^2)\)
堆优化版本
我们发现找离S最近的是哪一个点可以用优先队列(堆)来实现。
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // 因为pair是先排first、再排second
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue; // 如果此点在st内,就不松弛此点。
st[ver] = true; //把ver加入S
for (int i = h[ver]; i != -1; i = ne[i]) //遍历邻接点
{
int j = e[i];
if (dist[j] > dist[ver] + w[i])
{
dist[j] = dist[ver] + w[i]; //松弛
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
然后最短路里会有一种奇妙的环,叫负环。
如图,2、3、4就组成了一个负环
如果用dijkstra算法来跑一个有自环的图,就会一直在这个负环上不停的跑。
因为2、3、4点每次松弛之后,他还有更小的、可以松弛到的最短路,所以我们定义:带有负环的图没有最短路(只有更短路)。
于是我们便引入了一种可以处理负环图的算法:Bellman-Ford算法。
Bellman-Ford————单源最短路
效率极低,但代码极好写。
Bellman-Ford的思路是:不停的进行松弛、每次松弛把每条边都松弛一遍,所以我们能保证每次一条边再也不能松弛了(即成为了最短路),所以只需要松弛n-1次就能求出每一个点的最短路。那如果我们已经松弛了n-1次、如果还能松弛的话,就说明有负环了,因为只有有负环的情况,我才会一直不停的松弛,我们可以从这点的判断此图到底有没有负环。
- 先把所有点的dis设成\(+\infty\),然后把起始点的dis设成\(0\)。
- 把每条边都松弛一遍,重复n-1次。
- 最后判断还能不能松弛,如果还能松弛,就说明有负环,如果不能松弛了,就说明没有负环了。
代码
bool Bellman_Ford()
{
for(int i = 1; i <= nodenum; ++i) //初始化
dis[i] = (i == original ? 0 : MAX);
for(int i = 1; i <= nodenum - 1; ++i)
for(int j = 1; j <= edgenum; ++j)
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(顺序一定不能反~)
{
dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
pre[edge[j].v] = edge[j].u;
}
bool flag = 1; //判断是否含有负权回路
for(int i = 1; i <= edgenum; ++i)
if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
{
flag = 0;
break;
}
return flag;
}
此代码没有任何用,因为复杂度极高,我们一般用Bellman-Ford的队列优化形态:SPFA算法来判断有无负环。
SPFA————单源最短路
用一个队列来维护节点,容易被菊花图和链式图卡掉。
所以做一些缺德考虑周到的lxl出题人的题目时,一定不要用SPFA。
思路:
- 先把起始点加入队列,dist[起始点(假设为s)]设为0,st[s] = 1(st表示此点在不在队列里面)。
- 找到队列开头的点,然后遍历此点的所有邻接点,如果dis[i[5]] > dist[tmp[6]] + w[i][7],就松弛掉,i点既然更新了,那么dist[i]肯定变小了,所以i点也得松弛一边,然后看i在不在队列里面(st[i] == false),如果在的话就不管,如果不在的话就把i点加入队列,然后把i点的状态设为在队列里。
- 循环2直至队列为空
注:如果要求有没有负环的话就用一个cnt数组记录每个点到源点的边数,一个点被更新一次就+1,一旦有点的边数达到了n那就证明存在了负环。
代码
无求负环版:
int spfa()
{
queue<PII> q;
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
q.push({0, 1});
st[1] = true;
while(q.size())
{
PII p = q.front();
q.pop();
int t = p.se;
st[t] = false; //t点被弹出,st[t] = false;
for(int i = h[t]; i != -1; i = ne[i]) //使用链式前向星存图
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i]; //松弛
if(!st[j]) //判断在不在队列内
{
st[j] = true;
q.push({dist[j], j});
}
}
}
}
if(dist[n] == 0x3f3f3f3f)
return -1;
else
return dist[n];
}
求负环版:
int spfa() {
memset(dist, 0x3f, sizeof dist);
for (int i = 1; i <= n; i++) {
q.push(i);
st[i] = true;
}
st[1] = true;
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]) {
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true; //有负环
if (!st[j]) {
st[j] = true;
q.push(j);
}
}
}
}
return false;
}
多源最短路
多源最短路————多次单源最短路
你懂我意思吧
Floyd算法
最好写的最短路!
设\(dist[i][j]\)为从i走到j的最短路是多少。
\(dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]\)
代码
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
\[The End
\]标签:松弛,dist,int,短路,st,负环 来源: https://www.cnblogs.com/Akafuyu/p/15154356.html