图论算法与计算机

http://liuziming-2008.blog.163.com/blog/static/2572828520085265750851/

§1 最小生成树

11 生成树的概念
   
设图G(VE)是一个连通图,当从图中任一顶点出发遍历图G时,将边集E(G)分成两个集合A(G)B(G)。其中A(G)是遍历图时所经过的边的集合,B(G)是遍历图时未经过的边的集合。显然,G1(VA)是图G的子图,则称子图G1是连通图G的生成树。图的生成树不是惟一的。如对图1(a),当按深度和广度优先搜索法进行遍历就可以得到图1(b)(c)的两棵不同的生成树,并分别称之为深度优先生成树和广度优先生成树。

    

    对于有n个顶点的连通图,至少有n-1条边,而生成树中恰好有n-1条边,所以连通图的生成树是该图的极小连通子图。若图G的生成树中任意加一条边属于边集B(G)中的边,则必然形成回路。
   
求解生成树在许多领域有实际意义。例如,对于供电线路或煤气管道的铺设问题,即假设要把n个城市联成一个供电或煤气管道网络,则需要铺设n1条线路。任意两城市间可铺设一条线路,n个城市间最多可能铺设n(n1)/2条线路,各条线路的造价一般是不同的。一个很实际的问题就是如何在这些可能的线路中选择n-1条使该网络的建造费用最少,这就是下面要讨论的最小生成树问题。

1.2 网的最小生成树
   
在前面我们已经给出图的生成树的概念。这里来讨论生成树的应用。
   
假设,要在n个居民点之间敷设煤气管道。由于,在每一个居民点与其余n1个居民点之间都可能敷设煤气管道。因此,在n个居民点之间,最多可能敷设n(n-1)/2条煤气管道。然而,连通n个居民点之间的管道网络,最少需要n-1条管道。也就是说,只需要n-1条管道线路就可以把n个居民点间的煤气管道连通。另外,还需进一步考虑敷设每一条管道要付出的经济代价。这就提出了一个优选问题。即如何在n(n-1)/2条可能的线路中优选n-1条线路,构成一个煤气管道网络,从而既能连通n个居民点,又能使总的花费代价最小。
   
解决上述问题的数学模型就是求图中网的最小生成树问题。把居民点看作图的顶点,把居民点之间的煤气管道看作边,而把敷设各条线路的代价当作权赋给相应的边。这样,便构成一个带权的图,即网。对于一个有n个顶点的网可以生成许多互不相同的生成树,每一棵生成树都是一个可行的敷设方案。现在的问题是应寻求一棵所有边的权总和为最小的生成树。
   
如何构造这种网的最小生成树呢?下面给出这样一种解法:
    (1)
已知一个网,将网中的边按其权值由小到大的次序顺序选取。
    (2)
若选某边后不形成回路,则将其保留作为树的一条边;若选某边后形成回路,则将其舍弃,以后也不再考虑。
    (3)
如此依次进行,直到选够(n-1)条边即得到最小生成树。

    

    现以图2为例说明此算法。设此图是用边集数组EV表示的,且数组中各边是按权值由小到大的次序排列,如下表所示。

k

1

2

3

4

5

6

7

8

9

10

EV[k].p1

2

2

4

2

6

5

1

1

1

5

EV[k].p2

3

4

3

6

4

7

5

6

2

6

COST[EV[k].p1,EV[k].p2]

5

6

10

11

14

18

19

21

27

33

    按权值由小到大选取各边就是在数组中按下标k1en(图中边数)的次序选取。选前2条边(23)(24)时均无问题,保留作为树的边;到第3条边(43)时将与已保留的边形成回路,将其舍去;同样继续做:保留(2 6);舍去(64);保留(57)(15)(16),此时,保留的边数已够(n-1)=6条边,此时必定将7个顶点全部互相连通了,后面剩下的边(12)(56)就不必再考虑了。最后得到的最小生成树如图2a中深色边所示,其各边权值总和等于80。由离散数学中的图论可以证明,这就是最小生成树了,其权值最小。当图中有权值相等的边时,其最小生成树可能有不同的选取方案。
   
实现此算法的关键是,在选取某条边时应判断是否与已保留的边形成回路。
   
