签到

05月06日
尚未签到

共有回帖数 0

    做月子

    等级:
    图论杂谈系列(一):单源最短路
    话说这个系列春节之前就想写。不过因为过年,大家都懂得,免不了串门什么的,加上自己比较贪玩。不过好在今天晚上终于下决心写完了。
    首先来说这个系列的内容,计划图论要写十篇。不过正如题目所述,水平仅限于杂谈。不过也不会像市面上大部分算法书一样泛泛的讲,千篇一律。这些文章只讲一些片面的东西,不过是有启发性的。
    其次,本人是哈工程一名大二的学生。专业课没有开,水平也不是很高。知识都是自学的,看经典的算法书和网上大神写的论文。因为没有系统的学习过,谬误或者不严谨的地方肯定会有。所以恳请大家指正。
    最后想说的是,这个主要是写一些帮助大家思考的东西,注重思考的过程而不是得出的结论。

    对于求单源最短路,我们都知道bellman-ford 和 dijkstra 算法。思考一下bellman-ford算法是如何运行的。没错,对于每一个边都松弛一遍,执行v-1次。如果之后仍然存在边可以松弛,那么说明一定会有负权回路。
    代码清单:
       for(k=0 ;kn-1; k++)
              for(i=0;in;i++)
                     for(j=0;jn;j++)
                            if(map[j]  inf )
                                   relax (i , j , map[j]);
    算法的证明也不难,对于任何一个最短路来说,他一定不会包含回路(不考虑零权回路)。所以他的长度最多是v-1。这样的话,松弛v-1遍,对于每一条路径都可以让它成为最短路径。



    那么如何优化这个算法呢,首先我们会想到,如果没有负权回路,那么大多数情况下,它是不用执行v-1遍的。也就是说,执行n遍之后,如果没有边可以松弛的话,这个算法就结束了。所以我们可以加一个辅助变量和一个判断条件。
    代码清单:
          for(k=0;kn-1;k++)
    {
          flag = flase;
       for(i=0;in;i++)
                 for(j=0;jn;j++)
                        if(map[j]  inf)
                        {
                               relax(i,j,map[j]);
                               flag=true;
                        }
          if(flag)
                 break;
    }

    好的,如果没有负权回路的话,这样优化确实很不错。但是我们不是知足的人,可不可以提前判断是否有负权回路呢?
    我们都知道有向图判环的方法。很简单,深搜一遍,找到反向边就可以了。但是如何判断这是不是一个负权回路呢? 我们这里可以给每一个节点添加一个参数,如果遇到反向边的时候,distdist[v]+cost[e] 的话,那我们就可以确定这是一个负权回路了。下面的代码中,为了表示方便,我们用一个全局变量flag表示是否存在负权回路。
    代码清单:
       dfs( int x )
    {
          color[x]=1;
          for(i=0;in;i++)
          {
                 if(map[x] inf && color= =0)
    {
          dist=dist[x]+map[x]
                        dfs(i);
    }
                 if(map[x] inf && color==1)
                        if(dist+map[x]dist[x])
                               flag=true;
    }
    color[x]=2;
    }


    如果我们把这样一段程序写在前面,就可以提前判断是否存在负权回路了。
    不过这样的话依然存在存在一些并不完美的地方,比如执行第一层循环时如果某个点没有松弛的话,那么下次循环,这个点指向的其他的点依然也不会松弛。只有那些前一遍松弛中改变了距离估计值的点,才能引起他们的邻接点的距离估计值的改变。这样的话可以维持一个队列,让松弛过的点入队列。就有了spfa算法。
    我们可以在网上找到很多spfa算法的论文,其中可以看到一种优化方法。就是将距离估计值比较小的点放在队首,比较大的点放在队尾。这里没有用排序或者堆什么的,因为这样确实可以有几率优化,不过仅仅是“几率”。因为让拥有较大距离估计值的节点更容易被松弛,我们显然不希望让已经出队列的点又被松弛又回到了队列中。所以有了这样的一个概率型优化方法。

    然后我们分析一下用栈写的spfa算法,这里直接贴我的一段以前写过的c语言代码。
    void spfa(int s,int n,int m)
    {
       int top=0,u,v,e,i;
       for(i=1;i=n;i++)
       {
              vis=0;
              dist=inf;
       }
       dist[s]=0;
       stk[top++]=s;
       while(top0)
       {
              u=stk[--top];
              vis=0;
              for(e=head;e0;e=nxt[e])
              {
                     v=pnt[e];
                     if(relax(u,v,cost[e]) && vis[v] == 0)
                     {
                            vis[v]=1;
                            stk[top++]=v;
                     }
              }
       }
    }
    为什么用栈写有时候会很快呢? 首先它很节省空间,因为最多有v-1个节点同时在栈内。其次,它在栈底的点并不容易出栈,所以等到这些节点出栈的时候,它已经被松弛的差不多了。说白了还是一个概率型优化方法。
    spfa是一个不稳定的算法,用队列实现有点像广搜。当然大家可以用深搜实现一下。因为即使用栈写,那也是广度优先搜索的思路。因为它是‘一层一层’执行,而深搜是‘一个一个’执行。所以用深搜的思路写,效率可想而知了。


    洋洋洒洒,spfa就算说道这里了,后面讲到差分约束的时候还会继续讨论这个算法。
    接下来是dijkstra。一个思路很明晰的贪心算法。应用于正权有向图。之所以可以用贪心算法,是因为它是符合矩阵胚系统的。也可以叫做‘拟阵’。其实负权有向图也符合矩阵胚,理论上也可以用贪心算法。但是它没有一个好的选择策略,来判断那个点是已经不可能再松弛的了。所以并没有好的贪心算法去实现。
    Dijkstra伪代码:
       While(Q!=empty)
    {
          u=minheap();
          s=s+u;
          for(all v connected with u)
                 relax(u,v,cost);
    }
    这里需要注意relax这个过程是需要进行decrease-heap这个操作的
    relax(u,v,cost)
    {
    if(distdist[v]+cost)
    {
          dist=dist[v]+cost ;
          decrease(u);
    }
    }
    这里不得不提到c++中stl有一个priority queue,是不能直接访问类中元素的。所以不能进行decrease-heap操作。
    有一个方法就是在弹出节点的时候进行一次判断,看该节点是不是已经弹出过一次了。不过这样的话,时间复杂度会变成ElogE。


    楼主 2015-12-05 12:50 回复

共有回帖数 0
  • 回 帖
  • 表情 图片 视频
  • 发表

登录直线网账号

Copyright © 2010~2015 直线网 版权所有,All Rights Reserved.沪ICP备10039589号 意见反馈 | 关于直线 | 版权声明 | 会员须知