关于最短路的随笔

  今天做了一个最短路的练习,前面几道都还比较水,最后一道不久以前做过,而且还纠结过很长一段时间,方法记下了,所以做出来了。可是回头看看自己的代码,发现似乎全部都是照搬的白书的代码。想要重新看看白书加深一下了解,却发现,有关最短路的好多东西都还没有了解过,比如说图的邻接表的使用以及优先队列的优化都还不曾了解。再往前一看,却发现最小生成树的方法居然也不记得了。所以又重新看了看书,加深一下了解,下面把有关最短路的问题先简单整理一下,待以后慢慢添加。

  首先是最小生成树,他指的是权值最小的没有环的图。而解最小生成树就有一个最经典的方法,那就是Kruskal。下面是伪代码

先将所有的边按照权值的从小到大排序
首先树为空
初始化连通分量,让每个节点自成一个独立的连通分量
for(对于每一条边e)
{
    如果e的左右端点不在同一个连通分量
    {
        边e加入到树中
        合并边e的左右顶点
    }
}

    上面的方法求出来的树就是要求的最小生成树。由于for循环里面是按照顺序拿出的每条边,而边又是按照从小到大的顺序排序了的,所以加起来一定是权值最小的。但是个人感觉上面代码写的相当抽象,什么叫一个连通分量,怎么找两个点在不在同一个连通分量,又怎么合并???

这里就用到了并查集的知识。正好并查集就是对集合的操作,而这个集合正好就可以表示上面的连通分量的概念,至于查找和连通正好又是并查集的最基本的操作。(所以说感觉好像只要是用Kruskal就一定得用并查集一样)。不多说,

并查集的查找是否在同一集合

  int find(int x) {return x == p[x] ? x : p[x] = find(p[x]);}

合并(x,y):

  int a = find(x);

  int b = find(y);

  p[a] = b;

到这里,最小生成树问题就顺利解决了。

    然后是一般的最短路问题,首先回顾一下最简单的flyod,它的思想就是暴力枚举a到b的最短距离肯定可以划分为a到{......}再到b的最短距离的和(当然集合也可以为空),这个随便就可以想明白的。当然他的缺点和优点也是显而易见的、

代码:

void flyod()
{
    for(int k=0;k<N;k++)
    {
        for(int i=0;i<N;i++)
        {
            for(int j=0;j<N;j++)
            {
                if(d[i][j] > d[i][k] + d[k][j])
                {
                    d[i][j] = d[i][k] + d[k][j];
                }
            }
        }
    }
}

然后看一看Dijkstra。它是求单源最短路最唱使用的方法之一。它的思想就是然每一步都是走的当前最短的距离。先看一图:点击此处

从图中可以看到每一步都是找到的路径最短的路所走的。先看代码:

 1 void dijkstra(int s)
 2 {
 3     for(int i=0;i<=N;i++) d[i] = INF;
 4     d[s] = 0;
 5     for(int i=0;i<N;i++)
 6     {
 7         int m = INF;
 8         for(int j = 0;j<N;j++)if(!vis[j] && d[j]<m)m=d[s=j];
 9         vis[s] = 1;
10         for(int j=0;j<N;j++)if(d[j] > d[s]+Map[s][j])d[j]=d[s]+Map[s][j];
11     }
12 }

    可以看到代码中首先将所有的d值赋值为无穷大,只有起点(源点)是0,这也就保证了下面的for循环中第一个找的的满足d[j]<m的点一定是起点,然后就从它求出它到其他所有点的最小权值。即上面的Map[a][b]表示边a到b的权值(Map[a][b]=INF相当于a到b的边不存在)。
    在下一次for(i=1)时,又首先找到一个d值最小的点(也就是到起点s最近的点),再求一次,又更新最短距离,这样的话每次都是从选择的从起点出发的新的最小权值的点,因此也就求出来了从起点到其他所有点的最短路径。又因为每一个外部循环,都会有一个顶点被标记,所以外部循环就至少N次,也只需要N次就够了(继续循环没有意义了)。

然后就看了一下邻接表的优化

    首先明确的就是邻接表只对稀疏图(也就是边的数目远远小于顶点的数目)作用比较明显,因为这时就可以不用管那些不存在的边,我觉的我还是的好好学学邻接表的使用,除了下了一两道hash题目外,好像再也没有用过邻接表了。它的复杂度就由O(n^2),可以减少到O(mlogn),m是边的数目。

 1 int n,m;
 2 int first[MAXN],next[MAXM],u[MAXM],v[MAXM],w[MAXM];
 3 void read_graph()
 4 {
 5     scanf("%d%d", &n, &m);
 6     for(int i=0;i<n;i++) first[i] = -1;
 7     for(int e=0;e<m;e++)
 8     {
 9         scanf("%d%d%d", &u[e],&v[e],&w[e]);
10         next[e] = first[u[e]];
11         first[u[e]] = e;
12     }
13 }

    既然上面使用了邻接表来存边,那么要如何实现mlogn的算法呢,这里就再讲讲优先队列的实现。

    简而言之,优先队列就是存放在队列里的元素不是按照他们的存进顺序排列的,而是按照我们自定义的元素优先级的大小排列的,优先级大的元素会被首先取出来。

    这样的话,那我们就可以在存放每一条边的时候,按照他们每一个点的d[]值作为优先级比较放进队列中,这样的话每次取出d值最小的点,也就相当于上面dijkstra代码里面的

         for(int j = 0;j<N;j++)if(!vis[j] && d[j]<m)m=d[s=j];

