图论中的常见算法分析比较和模板

                                                                   图论小结(一)


    从一开始搞ACM到现在也有几个年头了,而搞图论的时间可是从一开始搞ACM开始。所以,总是对图论有着一种独有的感情。图论的内容说难不难,但是确实在算法中日常生活中可以经常遇到,且一个很有趣的算法。这也是我当初选择图论的原因,但是图论的代码量一般都比较的大,当初就是看到别人啪啪的一下只就敲出了几百行的图论代码,而自己当时是佩服的不得了啊。可是进入图论后,才发现,一照进图论十年不想敲代码啊。。。。

   废话说太多了,其实图论内容很多。今天,最要总结一下最短路和生成树还有拓扑问题。还有一下更高深的网络流啊啥的一堆等下次再说。

  

一、最小生成树

解决最小生成树的问题,主要有两个算法。

1、Kruscal算法

2、Prime算法

Kruscal算法百度百科的解释及步骤:

   求加权连通图的最小生成树的算法。kruskal算法总共选择n- 1条边,所使用的贪婪准则是:从剩下的边中选择一条不会产生环路的具有最小耗费的边加入已选择的边的集合中。注意到所选取的边若产生环路则不可能形成一棵生成树。kruskal算法分e 步,其中e 是网络中边的数目。按耗费递增的顺序来考虑这e 条边,每次考虑一条边。当考虑某条边时,若将其加入到已选边的集合中会出现环路,则将其抛弃,否则,将它选入。

   空间复杂度为O(N*N)

   时间复杂度为O(N*logN)(根据排序的算法时间决定)

Prim算法百度百科的步骤:

  Prim算法用于求无向图的最小生成树
  设图G =(V,E),其生成树的顶点集合为U。
  ①、把v0放入U。
  ②、在所有u∈U,v∈V-U的边(u,v)∈E中找一条最小权值的边,加入生成树。
  ③、把②找到的边的v加入U集合。如果U集合已有n个元素,则结束,否则继续执行②。
  其算法的时间复杂度为O(n^2) Prim算法实现:
 (1)集合:设置一个数组set(i=0,1,..,n-1),初始值为 0,代表对应顶点不在集合中(注意:顶点号与下标号差1)
 (2)图用邻接阵表示,路径不通用无穷大表示,在计算机中可用一个大整数代替。
     采用堆可以将复杂度降为O(mlog n),如果采用Fibonacci堆可以将复杂度降为O(n log n + m)
 
注意:prim算法适合稠密图,其时间复杂度为O(n^2),其时间复杂度与边得数目无关,而 kruskal 算法的时间复杂度为 O(eloge)跟边的数目有关,适合稀疏图。

在推荐一个生成树讲的话的博客以供大家参考学习:博客链接


int Prim()
{
    int dis[N],ans = 0;           //生成树当前的最小花费,最终的最小花费
    for(int i = 1;i <= n;++i){    //初始化
       dis[i] = graph[1][i];
    }
    for(int i = 2;i <= n;++i){    //N个顶点要连通只须n-1条边
        int x = 1,m = INF;
        for(int j = 2;j <= n;++j){ //根据贪心,找出当前最短的一条边
           if(dis[j] != -1&&dis[j] < m)
              m = dis[x = j];
        }
        ans += m;
        dis[x] = -1;                //改点已经使用过,不可能在出现更小的情况
        for(int j = 2;j <= n;++j){
          /*
              这个if就是prim算法和Dijkstra算法的本质不同之处
              等提到Dijkstra算法时候,会着重提出。进行比较区分
          */
           if((dis[j]!=-1)&&(dis[j] > graph[x][j]))  //更新点(当前j点的最短距离,跟从x到j那个更小)
              dis[j] = graph[x][j];
        }
    }
    return ans;
}


二、最短路算法

   最短路算法最要使用的有三个Dijkstra算法和Bellman-Ford以及Floyd算法。而这三个算法有的都有自己的优化程序,下面会一一讲述。

