• 无向图相关算法基础


    从这篇文章开始介绍图相关的算法,这也是Algorithms在线课程第二部分的第一次课程笔记。 图的应用很广泛,也有很多非常有用的算法,当然也有很多待解决的问题,根据性质,图可以分为无向图和有向图。本文先介绍无向图,后文再介绍有向图。 之所以要研究图,是因为图在生活中应用比较广泛:

    无向图

    图是若干个顶点(Vertices)和边(Edges)相互连接组成的。边仅由两个顶点连接,并且没有方向的图称为无向图。 在研究图之前,有一些定义需要明确,下图中表示了图的一些基本属性的含义,这里就不多说明。

    图的API 表示

    在研究图之前,我们需要选用适当的数据结构来表示图,有时候,我们常被我们的直觉欺骗,如下图,这两个其实是一样的,这其实也是一个研究问题,就是如何判断图的形态。

    要用计算机处理图,我们可以抽象出以下的表示图的API:

      Graph的API的实现可以由多种不同的数据结构来表示,最基本的是维护一系列边的集合,如下:

    还可以使用邻接矩阵来表示:

    也可以使用邻接列表来表示:

    由于采用如上方式具有比较好的灵活性,采用邻接列表来表示的话,可以定义如下数据结构来表示一个Graph对象。

     1 public class Graph
     2 {
     3     private readonly int verticals;
     4 //顶点个数
     5     private int edges;
     6 //边的个数
     7     private List<int>[] adjacency;
     8 //顶点联接列表
     9  
    10     public Graph(int vertical)
    11     {
    12         this.verticals = vertical;
    13         this.edges = 0;
    14         adjacency=new List<int>[vertical];
    15         for (int v = 0; v < vertical; v++)
    16         {
    17             adjacency[v]=new List<int>();
    18         }
    19     }
    20  
    21     public int GetVerticals ()
    22     {
    23         return verticals;
    24     }
    25  
    26     public int GetEdges()
    27     {
    28         return edges;
    29     }
    30  
    31     public void AddEdge(int verticalStart, int verticalEnd)
    32     {
    33         adjacency[verticalStart].Add(verticalEnd);
    34         adjacency[verticalEnd].Add(verticalStart);
    35         edges++;
    36     }
    37  
    38     public List<int> GetAdjacency(int vetical)
    39     {
    40         return adjacency[vetical];
    41     }
    42 }

    图也分为稀疏图和稠密图两种,如下图: 在这两个图中,顶点个数均为50,但是稀疏图中只有200个边,稠密图中有1000个边。在现实生活中,大部分都是稀疏图,即顶点很多,但是顶点的平均度比较小。

    采用以上三种表示方式的效率如下:

    在讨论完图的表示之后,我们来看下在图中比较重要的一种算法,即深度优先算法:

    深度优先算法

    在谈论深度优先算法之前,我们可以先看看迷宫探索问题。下面是一个迷宫和图之间的对应关系: 迷宫中的每一个交会点代表图中的一个顶点,每一条通道对应一个边。

    迷宫探索可以采用Trémaux绳索探索法。即:

    • 在身后放一个绳子
    • 访问到的每一个地方放一个绳索标记访问到的交会点和通道
    • 当遇到已经访问过的地方,沿着绳索回退到之前没有访问过的地方:

    图示如下:

    下面是迷宫探索的一个小动画:

    深度优先搜索算法模拟迷宫探索。在实际的图处理算法中,我们通常将图的表示和图的处理逻辑分开来。所以算法的整体设计模式如下:

    • 创建一个Graph对象
    • 将Graph对象传给图算法处理对象,如一个Paths对象
    • 然后查询处理后的结果来获取信息

    下面是深度优先的基本代码,我们可以看到,递归调用dfs方法,在调用之前判断该节点是否已经被访问过。

     1 {
     2     private bool[] marked;
     3 //记录顶点是否被标记
     4     private int count;
     5 //记录查找次数
     6  
     7     private DepthFirstSearch(Graph g, int v)
     8     {
     9         marked = new bool[g.GetVerticals()];
    10         dfs(g, v);
    11     }
    12  
    13     private void dfs(Graph g, int v)
    14     {
    15         marked[v] = true;
    16         count++;
    17         foreach (int vertical in g.GetAdjacency(v))
    18         {
    19             if (!marked[vertical])
    20                 dfs(g,vertical);
    21         }
    22     }
    23  
    24     public bool IsMarked(int vertical)
    25     {
    26         return marked[vertical];
    27     }
    28  
    29     public int Count()
    30     {
    31         return count;
    32     }
    33 }

    试验一个算法最简单的办法是找一个简单的例子来实现。

    深度优先路径查询

    有了这个基础,我们可以实现基于深度优先的路径查询,要实现路径查询,我们必须定义一个变量来记录所探索到的路径。 所以在上面的基础上定义一个edgesTo变量来后向记录所有到s的顶点的记录,和仅记录从当前节点到起始节点不同,我们记录图中的每一个节点到开始节点的路径。为了完成这一日任务,通过设置edgesTo[w]=v,我们记录从v到w的边,换句话说,v-w是做后一条从s到达w的边。 edgesTo[]其实是一个指向其父节点的树。

     1 public class DepthFirstPaths
     2 {
     3     private bool[] marked;
     4 //记录是否被dfs访问过
     5     private int[] edgesTo;
     6 //记录最后一个到当前节点的顶点
     7     private int s;
     8 //搜索的起始点
     9  
    10     public DepthFirstPaths(Graph g, int s)
    11     {
    12         marked = new bool[g.GetVerticals()];
    13         edgesTo = new int[g.GetVerticals()];
    14         this.s = s;
    15         dfs(g, s);
    16     }
    17  
    18     private void dfs(Graph g, int v)
    19     {
    20         marked[v] = true;
    21         foreach (int w in g.GetAdjacency(v))
    22         {
    23             if (!marked[w])
    24             {
    25                 edgesTo[w] = v;
    26                 dfs(g,w);
    27             }
    28         }
    29     }
    30  
    31     public bool HasPathTo(int v)
    32     {
    33         return marked[v];
    34     }
    35  
    36     public Stack<int> PathTo(int v)
    37     {
    38  
    39         if (!HasPathTo(v)) return null;
    40         Stack<int> path = new Stack<int>();
    41  
    42         for (int x = v; x!=s; x=edgesTo[x])
    43         {
    44             path.Push(x);
    45         }
    46         path.Push(s);
    47         return path;
    48     }
    49 }

    上图中是黑色线条表示 深度优先搜索中,所有定点到原点0的路径, 他是通过edgeTo[]这个变量记录的,可以从右边可以看出,他其实是一颗树,树根即是原点,每个子节点到树根的路径即是从原点到该子节点的路径。 下图是深度优先搜索算法的一个简单例子的追踪。 

    广度优先算法

    通常我们更关注的是一类单源最短路径的问题,那就是给定一个图和一个源S,是否存在一条从s到给定定点v的路径,如果存在,找出最短的那条(这里最短定义为边的条数最小) 深度优先算法是将未被访问的节点放到一个堆中(stack),虽然在上面的代码中没有明确在代码中写stack,但是 递归 间接的利用递归堆实现了这一原理。 和深度优先算法不同, 广度优先是将所有未被访问的节点放到了队列中。其主要原理是:

    • 将 s放到FIFO中,并且将s标记为已访问
    • 重复直到队列为空
    1. 移除最近最近添加的顶点v
    2. 将v未被访问的节点添加到队列中
    3. 标记他们为已经访问

    广度优先是以距离递增的方式来搜索路径的。

     1 class BreadthFirstSearch
     2 {
     3     private bool[] marked;
     4     private int[] edgeTo;
     5     private int sourceVetical;
     6 //Source vertical
     7  
     8     public BreadthFirstSearch(Graph g, int s)
     9     {
    10         marked=new bool[g.GetVerticals()];
    11         edgeTo=new int[g.GetVerticals()];
    12         this.sourceVetical = s;
    13         bfs(g, s);
    14     }
    15  
    16     private void bfs(Graph g, int s)
    17     {
    18         Queue<int> queue = new Queue<int>();
    19         marked[s] = true;
    20         queue.Enqueue(s);
    21         while (queue.Count()!=0)
    22         {
    23             int v = queue.Dequeue();
    24             foreach (int w in g.GetAdjacency(v))
    25             {
    26                 if (!marked[w])
    27                 {
    28                     edgeTo[w] = v;
    29                     marked[w] = true;
    30                     queue.Enqueue(w);
    31                 }
    32             }
    33         }
    34     }
    35  
    36     public bool HasPathTo(int v)
    37     {
    38         return marked[v];
    39     }
    40  
    41     public Stack<int> PathTo(int v)
    42     {
    43         if (!HasPathTo(v)) return null;
    44  
    45         Stack<int> path = new Stack<int>();
    46         for (int x = v; x!=sourceVetical; x=edgeTo[x])
    47         {
    48             path.Push(x);
    49         }
    50         path.Push(sourceVetical);
    51         return path;
    52     }
    53  
    54 }

    广度优先算法的搜索步骤如下:

    广度优先搜索首先是在距离起始点为1的范围内的所有邻接点中查找有没有到达目标结点的对象,如果没有,继续前进在距离起始点为2的范围内查找,依次向前推进。

    总结

    本文简要介绍了无向图中的深度优先和广度优先算法,这两种算法时图处理算法中的最基础算法,也是后续更复杂算法的基础。其中图的表示,图算法与表示的分离这种思想在后续的算法介绍中会一直沿用,下文将讲解无向图中深度优先和广度优先的应用,以及利用这两种基本算法解决实际问题的应用。

    转自:http://blog.jobbole.com/79314/

  • 相关阅读:
    JS的基础语法
    PHP中的for循环
    我爱java系列---【自定义注解】
    开发中遇到的问题---【ERROR in ch.qos.logback.core.joran.spi.Interpreter@49:40
    开发中遇到的问题---【使用mybatis时 有一个sql查询不到结果 日志也显示查询为o 但是从日志中取出执行的sql到数据库客户端手动执行,可以查到数据】
    开发中遇到的问题---【 is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-pr oxying】
    开发中遇到的问题---【堡垒机跳转打开本地软件时,没有反应,怎么办?】
    开发中遇到的问题---【两个switch尽量不要套用】
    我爱java系列---【String.Split方法】
    少年启示录系列之---【少年】
  • 原文地址:https://www.cnblogs.com/lpxblog/p/5121016.html
Copyright © 2020-2023  润新知