• 关于最短路的随笔


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

      首先是最小生成树,他指的是权值最小的没有环的图。而解最小生成树就有一个最经典的方法,那就是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的题目链接,慢慢刷

    最短路:
  • 相关阅读:
    git 学习
    公司领导写给新员工的信
    PLSQl远程连接oracle数据库
    hdu2222之AC自动机入门
    代码中添加事务控制 VS(数据库存储过程+事务) 保证数据的完整性与一致性
    ubuntu13.04安装SenchaArchitect-2.2无法启动的问题
    MVVMLight Toolkit在Windows Phone中的使用扩展之一:在ViewModel中实现导航,并传递参数
    面试题24:二叉搜索树与双向链表
    Struts2中的包的作用描述
    filter-mapping中的dispatcher使用
  • 原文地址:https://www.cnblogs.com/gj-Acit/p/3254311.html
Copyright © 2020-2023  润新知