这一行语句,所以也就不用每次都循环n次来查找了。但是有个问题就是如果仅仅是将d值放进优先队列的话,在取出来,我们也不会知道它是属于哪一个顶点的值。
所以这里就又新添加了一个STL的东西,叫做pair,用它便可以将两个值捆绑在一起,在取出一个元素的时候,也就把它的d值和顶点编号一并取了出来(当然用结构体也是相当方便的)。
看代码
 1 struct cmp//定义优先队列的优先级比较
 2 {
 3     bool operator() (Pair a, Pair b)
 4     {
 5         return a.first < b.first;
 6     }
 7 };
 8 
 9 bool done[MAXN];
10 typedef pair<int, int> Pair;//用于捆绑d值和序号顶点序号
11 void dijkstra(int s)
12 {
13     mem(done);
14     for(int i=0;i<=N;i++) d[i] = INF;//初始时将suoyoud值设置为+∞
15     priority_queue<Pair, vector<Pair>, cmp>q;//定义一个优先队列
16     q.push(make_pair(d[s]=0, s));//将起点放入队列中,且只有起点的d值为0
17     while(!q.empty())//依次从优先队列中取出优先级最大的元素(也就是d值最小的点)直到为空
18     {
19         Pair top = q.top();  q.pop();
20         int x = top.second;
21         if(done[x]) continue;//如果此顶点已经算过了,不在讨论
22         done[x] = true;
23         for(int e = first[x]; e != -1; e = next[e])//枚举此个顶点的所有边
24         {
25             if(d[v[e]] > d[x] + w[e])
26             {
27                 d[v[e]] = d[x] + w[e];
28                 q.push(make_pair(d[v[e]], v[e]));//将新的d值变小的点放进优先队列
29             }
30         }
31     }
32 }

Bellman-Ford算法

    由于之前的算法都是针对于只含有正权的边的最短路,如果存在负权,那就该使用Bellman-Ford了。首先需要明确的是,如果存在负权的话,有可能最短路都会不存在(如果n个点形成了一个负权回路的话,那么每一个点再绕一个环回来后那么“最短路”又会缩小),所以Bellman-Ford就给我们提供了一个判断是否存在负权回路的方法。时间复杂度O(nm)

见代码:

 1 bool Bellman_Ford(int s)//判断是否存在最短路,如果存在,则d值保留起点s的单源最短路
 2 {
 3     for(int i = 1; i <= N; i ++) d[i] = INF;
 4     d[s] = 0;
 5     for(int i=1;i<N;i++)//由于由起点出发只需要N-1次就可以确定起点到其他所有点的最短路
 6     {
 7         for(int e = 0;e < M; e ++) //枚举每条边
 8         {
 9             int x = u[e], y = v[e];
10             if(d[x] < INF) d[y] = MIN(d[y], d[x] + w[e]);//松弛
11         }
12     }
13     for(int e = 0; e < M; e ++) if(d[u[e]] < INF)//再一次枚举所有边
14     {
15         int x = u[e], y = v[e];
16         if(d[y] > d[x] + w[e]) return false;//如果还有顶点可以松弛,存在负权回路,不存在最短路
17     }
18     return true;
19 }

有了上面dijkstra的思路,我们不难理解他的正确性,这里边不给出解释。

     同样,Bellman-Ford也可以用队列来优化,由于不再需要像dijkstra一样每次取出d值最小的顶点,所以我们也就不需要使用优先队列,而使用一般的队列便可以实现下面是白书上的一段代码:

 1 queue<int>q;
 2 bool inq[MAXN];
 3 for(int i = 0; i < n; i ++) d[i] = (i == s ? 0 : INF);
 4 memset(inq, 0, sizeof(inq));   //   “在队列中”的标志
 5 q.push(s);
 6 while(!q.empty())
 7 {
 8     int x = q.front(); q.pop();
 9     inq[x] = false;        //  清除“在队列中”的标志
10     for(int e = first[x]; e != -1; e = next[e]) if(d[v[e]] > d[x] + w[e])
11     {
12         d[v[e]] = d[x] +w[e];
13         if(!inq[d[e]])   //   如果已经在队列中,就不要重复添加了
14         {
15             inq[v[e]] = true;
16             q.push(v[e]);
17         }
18     }
19 }

我想如果明白了邻接表的使用和Bellman-Ford的思想,理解上面的代码应该问题就不大了。

copy的题目链接,慢慢刷

最短路:
原文地址:https://www.cnblogs.com/gj-Acit/p/3254311.html