这可用将各顶点划分为集合的办法解决:假设数组tag(1..en)作为顶点集合划分的标志初值为0。在算法的执行过程中,当所选顶点uv是连通的,则将相应位置的tag[u]tag[v]置以相同的数字,而不连通的点在初期分属不同的集合,置不同的数字;一旦两个不同的连通分支连通了,则修改tag的值,将新的连通分支改为相同的数字。我们以图2为例。首先选(23)(24)边,由于是连通的,并且不出现回路。tag[2]:1tag[3]:=tag[4]:=1是同一个集合 A;选(62)边与A集合连通;tag[6]:=1;再选(57)与集合A不连通,tag[5]: tag[7]:2构成另一集合B;选(15)边与集合B连通,tag [1]:=2;此时,集合A {2346};集合B{571};当选(16)边时,(16)与集合A、集合B都连通,并且两个顶点分别属于两个不同的集合AB,这使得集合A与集合B通过边(16)连通。修改集合Btag的值,置为1,即将集合B并入集合A。边为n-1条,这就是一棵最小生成树。
   
根据集合标志数组tag的变化过程,很容易判断,选择一条新的边是否构成回路。当新选边的两个顶点uv,若tag[u]tag[v]相同并且均不等于0时,即uv已在生成树集合中被保留过,加入uv后即形成回路,不能选。而当tag [u]<>tag[v]或者tag[u]tag[v]0时,可以选并且不形成回路,说明uv中至少有一个顶点未被选过或者被选过的uv分别属于两个不同的集合,此时选择uv可以将含u的集合与含v的集合连通,修改tag数组。如此下去,到所有顶点均已属于一个集合时,此最小生成树就完全构成了。

网的最小生成树算法描述如下:
   
假设算法中用到的数据结构是经过处理的。
    COST(1..n
1..n)是带权数组存放网中顶点之间的权。EV(1..n*(n-1/2))按权从小到大存放排序后的顶点对,即EV[K].P1存放一个顶点,边的另一顶点存放在EV[K].P2之中。
    tag(1..n)
:顶点集合划分标志的数组。
    Enumb
:当前生成树的边数。
    SM
:当前权累计和。

PROC minspanningtree(VAR costVAR ev)

 Var tag

 BEGIN

   CALL INITIAL(tag);          

   Enumb:=0SM:=0; {诸参量初始化}          

   k:=1;    {边数累计}          

   WHILE (Enumb<=n-1) AND (k<n) DO          

    Begin U:=EV[k].P1V:=EV[k].P2    {选一对顶点(UV)}          

      CALL FIND(UT)  {找到含顶点U的集合T}          

      CALL FIND(VW)  {找到含顶点V的集合W}          

      IF (T<>W) THEN

      Begin

        write(uv)Enumb:=Enumb+1    {最小生成树增加一条边}          

        SM:=SM+COST[uv]          

        MERGE(TW){uv不会形成环,合并TW集合,并修改tag}          

      end         

      K:=K+1;  {找下一条边}          

    end          

   IF Enumb          

   ELSE write(SM)

 END

由算法可知图2的最小生成树的结果是(23)(24)(26)(57)(15)(16)

§2 最短路径

在一个赋权有向图上寻找最短路径问题也是图应用的一个重要课题。
   
假定图3中的有向图G=(VE)是一个航空图,V的每一顶点表示个城市,正中的每条弧v—>w表示从城市v到城市W的航线,弧V—>w上的标号代表从V城飞到w城所需要的时间。要寻找由该航空图上一给定城市到另一城市所需要的最短飞行时间。可以用求解这个有向图的单源最短路径算法来完成。

 

下面,我们讨论求解单源最短路径问题的贪心算法,也称Dijkstra算法。
   
设有向图G=(VE),其中,V={12n)cost是表示G的邻接矩阵,cost[ij]表示有向边(ij)的权。若不存在有向边(ij),则cost[ij]的权为无限大(oo)。令S是一个集合,其中的每个元素表示一个顶点,从源点到这些顶点的最短距离已经求出。
    (1)
令顶点V0为源点,集合S的初态只包含顶点V0,即S={V0}。数组dist记录从源点到其他各顶点当前的最短距离,其初值为dist[i]:=cost[v0i](i=2n)
    (2)