Dijkstra算法:

      这个算法是通过为每个顶点 v 保留目前为止所找到的从s到v的最短路径来工作的。初始时,原点 s 的路径长度值被赋为 0 (d[s] = 0),若存在能直接到达的边(s,m),则把d[m]设为w(s,m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大,即表示我们不知道任何通向这些顶点的路径(对于 V 中所有顶点 v 除 s 和上述 m 外 d[v] = ∞)。当算法退出时,d[v] 中存储的便是从 s 到 v 的最短路径,或者如果路径不存在的话是无穷大。 Dijkstra 算法的基础操作是边的拓展:如果存在一条从 u 到 v 的边,那么从 s 到 v 的最短路径可以通过将边(uv)添加到尾部来拓展一条从 s 到 v 的路径。这条路径的长度是 d[u] + w(u, v)。如果这个值比目前已知的 d[v] 的值要小,我们可以用新值来替代当前 d[v] 中的值。拓展边的操作一直运行到所有的 d[v] 都代表从 s 到 v 最短路径的花费。这个算法经过组织因而当 d[u] 达到它最终的值的时候每条边(uv)都只被拓展一次。

时间复杂度我O(|V^2|)

但是可以优化到O(n*logn)优化程序可以看我以前写的文章:文章链接


function Dijkstra(G, w, s)
    for each vertex v in V[G]                        // 初始化
        d[v] := infinity                                 // 將各點的已知最短距離先設成無窮大
        previous[v] := undefined                         // 各点的已知最短路径上的前趋都未知
    d[s] := 0                                              // 因为出发点到出发点间不需移动任何距离,所以可以直接将s到s的最小距离设为0
    S := empty set
    Q := set of all vertices
    while Q is not an empty set                      // Dijkstra演算法主體
        u := Extract_Min(Q)
           S.append(u)
           for each edge outgoing from u as (u,v)
                  if d[v] > d[u] + w(u,v)             // 拓展边(u,v)。w(u,v)为从u到v的路径长度。
                        d[v] := d[u] + w(u,v)               // 更新路径长度到更小的那个和值。
                        previous[v] := u                    // 紀錄前趨頂點


  现在就说说刚才在Prim算法中提到了但是没有解释的那行程序吧。

Prim更新:if(d[y] != -1&&d[y]>graph[x][y]) d[y] = graph[x][y];   //d[i]==-1表示改点已经更新过

Dijkstra更新:if(d[y]>d[x]+w[x][y]) d[y] = d[x]+w[x][y];

  为什么会有这个区别呢?其实,只要你对这两个算法的本质理解了,你就自然会知道了。上面我们已经提到了Prim是求解最小生成树的,其终极目标是使得一个图连通且最终花费最小。而Dijkstra是求单源最短路的。即,给你一个起始点,叫你求出从这个起始点到各点或者给定目标点的最短距离。所以,我们在为Prim更新的时候考虑的是最终总的花费最小,而考虑Dijkstra是考虑起点到当前更新点的花费最小。

   所以,最终我得到的d[]也是有区别的。prim的d[]是表示当前连通改点最小的花费d[],而Dijkstra表示的是从起点到当前更新点的最小花费是d[].

 


图虽然搓了点,但是却可以很好的解释他们两个的区别。其实,是想画无向图的,就凑合着看吧。

如果,是prim的话是选择(1,2)+(2,3);而Dijkstra的话则是更新(1,3)(假设起点为1).现在应该知道他他们的区别了吧。


Bellman-Ford算法维基百科解释:

   它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。

      贝尔曼-福特算法与迪科斯彻算法类似,都以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。在两个算法中,计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。 然而,迪科斯彻算法以贪心法选取未被处理的具有最小权值得节点,然后对其的出边进行松弛操作;而贝尔曼-福特算法简单地对所有边进行松弛操作,共|E | − 1次,其中 |E |是图的边的数量。在重复地计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。这样的策略使得贝尔曼-福特算法比迪科斯彻算法适用于更多种类的输入。

   贝尔曼-福特算法的最多运行O(|V|·|E|)次,|V|和|E|分别是节点和边的数量)。


procedure BellmanFord(list vertices, list edges, vertex source)
   // 该实现读入边和节点的列表,并向两个数组(distance和predecessor)中写入最短路径信息

   // 步骤1:初始化图
   for each vertex v in vertices:
       if v is source then distance[v] := 0
       else distance[v] := infinity
       predecessor[v] := null

   // 步骤2:重复对每一条边进行松弛操作
   for i from 1 to size(vertices)-1:
       for each edge (u, v) with weight w in edges:
           if distance[u] + w < distance[v]:
               distance[v] := distance[u] + w
               predecessor[v] := u

   // 步骤3:检查负权环
   for each edge (u, v) with weight w in edges:
       if distance[u] + w < distance[v]:
           error "图包含了负权环"

 

 改算法同样也有一个很常用的优化算法SPFA算法,就是将检查的时候常常用FIFO来代替了循环检查。

/*
    Head[]为邻接表的表头
    Key[] 为邻接表的顶点
    Next[]为邻接表的下个节点
    w[]   为该点的权重
*/
void SPFA(int s)              //s 为起点
{
    queue<int> q;
    int c[MAXV];           //判断是否有负环
    bool inq[MAXV];        //在队列中的标记
    for(int i = 0;i < n;++i)
      d[i] = (i==s?0:INF);
    memset(inq,0,sizeof(inq));
    q.push(s);
    while(!q.empty())
    {
        int x = q.front();
        q.pop();
        inq[x] = 0;                   //清除在队列中的标记
        for(int e = Head[x];e != -1;e = Next[e])if(d[Key[e]] > d[x]+w[e]){
            d[Key[e]] = d[x] + w[e];
            if(!inq[Key[e]]){          //如果已经在队列中,就不要重复加了
               inq[Key[e]] = 1; 
               c[Key[e]]++;
               if(c[Key[e]] > MaxVerter)
                  RETURN "有负环"
               q.push(Key[e]);     
            }
        }
    }
}

判断有无负环:如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)





  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值