其他分享
首页 > 其他分享> > 最短路

最短路

作者:互联网

目录

前言

最短路算法是图论里最重要且最基础的算法之一(但是他也有很难的题)。

最短路算法的主要难点就是在于建模,建模建好了直接跑模板就行。

Dijkstra————单源最短路

朴素版本

先讲Dijkstra算法的朴素版本。

Dijkstra算法的思路是:

  1. 先把所有的dist[i] [1] 初始化成\(+\infty\) [2],然后把起始点的dist设为\(0\)。
  2. 然后找到一个不在S[3]里且距离S最近的一个点。
  3. 把此点加入S
  4. 更新此点的所有不在S里的邻接点,这个操作就叫松弛。
  5. 循环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次、如果还能松弛的话,就说明有负环了,因为只有有负环的情况,我才会一直不停的松弛,我们可以从这点的判断此图到底有没有负环。

  1. 先把所有点的dis设成\(+\infty\),然后把起始点的dis设成\(0\)。
  2. 把每条边都松弛一遍,重复n-1次。
  3. 最后判断还能不能松弛,如果还能松弛,就说明有负环,如果不能松弛了,就说明没有负环了。
代码
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。

思路:

  1. 先把起始点加入队列,dist[起始点(假设为s)]设为0,st[s] = 1(st表示此点在不在队列里面)。
  2. 找到队列开头的点,然后遍历此点的所有邻接点,如果dis[i[5]] > dist[tmp[6]] + w[i][7],就松弛掉,i点既然更新了,那么dist[i]肯定变小了,所以i点也得松弛一边,然后看i在不在队列里面(st[i] == false),如果在的话就不管,如果不在的话就把i点加入队列,然后把i点的状态设为在队列里。
  3. 循环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 \]


  1. 起点到i的最短路。 ↩︎

  2. 写代码的时候可以写作一个较大的数就行了。 ↩︎

  3. 已经求出(确定)了最短路的点的集合。 ↩︎

  4. 其实就是更新最短路的意思。 ↩︎

  5. 队列开头节点的邻接点。 ↩︎

  6. 队列开头节点。 ↩︎

  7. tmp到i的距离(输入的或算出来的,反正不是dist)。 ↩︎

标签:松弛,dist,int,短路,st,负环
来源: https://www.cnblogs.com/Akafuyu/p/15154356.html