S之外的顶点集合V-S中选出一个顶点W,使dist[W]的值最小。于是,从源点到达W只通过S中的顶点,我们把W加入集合S
    (3)
调整dist中记录的从源点到V-S中每个顶点V的距离:从原来的dist[v]dist[w]+cost[wv]中选择较小的值作为新的dist[v]
    (4)
重复上述过程(2)和(3),直到S中包含V的全部顶点。
   
最终数组dist记录了从源点到V中其余各顶点的最短路径。
   
对图3所示的加权有向图应用Dijkstra算法,从源点V2出发到达各顶点的最短路径如下表所示。

                最短路径

  ---------------------------------------

  源点     中间顶点    终止顶点        长度

   2                      5           10

                          3           15

              3           4           30

              3           1           35

                          6           oo

  --------------------------------------

对图3的执行过程:初始时,S{2}dist[1]oodist[3]15dist[4]oodist[5]10dist[6]oo,第一遍处理时,W2使dist[5]最小、于是把5加入S。然后,调整dist中从源点到其余各顶点的距离:dist[3]15,为次小,将3加入Sdist[4]cost[23]+cost[34]=15+1530,经中间点3S{2534},同理,dist[1]cost[23]+cost[31]35S{25341},由于2没有一条到6的路径,所以dist[6]=oo
   
由此我们给出最短路径算法如下
PROC shortpath(VAR cost
VAR distVAR pathVAR SV0)
BEGIN
  FOR W:=1 TO n DO
  Begin dist[W]:=cost[V0
W];{最短路径初始化值}
    IF cost[V0
W]    THEN path[W]:=V0{path记载当前最短路径}
  End;
  S:=[V0];Vnum:=1
{到达点集合S和到达点S个数初值}
  WHILE (Vnum
最后一点已无选择余地}
  Begin Wm:=max
u:=V0;
    FOR W:=1 TO n DO
      IF(NOT W IN S) AND (dist[W]      THEN Begin U:=W
Wm:=dist[w] End;
      {
找最小dist[w]}
    S:=S+[U];Vnum:=Vnum+1; {U
为找到最短路径的终点}
    FOR W:=1 TO n DO
    IF (NOT W IN S) AND (dist[U]+cost[U
W]    THEN Begin dist[W]:=dist[U]+cost[UW]; {调整非S集各点最短路径值}
      path[W]:=U
{调整非S集各点最短路径}
    End;
    Vnum:=Vnum+1
  End;
END

PROC PRINTPATH(VAR dist VAR path
VAR SV0)
BEGIN
  FOR i:=1 TO n DO
  IF(i IN S)
  THEN Begin k:=i;
     WHILE (k<>V0) DO
      Begin write(k)
k:=path[k] End; {通过找前趋点,反向输出最短路径}
     write(k)
writeln(dist[i])
    End;
  ELSE Begin write(i
V0)writeln('max') End;
END

容易看出,算法short path的时间复杂度为O(n2),空间复杂度为O(n)

§3 拓扑排序

本节说明了如何用深度优先搜索,对一个有向无回路图进行拓扑排序。有向无回路图又称为dag。对这种有向无回路图的拓扑排序的结果为该图所有顶点的一个线性序列,满足如果G包含(u,v),则在序列中u出现在v之前(如果图是有回路的就不可能存在这样的线性序列)。一个图的拓扑排序可以看成是图的所有顶点沿水平线排成的一个序列,使得所有的有向边均从左指向右。因此,拓扑排序不同于通常意义上对于线性表的排序。

有向无回路图经常用于说明事件发生的先后次序,1给出一个实例说明早晨穿衣的过程。必须先穿某一衣物才能再穿其他衣物(如先穿袜子后穿鞋),也有一些衣物可以按任意次序穿戴(如袜子和短裤)。图1(a)所示的图中的有向边(u,v)表明衣服u必须先于衣服v穿戴。因此该图的拓扑排序给出了一个穿衣的顺序。每个顶点旁标的是发现时刻与完成时刻。图1(b)说明对该图进行拓扑排序后将沿水平线方向形成一个顶点序列,使得图中所有有向边均从左指向右。

下列简单算法可以对一个有向无回路图进行拓扑排序。

procedure Topological_Sort(G);

begin

 1.调用DFS(G)计算每个顶点的完成时间f[v];

 2.当每个顶点完成后,把它插入链表前端;

 3.返回由顶点组成的链表;

end;

1(b)说明经拓扑排序的结点以与其完成时刻相反的顺序出现。因为深度优先搜索的运行时间为θ(V+E),每一个v中结点插入链表需占用的时间为θ(1),因此进行拓扑排序的运行时间θ(V+E)

1 早晨穿衣的过程

为了证明算法的正确性,我们运用了下面有关有向无回路图的重要引理。

引理1

有向图G无回路当且仅当对G进行深度优先搜索没有得到反向边。

证明:

:假设有一条反向边(u,v),那么在深度优先森林中结点v必为结点u的祖先,因此G中从vu必存在一通路,这一通路和边(u,v)构成一个回路。

:假设G中包含一回路C,我们证明对G的深度优先搜索将产生一条反向边。设v是回路C中第一个被发现的结点且边(u,v)C中的优先边,在时刻d[v]vu存在一条由白色结点组成的通路,根据白色路径定理可知在深度优先森林中结点u必是结点v的后裔,因而(u,v)是一条反向边。(证毕)

定理1

Topological_Sort(G)算法可产生有向无回路图G的拓扑排序。

证明:

假设对一已知有问无回路图G=(V,E)运行过程DFS以确定其结点的完成时刻。那么只要证明对任一对不同结点u,vV,若G中存在一条从uv的有向边,则f[v]即可。考虑过程DFS(G)所探寻的任何边(u,v),当探寻到该边时,结点v不可能为灰色,否则v将成为u的祖先,(u,v)将是一条反向边,和引理1矛盾。因此,v必定是白色或黑色结点。若v是白色,它就成为u的后裔,因此f[v]。若v是黑色,同样f[v]。这样一来对于图中任意边(u,v),都有f[v],从而定理得证。(证毕)

另一种拓扑排序的算法基于以下思想:首先选择一个无前驱的顶点(即入度为0的顶点,图中至少应有一个这样的顶点,否则肯定存在回路),然后从图中移去该顶点以及由他发出的所有有向边,如果图中还存在无前驱的顶点,则重复上述操作,直到操作无法进行。如果图不为空,说明图中存在回路,无法进行拓扑排序;否则移出的顶点的顺序就是对该图的一个拓扑排序。

下面是该算法的具体实现:

procedure Topological_Sort_II(G);

begin 

1  for 每个顶点uV[G] do d[u]←0;  //初始化d[u],d[u]用来记录顶点u的入度

 

2  for 每个顶点uV[G] do

3    for 每个顶点vAdj[u] do d[v]←d[v]+1;  //统计每个顶点的入度

 

4  CreateStack(s);  //建立一个堆栈s

 

5  for 每个顶点uV[G] do       

6    if d[u]=0 then push(u,s);  //将度为0的顶点压入堆栈

 

7  count←0;             

 

8  while (not Empty(s)) do

     begin

9      u←top(s);  //取出栈顶元素

10     pop(s);     //弹出一个栈顶元素

11     count←count+1;

12     R[count]←u;   //线性表R用来记录拓扑排序的结果

 

13     for 每个顶点vAdj[u] do //对于每个和u相邻的节点v

                       begin

14                                    d[v]←d[v]-1;

15                                    if d[v]=0 then push(v,s);  //如果出现入度为0的顶点将其压入栈

                                       end;                                               

     end;

                   

16 if count<>G.size then writeln('Error! The graph has cycle.')

17                  else 按次序输出R;

end;

上面的算法中利用d[u]来记录顶点u的入度,第2-3行用来统计所有顶点的入度,第5-6行将入度为0的顶点压入堆栈,第8-15行不断地从栈顶取出顶点,将该顶点输出到拓扑序列中,并将所有与该顶点相邻的顶点的入度减1,如果某个顶点的入度减至0,则压入堆栈,重复该过程直到堆栈空了为止。显而易见该算法的复杂度为O(VE),因为第2-3行的复杂性就是O(VE),后面8-15行的复杂性也是O(VE)。这个算法虽然简单,但是没有前面一个算法的效率高。

§4 网络流算法:概念

在实际生活中有许多流量问题,例如在交通运输网络中的人流、车流、货物流,供水网络中的水流,金融系统中的现金流,通讯系统中的信息流,等等。50年代以福特(Ford)、富克逊(Fulkerson)为代表建立的网络流理论,是网络应用的重要组成部分。在最近的奥林匹克信息学竞赛中,利用网络流算法高效地解决问题已不是什么稀罕的事了。本节着重介绍最大流(包括最小费用)算法,并通过实际例子,讨论如何在问题的原型上建立个网络流模型,然后用最大流算法高效地解决问题。

[问题描述]如图4-1所示是联结某产品地v1和销售地v4的交通网,每一弧(vi,vj)代表从vivj的运输线,产品经这条弧由vi输送到vj,弧旁的数表示这条运输线的最大通过能力。产品经过交通网从v1v4。现在要求制定一个运输方案使从v1v4的产品数量最多。

 

4 - 1

4 - 2

一、基本概念及相关定理

1)网络与网络流
   
定义1 给一个有向图N=(VE),在V中指定一点,称为源点(记为vs,和另一点,称为汇点(记为vt),其余的点叫中间点,
对于E中每条弧(vivj)都对应一个正整数c(vivj)≥O(或简写成cij),称为f的容量,则赋权有向图N=(VEcvsvt)称为一个网络。如图4-1所给出的一个赋权有向图N就是一个网络,指定v1是源点,v4为汇点,弧旁的数字为cij
   
所谓网络上的流,是指定义在弧集合E上一个函数f={f(vivj)},并称f(vivj)为弧(vivj)上的流量(下面简记为fij)。如图4-2所示的网络N,弧上两个数,第一个数表示容量cij,第二个数表示流量fij

2)可行流与最大流
   
在运输网络的实际问题中,我们可以看出,对于流有两个显然的要求:一是每个弧上的流量不能超过该弧的最大通过能力(即弧的容量);二是中间点的流量为0,源点的净流出量和汇点的净流入量必相等且为这个方案的总输送量。因此有:
   
定义2 满足下列条件
    (1)
容量约束:0≤fij≤cij(vi,vj)E
    (2)
守恒条件
   
对于中间点:流入量=流出量;对于源点与汇点:源点的净流出量vs(f)=汇点的净流入量(-vt(f)
的流f,称为网络N上的可行流,并将源点s的净流量称为流f的流值v(f)
   
网络N中流值最大的流f*称为N的最大流。

3)可增广路径
   
所谓可增广路径,是指这条路径上的流可以修改,通过修改,使得整个网络的流值增大。
   
定义3 f是一个可行流,P是从源点s到汇点t的一条路,若p满足下列条件:
    (1)
p上的所有前向弧(vivj)都是非饱和弧,即0≤fijij 
    (2)
p上的所有后向弧(vivj)都是非零弧,即0ij≤cij
则称p(关于可行流f)一条可增广路径。

(4)割及其容量
   
定义4 如果AV的一个子集,A-=V-AsAtA-,则称边集(AA-)为网络N的一个割,显然,若把某一割的弧从网络中丢去,则从vsvt就不存在路。所以直观上讲,割是从vivj的必经之道。
   
定义5 给一割(AA-),把其中所有弧的容量之和称为这个割的容量,记为c(AA-),即
          c(A
A-)=c(e)
网络N中容量最小的割(A*A*-)称为N的最小割。
   
不难证明,任何一个可行流的流量v(f)都不会超过任一割的容量,即
          v(f)≤c(A
A-)
例如,图4-2中,若A={s}(AA-)={(sv3)(sv2)}c(AA-)=4+3=7

(5)有关定理
   
定理1 当且仅当不存在关于f*的增广路径,可行流f*为最大流。
   
证明 必要性:若f*是最大流,设N中存在关于f*的增广路径p,令:
          Q=min{min(cij-fij)
minf*ij}
   
由增广路径定义可知,Q>0,再令:
    f**ij =  f*ij+Q   (vi
vj) P的前向弧的集合
    f**ij =  f*ij-Q   (vi
vj) P的后向弧的集合
    f**ij =  f*ij     (vi
vj)不属于P的集合
不难证明{f**ij}可行流,且v(f**)=v(f*)+Q>v(f*)。这与f*是最大流假设矛盾,必要性证毕。
   
证明 充分性:设N中不存在关于f*的增广路径,证明f*是最大流。我们利用下面的方法来定义A*
   
vsA*
   
viA*,且fijij,则令vjA*
   
viA*,且fji>0,则令vjA*
因为不存在关于f*的增广路径,故vt不属于A*
   
A*-=V-A*,于是得到一个割(A*A*-),显然有
    f*ij=cij
(vivj)(A*A*-)
    f*ij=0
(vivj)(A*-A*)
所以v(f*)=c(A*A*-)。于是f*必是最大流,定理得证。
   
由上述证明中可得,若f*是最大流,则网络中必存在一个割集c(A*A*-),使得v(f*)=c(A*A*-)
   
于是有以下重要定理。
   
定理2 最大流最小割定理:在一个网络N中,从vsvt最大流的容量等于分离vsvt的最小割的容量。

二.最大网络流

最大流问题实际上是求一可行流{fij},使得v(f达到最大。定理1中证明实际上已为我们提供了寻求网络中最大流的一个方 法。若给了一个可行流f,只要判断N中有无关于f的增广路径,如果有增广路径,则按定理1的必要性证明中的办法,改进f 得到一个沈量增大的新的可行流;如果没有增广路径,则得到最大流。而利用定理1充分性证明中定义A*的办法,可以根据vt 否属于A*来判断N中有无关于f的增广路径。
   
下面我们用顶点标号法来定义A*,在标号过程中,有标号的顶点表示是A*中的点,没有标号的点表示不是A*中的点。如 vt有标号,则说明找到了一条增广路径;如果标号过程进行不下去,而vt又还没有标号,则说明不存在增广路径,于是得到了最大流,同时也得到了一个最小割集。

1)寻求最大流的标号法(Ford,Fulkerson)
   
从一个可行流(一般取零流)开始,不断进行以下的标号过程与调整过程,直到找不到关于f的可增广路径为止。
    (1)
标号过程
   
在这个过程中,网络中的点分为已标号点和未标号点,已标号点又分为已检查和未检查两种。每个标号点的标号信息表示两个 部分:第一标号表明它的标号从哪一点得到的,以便从vt开始反向追踪找出也增广路径;第二标号是为了表示该顶点是否已检查过。
   
标号开始时,给vs标上(s0),这时vs是标号但末检查的点,其余都是未标号的点,记为(00)
   
取一个标号而未检查的点vi,对于一切未标号的点vj
    A
.对于弧(vivj),若fij,则给vj标号(vi0),这时,vj点成为标号而未检查的点。
    B
.对于弧(vivj),若fji>0,则给vj标号(-vi0),这时,vj点成为标号而未检查的点。
   
于是vi成为标号且已检查的点,将它的第二个标号记为1。重复上述步骤,一旦vt被标上号,表明得到一条从vivt的增广路径p,转入调整过程。
   
若所有标号都已检查过去,而标号过程进行不下去时,则算法结束,这时的可行流就是最大流。
    (2)
调整过程
   
vt点开始,通过每个点的第一个标号,反向追踪,可找出增广路径P。例如设vt的第一标号为vk(-vk),则弧(vkvt)( 相应地(vtvk))p上弧。接下来检查vk的第一标号,若为vi(-vi),则找到(vivk)(或相应地(vkvi))。再检查vi的第一 标号,依此类推,直到vs为止。这时整个增广路径就找到了。在上述找增广路径的同时计算Q
          Q=min{min(cij-fij)
minf*ij}
   
对流f进行如下的修改:
    f'ij =  fij+Q   (vi
vj) P的前向弧的集合
    f'ij =  fij-Q   (vi
vj) P的后向弧的集合
    f'ij =  f*ij     (vi
vj)不属于P的集合
接着,清除所有标号,对新的可行流f’,重新进入标号过程。

你可能感兴趣的:(14,图论)